refactor all

This commit is contained in:
Timi
2026-07-31 01:03:53 +08:00
parent 4e7f340cc6
commit e45809a0db
512 changed files with 14620 additions and 13519 deletions
@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.gao.annotation;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
///
/// 要求 GAO 模块权限
///
/// @author Codex
/// @since 2026-07-26
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireGaoPermission {
/// 所需权限
///
/// @return 权限列表
GaoPermissionCode[] value();
/// 匹配模式
///
/// @return 匹配模式
PermissionMatchMode mode() default PermissionMatchMode.ALL;
}
@@ -0,0 +1,46 @@
package com.imyeyu.api.modules.gao.annotation;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.PermissionMatchMode;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.java.TimiJava;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
///
/// GAO 权限注解处理器
///
/// @author Codex
/// @since 2026-07-27
@Component
public class RequireGaoPermissionInterceptor implements HandlerInterceptor {
private final PermissionChecker permissionChecker;
public RequireGaoPermissionInterceptor(PermissionChecker permissionChecker) {
this.permissionChecker = permissionChecker;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
RequireGaoPermission annotation = handlerMethod.getMethodAnnotation(RequireGaoPermission.class);
if (annotation == null || TimiJava.isEmpty(annotation.value())) {
return true;
}
String[] permissionCodeList = java.util.Arrays.stream(annotation.value()).map(GaoPermissionCode::getValue).toArray(String[]::new);
if (annotation.mode() == PermissionMatchMode.ANY) {
permissionChecker.checkAny(ModuleCode.GAO, permissionCodeList);
} else {
permissionChecker.checkAll(ModuleCode.GAO, permissionCodeList);
}
return true;
}
}
@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.gao.annotation;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
///
/// 要求 GAO 模块角色
///
/// @author Codex
/// @since 2026-07-26
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireGaoRole {
/// 所需角色
///
/// @return 角色列表
GaoRoleCode[] value();
/// 匹配模式
///
/// @return 匹配模式
RoleMatchMode mode() default RoleMatchMode.ALL;
}
@@ -0,0 +1,46 @@
package com.imyeyu.api.modules.gao.annotation;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.java.TimiJava;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
///
/// GAO 角色注解处理器
///
/// @author Codex
/// @since 2026-07-27
@Component
public class RequireGaoRoleInterceptor implements HandlerInterceptor {
private final RoleChecker roleChecker;
public RequireGaoRoleInterceptor(RoleChecker roleChecker) {
this.roleChecker = roleChecker;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
RequireGaoRole annotation = handlerMethod.getMethodAnnotation(RequireGaoRole.class);
if (annotation == null || TimiJava.isEmpty(annotation.value())) {
return true;
}
String[] roleCodeList = java.util.Arrays.stream(annotation.value()).map(GaoRoleCode::name).toArray(String[]::new);
if (annotation.mode() == RoleMatchMode.ANY) {
roleChecker.checkAny(ModuleCode.GAO, roleCodeList);
} else {
roleChecker.checkAll(ModuleCode.GAO, roleCodeList);
}
return true;
}
}
@@ -0,0 +1,94 @@
package com.imyeyu.api.modules.gao.bean;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.bean.BuiltinPermissionCode;
///
/// GAO 模块权限枚举
///
/// @author Codex
/// @since 2026-07-26
public enum GaoPermissionCode implements BuiltinPermissionCode {
CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建"),
CUSTOMER_READ("CUSTOMER:READ", "客户读取"),
CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改"),
CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除"),
REGISTRATION_EVENT_CREATE("REGISTRATION_EVENT:CREATE", "登记事件创建"),
REGISTRATION_EVENT_READ("REGISTRATION_EVENT:READ", "登记事件读取"),
REGISTRATION_EVENT_UPDATE("REGISTRATION_EVENT:UPDATE", "登记事件修改"),
REGISTRATION_EVENT_DELETE("REGISTRATION_EVENT:DELETE", "登记事件删除"),
REGISTRATION_EVENT_RECORD_CREATE("REGISTRATION_EVENT_RECORD:CREATE", "登记事件记录创建"),
REGISTRATION_EVENT_RECORD_READ("REGISTRATION_EVENT_RECORD:READ", "登记事件记录读取"),
REGISTRATION_EVENT_RECORD_UPDATE("REGISTRATION_EVENT_RECORD:UPDATE", "登记事件记录修改"),
REGISTRATION_EVENT_RECORD_DELETE("REGISTRATION_EVENT_RECORD:DELETE", "登记事件记录删除"),
STAT_READ("STAT:READ", "统计读取"),
QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建"),
USER_CREATE("USER:CREATE", "用户创建", true),
USER_READ("USER:READ", "用户读取", true),
USER_UPDATE("USER:UPDATE", "用户修改", true),
USER_DELETE("USER:DELETE", "用户删除", true);
final String value;
final String defaultName;
final boolean grantToStoreManager;
GaoPermissionCode(String value, String defaultName) {
this(value, defaultName, false);
}
GaoPermissionCode(String value, String defaultName, boolean grantToStoreManager) {
this.value = value;
this.defaultName = defaultName;
this.grantToStoreManager = grantToStoreManager;
}
@Override
public ModuleCode getModuleCode() {
return ModuleCode.GAO;
}
@Override
public String getValue() {
return value;
}
@Override
public String getDefaultName() {
return defaultName;
}
@Override
public boolean isGrantToSystem() {
return false;
}
@Override
public boolean isGrantToAdmin() {
return false;
}
/// true 为初始化时授权给店长角色
///
/// @return 是否授权给店长角色
public boolean isGrantToStoreManager() {
return grantToStoreManager;
}
}
@@ -0,0 +1,41 @@
package com.imyeyu.api.modules.gao.bean;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.bean.BuiltinRoleCode;
import lombok.AllArgsConstructor;
///
/// GAO 模块角色枚举
///
/// @author Codex
/// @since 2026-07-26
@AllArgsConstructor
public enum GaoRoleCode implements BuiltinRoleCode {
/// 店长
STORE_MANAGER("店长"),
EMPLOYEE("员工");
final String defaultName;
@Override
public ModuleCode getModuleCode() {
return ModuleCode.GAO;
}
@Override
public String getDefaultName() {
return defaultName;
}
@Override
public boolean isProtected() {
return false;
}
@Override
public String getParentCode() {
return null;
}
}
@@ -0,0 +1,190 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRole;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStatReq;
import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
///
/// GAO 客户接口
///
/// @author Codex
/// @since 2026-07-27
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/customer")
@RequireGaoRole(value = {GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY)
public class GaoCustomerController {
private static final String SIGN_IN_REGISTRATION_EVENT_NAME = "签到";
private final GaoCustomerService service;
private final GaoRegistrationEventRecordService registrationEventRecordService;
private final GaoRegistrationEventService registrationEventService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/list")
public PageResult<GaoCustomer> list(@RequestBody Page<GaoCustomer> page) {
PageResult<GaoCustomer> result = service.page(page);
fillLastSignInAt(result.getList());
return result;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/search")
public List<GaoCustomer> search(@RequestBody GaoCustomerSearchReq req) {
return service.search(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/detail")
public GaoCustomer detail(@RequestParam String id) {
return service.get(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/detail/code")
public GaoCustomer detailByCode(@RequestParam String code) {
return service.getByCode(code);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/invited/list")
public List<GaoCustomer> invitedList(@RequestParam String introducerCustomerId) {
return service.listInvited(introducerCustomerId);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_CREATE)
@PostMapping("/create")
public void create(@RequestBody GaoCustomer customer) {
service.create(customer);
}
@AOPLog
@RequestRateLimit
@PostMapping("/quick/register")
public GaoRegistrationEventRecord quickRegister(@RequestBody GaoQuickRegistrationEventRecordReq req) {
return registrationEventRecordService.quickRegister(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoCustomer customer) {
service.update(customer);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE)
@PostMapping("/invite/create")
public void createInvite(@RequestBody GaoCustomerInviteUpdateReq req) {
service.saveInviteRelation(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/invite/detail")
public GaoCustomer inviteDetail(@RequestParam String customerId) {
return service.get(customerId);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE)
@PostMapping("/invite/update")
public void updateInvite(@RequestBody GaoCustomerInviteUpdateReq req) {
service.saveInviteRelation(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE)
@PostMapping("/invite/delete")
public void deleteInvite(@RequestParam String customerId) {
service.deleteInviteRelation(customerId);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ)
@PostMapping("/stat/daily")
/// 按天统计客户新增数和累计总数
public List<GaoCustomerDailyStatView> dailyStat(@RequestBody GaoCustomerTrendStatReq req) {
return service.statDailyTrend(req);
}
private void fillLastSignInAt(List<GaoCustomer> customerList) {
if (customerList == null || customerList.isEmpty()) {
return;
}
String signInRegistrationEventId = null;
for (GaoRegistrationEvent registrationEvent : registrationEventService.listEnabled()) {
if (SIGN_IN_REGISTRATION_EVENT_NAME.equals(registrationEvent.getName())) {
signInRegistrationEventId = registrationEvent.getId();
break;
}
}
if (signInRegistrationEventId == null) {
return;
}
List<String> customerIdList = customerList.stream().map(GaoCustomer::getId).toList();
Map<String, GaoRegistrationEventRecord> recordMap = registrationEventRecordService.listLatestByCustomerIdListAndRegistrationEventId(customerIdList, signInRegistrationEventId).stream()
.collect(Collectors.toMap(GaoRegistrationEventRecord::getCustomerId, Function.identity(), (left, right) -> left));
for (GaoCustomer customer : customerList) {
GaoRegistrationEventRecord record = recordMap.get(customer.getId());
if (record != null) {
customer.setLastSignInAt(record.getRegisteredAt());
}
}
}
}
@@ -0,0 +1,82 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRole;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
///
/// GAO 登记事件接口
///
/// @author Codex
/// @since 2026-07-27
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/registration-event")
@RequireGaoRole(value = {GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY)
public class GaoRegistrationEventController {
private final GaoRegistrationEventService service;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_READ)
@PostMapping("/list")
public PageResult<GaoRegistrationEvent> list(@RequestBody Page<GaoRegistrationEvent> page) {
return service.pageWithRegistrationEventLimit(page);
}
@AOPLog
@RequestRateLimit
@PostMapping("/enabled/list")
public List<GaoRegistrationEvent> enabledList() {
return service.listEnabled();
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_READ)
@PostMapping("/detail")
public GaoRegistrationEvent detail(@RequestParam String id) {
return service.get(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_CREATE)
@PostMapping("/create")
public void create(@RequestBody GaoRegistrationEvent registrationEvent) {
service.create(registrationEvent);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoRegistrationEvent registrationEvent) {
service.update(registrationEvent);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
}
@@ -0,0 +1,192 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRole;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordTrendStatReq;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
///
/// GAO 登记事件记录接口
///
/// @author Codex
/// @since 2026-07-27
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/registration-event-record")
@RequireGaoRole(value = {GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY)
public class GaoRegistrationEventRecordController {
private final GaoRegistrationEventRecordService service;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/list")
public PageResult<GaoRegistrationEventRecord> list(@RequestBody Page<GaoRegistrationEventRecord> page) {
return service.page(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/detail")
public GaoRegistrationEventRecord detail(@RequestParam String id) {
return service.get(id);
}
@AOPLog
@RequestRateLimit
@PostMapping("/create")
public GaoRegistrationEventRecord create(@RequestBody GaoRegistrationEventRecordCreateReq req) {
return service.createRecord(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_UPDATE)
@PostMapping("/update")
public void update(@RequestBody GaoRegistrationEventRecord record) {
service.update(record);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/period/list")
public List<GaoRegistrationEventRecord> periodList(@RequestBody GaoRegistrationEventRecordPeriodReq req) {
return service.listByRegistrationEventAndPeriod(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/range/list")
public List<GaoRegistrationEventRecord> rangeList(@RequestBody GaoRegistrationEventRecordRangeReq req) {
return service.listByRange(req);
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ)
@PostMapping("/range/export")
public void rangeExport(@RequestBody GaoRegistrationEventRecordRangeReq req, HttpServletResponse resp) throws IOException {
List<GaoRegistrationEventRecord> list = service.listByRange(req);
resp.setContentType("application/vnd.ms-excel;charset=UTF-8");
resp.setHeader("Content-Disposition", "attachment; filename=\"gao-registration-event-record.csv\"");
resp.getOutputStream().write(buildCsv(list).getBytes(StandardCharsets.UTF_8));
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/period/stat")
public List<GaoRegistrationEventPeriodStatView> periodStat(@RequestBody GaoRegistrationEventRecordPeriodReq req) {
return service.statByRegistrationEventAndPeriodType(req.getRegistrationEventId(), req.getPeriodType());
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/stat")
public List<GaoCustomerRegisterStatView> customerStat(@RequestParam String customerId) {
return service.statByCustomerId(customerId);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/range/stat")
/// 按时间段统计单个客户各登记事件记录数
public List<GaoCustomerRegisterStatView> customerRangeStat(@RequestBody GaoRegistrationEventRecordStatReq req) {
return service.statByCustomerAndRange(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/all-customer/range/stat")
/// 按时间段统计全部客户各登记事件记录数
public List<GaoCustomerRegisterStatView> allCustomerRangeStat(@RequestBody GaoRegistrationEventRecordStatReq req) {
return service.statAllCustomerByRange(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/registration-event/daily/stat")
/// 按天统计单个客户单个登记事件记录趋势
public List<GaoRegistrationEventRecordDailyStatView> customerRegistrationEventDailyStat(@RequestBody GaoRegistrationEventRecordTrendStatReq req) {
return service.statDailyByCustomerAndRegistrationEvent(req);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/all-customer/registration-event/daily/stat")
/// 按天统计全部客户单个登记事件记录趋势
public List<GaoRegistrationEventRecordDailyStatView> allCustomerRegistrationEventDailyStat(@RequestBody GaoRegistrationEventRecordTrendStatReq req) {
return service.statDailyByRegistrationEvent(req);
}
private String buildCsv(List<GaoRegistrationEventRecord> list) {
StringBuilder builder = new StringBuilder();
builder.append((char) 65279);
builder.append("客户编码,客户姓名,登记事件,登记时间,登记人用户 ID,备注\n");
for (GaoRegistrationEventRecord record : list) {
builder.append(csv(record.getCustomer() == null ? null : record.getCustomer().getCode())).append(',');
builder.append(csv(record.getCustomer() == null ? null : record.getCustomer().getName())).append(',');
builder.append(csv(record.getRegistrationEvent() == null ? null : record.getRegistrationEvent().getName())).append(',');
builder.append(csv(record.getRegisteredAt())).append(',');
builder.append(csv(record.getOperatorUserId())).append(',');
builder.append(csv(record.getRemark())).append('\n');
}
return builder.toString();
}
private String csv(Object value) {
if (value == null) {
return "";
}
String text = value.toString();
if (!text.contains(",") && !text.contains("\"") && !text.contains("\n") && !text.contains("\r")) {
return text;
}
return "\"%s\"".formatted(text.replace("\"", "\"\""));
}
}
@@ -0,0 +1,36 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRole;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.service.RoleService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
///
/// GAO 角色接口
///
/// @author Codex
/// @since 2026-07-28
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/role")
@RequireGaoRole(GaoRoleCode.STORE_MANAGER)
public class GaoRoleController {
private final RoleService roleService;
@AOPLog
@RequestRateLimit
@PostMapping("/available/list")
public List<Role> availableList() {
return roleService.listByModuleCode(ModuleCode.GAO);
}
}
@@ -0,0 +1,96 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRole;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.api.modules.gao.service.GaoUserService;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.service.RoleService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
///
/// GAO 用户接口
///
/// @author Codex
/// @since 2026-07-30
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/user")
@RequireGaoRole(GaoRoleCode.STORE_MANAGER)
public class GaoUserController {
private final GaoUserService service;
private final RoleService roleService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/list")
public PageResult<GaoUser> list(@RequestBody Page<GaoUser> page) {
return service.pageWithUser(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/detail")
public GaoUser detail(@RequestParam String id) {
return service.get(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/detail/user")
public GaoUser detailByUserId(@RequestParam String userId) {
return service.getByUserId(userId);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_CREATE)
@PostMapping("/create")
public GaoUser create(@RequestBody GaoUser gaoUser) {
service.create(gaoUser);
return service.get(gaoUser.getId());
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_UPDATE)
@PostMapping("/update")
public GaoUser update(@RequestBody GaoUser gaoUser) {
service.update(gaoUser);
return service.get(gaoUser.getId());
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/role/list")
public List<Role> roleList() {
return roleService.listByModuleCode(ModuleCode.GAO);
}
}
@@ -0,0 +1,77 @@
package com.imyeyu.api.modules.gao.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.user.bean.Gender;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
///
/// 医疗护理客户
///
/// @author Codex
/// @since 2026-07-27
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoCustomer extends UUIDEntity {
///
/// 客户附件类型
///
/// @author Codex
/// @since 2026-07-27
public enum AttachType {
PHOTO
}
/// 客户编码
private String code;
/// 姓名
private String name;
/// 性别
private Gender gender;
/// 年龄
private Integer age;
/// 电话
private String telephone;
/// 住址
private String address;
/// 备注
private String remark;
/// 病症
private String disease;
/// 调理部位,结构由前端决定
private JsonNode conditioningPart;
/// 介绍人客户 ID
private String introducerCustomerId;
@Transient
private Long lastSignInAt;
@Transient
private GaoCustomer introducerCustomer;
@Transient
private Long invitedCount;
@Transient
private List<Attachment> attachmentList;
/// 临时照片附件 ID 列表
@Transient
private List<String> attachmentIdList;
}
@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
///
/// 登记事件
///
/// @author Codex
/// @since 2026-07-27
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoRegistrationEvent extends UUIDEntity {
/// 登记事件名称
private String name;
/// 说明
private String description;
/// true 为启用
private Boolean enabled;
/// true 为登记时要求拍照
private Boolean requirePhoto;
/// 排序值
private Integer sort;
/// 可登记角色代码列表
@Transient
private List<String> roleCodeList;
}
@@ -0,0 +1,61 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
///
/// 登记事件记录
///
/// @author Codex
/// @since 2026-07-27
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoRegistrationEventRecord extends UUIDEntity {
///
/// 登记事件记录附件类型
///
/// @author Codex
/// @since 2026-07-27
public enum AttachType {
PHOTO
}
/// 客户 ID
private String customerId;
/// 登记登记事件 ID
private String registrationEventId;
/// 操作管理员用户 ID
private String operatorUserId;
/// 备注
private String remark;
/// 登记时间
private Long registeredAt;
@Transient
private GaoCustomer customer;
@Transient
private GaoRegistrationEvent registrationEvent;
@Transient
private User operator;
@Transient
private List<Attachment> attachmentList;
/// 临时照片附件 ID 列表
@Transient
private List<String> attachmentIdList;
}
@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
///
/// 登记事件角色关联
///
/// @author Codex
/// @since 2026-07-28
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoRegistrationEventRoleRelation extends UUIDEntity {
/// 登记事件 ID
private String registrationEventId;
/// 角色代码
private String roleCode;
}
@@ -0,0 +1,38 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
///
/// GAO 用户
///
/// @author Codex
/// @since 2026-07-30
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoUser extends UUIDEntity {
/// 用户 ID
private String userId;
/// true 为启用
private Boolean enabled;
/// 备注
private String remark;
@Transient
private User user;
@Transient
private List<Role> roleList;
@Transient
private List<String> roleIdList;
}
@@ -0,0 +1,39 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
///
/// 客户 Mapper
///
/// @author Codex
/// @since 2026-07-27
public interface GaoCustomerMapper extends BaseMapper<GaoCustomer, String> {
@Select("SELECT * FROM `gao_customer` WHERE `code` = #{code} AND `deleted_at` IS NULL LIMIT 1")
GaoCustomer selectByCode(@Param("code") String code);
List<GaoCustomer> search(@Param("keyword") String keyword, @Param("introducerCustomerId") String introducerCustomerId);
List<GaoCustomer> selectInvitedList(@Param("introducerCustomerId") String introducerCustomerId);
Long countInvited(@Param("introducerCustomerId") String introducerCustomerId);
/// 统计每日新增客户数
///
/// @param beginCreatedAt 开始创建时间
/// @param endCreatedAt 结束创建时间
/// @return 每日新增客户数
List<GaoCustomerDailyStatView> statDailyNewCustomer(@Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt);
/// 统计开始时间前客户总数
///
/// @param beginCreatedAt 开始创建时间
/// @return 客户总数
Long countCreatedBefore(@Param("beginCreatedAt") Long beginCreatedAt);
}
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
///
/// 登记事件 Mapper
///
/// @author Codex
/// @since 2026-07-27
public interface GaoRegistrationEventMapper extends BaseMapper<GaoRegistrationEvent, String> {
@Select("SELECT * FROM `gao_registration_event` WHERE `enabled` = TRUE AND `deleted_at` IS NULL ORDER BY `sort` ASC, `created_at` ASC")
List<GaoRegistrationEvent> selectEnabledList();
}
@@ -0,0 +1,106 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
///
/// 登记事件记录 Mapper
///
/// @author Codex
/// @since 2026-07-27
public interface GaoRegistrationEventRecordMapper extends BaseMapper<GaoRegistrationEventRecord, String> {
/// 按登记事件查询客户最后登记事件记录
///
/// @param customerIdList 客户 ID 列表
/// @param registrationEventId 登记事件 ID
/// @return 最后登记事件记录列表
List<GaoRegistrationEventRecord> selectByRegistrationEventAndPeriod(@Param("registrationEventId") String registrationEventId, @Param("periodType") GaoRegistrationEventRecordPeriodReq.PeriodType periodType, @Param("periodValue") Integer periodValue);
/// 按周期统计登记事件记录
///
/// @param registrationEventId 登记事件 ID
/// @param periodType 周期类型
/// @return 统计结果
List<GaoRegistrationEventPeriodStatView> statByRegistrationEventAndPeriodType(@Param("registrationEventId") String registrationEventId, @Param("periodType") GaoRegistrationEventRecordPeriodReq.PeriodType periodType);
/// 统计客户所有登记
///
/// @param customerId 客户 ID
/// @return 统计结果
List<GaoCustomerRegisterStatView> statByCustomerId(@Param("customerId") String customerId);
/// 按时间段统计单个客户登记
///
/// @param customerId 客户 ID
/// @param beginRegisteredAt 开始登记时间
/// @param endRegisteredAt 结束登记时间
/// @return 统计结果
List<GaoCustomerRegisterStatView> statByCustomerIdAndRange(@Param("customerId") String customerId, @Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt);
/// 按时间段统计全部客户登记
///
/// @param beginRegisteredAt 开始登记时间
/// @param endRegisteredAt 结束登记时间
/// @return 统计结果
List<GaoCustomerRegisterStatView> statAllCustomerByRange(@Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt);
/// 统计单个客户单个登记事件的每日登记事件记录数
///
/// @param customerId 客户 ID
/// @param registrationEventId 登记事件 ID
/// @param beginRegisteredAt 开始登记时间
/// @param endRegisteredAt 结束登记时间
/// @return 每日统计
List<GaoRegistrationEventRecordDailyStatView> statDailyCountByCustomerIdAndRegistrationEventIdRange(@Param("customerId") String customerId, @Param("registrationEventId") String registrationEventId, @Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt);
/// 统计全部客户单个登记事件的每日登记事件记录数
///
/// @param registrationEventId 登记事件 ID
/// @param beginRegisteredAt 开始登记时间
/// @param endRegisteredAt 结束登记时间
/// @return 每日统计
List<GaoRegistrationEventRecordDailyStatView> statDailyCountByRegistrationEventIdRange(@Param("registrationEventId") String registrationEventId, @Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt);
/// 统计时间段前单个客户单个登记事件累计登记事件记录数
///
/// @param customerId 客户 ID
/// @param registrationEventId 登记事件 ID
/// @param beginRegisteredAt 开始登记时间
/// @return 累计登记数
Long countBeforeByCustomerIdAndRegistrationEventId(@Param("customerId") String customerId, @Param("registrationEventId") String registrationEventId, @Param("beginRegisteredAt") Long beginRegisteredAt);
/// 统计时间段前全部客户单个登记事件累计登记事件记录数
///
/// @param registrationEventId 登记事件 ID
/// @param beginRegisteredAt 开始登记时间
/// @return 累计登记数
Long countBeforeByRegistrationEventId(@Param("registrationEventId") String registrationEventId, @Param("beginRegisteredAt") Long beginRegisteredAt);
/// 查询客户指定登记事件的最后登记事件记录
///
/// @param customerIdList 客户 ID 列表
/// @param registrationEventId 登记事件 ID
/// @return 最后登记事件记录列表
List<GaoRegistrationEventRecord> selectLatestByCustomerIdListAndRegistrationEventId(@Param("customerIdList") List<String> customerIdList, @Param("registrationEventId") String registrationEventId);
/// 查询客户全部登记事件的最后登记事件记录
///
/// @param customerId 客户 ID
/// @return 最后登记事件记录列表
List<GaoRegistrationEventRecord> selectLatestByCustomerId(@Param("customerId") String customerId);
/// 按任意时间范围查询登记事件记录
///
/// @param req 范围查询请求
/// @return 登记事件记录列表
List<GaoRegistrationEventRecord> selectByRange(@Param("req") GaoRegistrationEventRecordRangeReq req);
}
@@ -0,0 +1,23 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRoleRelation;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
///
/// 登记事件角色关联 Mapper
///
/// @author Codex
/// @since 2026-07-28
public interface GaoRegistrationEventRoleRelationMapper extends BaseMapper<GaoRegistrationEventRoleRelation, String> {
@Select("SELECT `role_code` FROM `gao_registration_event_role_relation` WHERE `registration_event_id` = #{registrationEventId} AND `deleted_at` IS NULL ORDER BY `created_at` ASC")
List<String> selectRoleCodeListByRegistrationEventId(@Param("registrationEventId") String registrationEventId);
@Update("UPDATE `gao_registration_event_role_relation` SET `deleted_at` = UNIX_TIMESTAMP() WHERE `registration_event_id` = #{registrationEventId} AND `deleted_at` IS NULL")
void deleteByRegistrationEventId(@Param("registrationEventId") String registrationEventId);
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
///
/// GAO 用户 Mapper
///
/// @author Codex
/// @since 2026-07-30
public interface GaoUserMapper extends BaseMapper<GaoUser, String> {
@Select("SELECT * FROM `gao_user` WHERE `user_id` = #{userId} AND `deleted_at` IS NULL LIMIT 1")
GaoUser selectByUserId(@Param("userId") String userId);
}
@@ -0,0 +1,64 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStatReq;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
///
/// 客户服务
///
/// @author Codex
/// @since 2026-07-27
public interface GaoCustomerService extends BaseService<GaoCustomer, String> {
/// 按编码查询客户
///
/// @param code 客户编码
/// @return 客户
GaoCustomer getByCode(String code);
/// 按编码查询客户,不存在时返回 null
///
/// @param code 客户编码
/// @return 客户
GaoCustomer getNullableByCode(String code);
/// 按编码获取或快速创建客户
///
/// @param code 客户编码
/// @return 客户
GaoCustomer getOrQuickCreateByCode(String code);
/// 搜索客户
///
/// @param req 搜索请求
/// @return 客户列表
List<GaoCustomer> search(GaoCustomerSearchReq req);
/// 查询邀请的客户
///
/// @param introducerCustomerId 介绍人客户 ID
/// @return 被邀请客户列表
List<GaoCustomer> listInvited(String introducerCustomerId);
/// 保存邀请关系
///
/// @param req 邀请关系请求
void saveInviteRelation(GaoCustomerInviteUpdateReq req);
/// 删除邀请关系
///
/// @param customerId 客户 ID
void deleteInviteRelation(String customerId);
/// 统计客户每日趋势
///
/// @param req 趋势统计请求
/// @return 每日趋势列表
List<GaoCustomerDailyStatView> statDailyTrend(GaoCustomerTrendStatReq req);
}
@@ -0,0 +1,97 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordTrendStatReq;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
///
/// 登记事件记录服务
///
/// @author Codex
/// @since 2026-07-27
public interface GaoRegistrationEventRecordService extends BaseService<GaoRegistrationEventRecord, String> {
/// 创建登记事件记录
///
/// @param req 创建请求
/// @return 创建后的登记事件记录
GaoRegistrationEventRecord createRecord(GaoRegistrationEventRecordCreateReq req);
/// 快速录入客户并创建登记事件记录
///
/// @param req 快速录入请求
/// @return 创建后的登记事件记录
GaoRegistrationEventRecord quickRegister(GaoQuickRegistrationEventRecordReq req);
/// 按任意时间范围查询登记事件记录
///
/// @param req 范围查询请求
/// @return 登记事件记录列表
List<GaoRegistrationEventRecord> listByRange(GaoRegistrationEventRecordRangeReq req);
/// 按周期查询登记事件记录
///
/// @param req 周期请求
/// @return 登记事件记录列表
List<GaoRegistrationEventRecord> listByRegistrationEventAndPeriod(GaoRegistrationEventRecordPeriodReq req);
/// 按周期统计登记事件记录
///
/// @param registrationEventId 登记事件 ID
/// @param periodType 周期类型
/// @return 统计列表
List<GaoRegistrationEventPeriodStatView> statByRegistrationEventAndPeriodType(String registrationEventId, GaoRegistrationEventRecordPeriodReq.PeriodType periodType);
/// 查询客户指定登记事件的最后登记事件记录
///
/// @param customerIdList 客户 ID 列表
/// @param registrationEventId 登记事件 ID
/// @return 每个客户的最后登记事件记录
List<GaoRegistrationEventRecord> listLatestByCustomerIdListAndRegistrationEventId(List<String> customerIdList, String registrationEventId);
/// 统计客户所有登记
///
/// @param customerId 客户 ID
/// @return 统计结果
List<GaoCustomerRegisterStatView> statByCustomerId(String customerId);
/// 按时间段统计单个客户登记
///
/// @param req 统计请求
/// @return 统计结果
List<GaoCustomerRegisterStatView> statByCustomerAndRange(GaoRegistrationEventRecordStatReq req);
/// 按时间段统计全部客户登记
///
/// @param req 统计请求
/// @return 统计结果
List<GaoCustomerRegisterStatView> statAllCustomerByRange(GaoRegistrationEventRecordStatReq req);
/// 按时间段统计单个客户单个登记事件每日趋势
///
/// @param req 趋势统计请求
/// @return 每日趋势列表
List<GaoRegistrationEventRecordDailyStatView> statDailyByCustomerAndRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req);
/// 按时间段统计全部客户单个登记事件每日趋势
///
/// @param req 趋势统计请求
/// @return 每日趋势列表
List<GaoRegistrationEventRecordDailyStatView> statDailyByRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req);
/// 查询客户全部登记事件的最后登记事件记录
///
/// @param customerId 客户 ID
/// @return 每个登记事件的最后登记事件记录
List<GaoRegistrationEventRecord> listLatestByCustomerId(String customerId);
}
@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
///
/// 登记事件服务
///
/// @author Codex
/// @since 2026-07-27
public interface GaoRegistrationEventService extends BaseService<GaoRegistrationEvent, String> {
/// 分页查询登记事件并填充可登记角色
///
/// @param page 分页参数
/// @return 登记事件分页
PageResult<GaoRegistrationEvent> pageWithRegistrationEventLimit(Page<GaoRegistrationEvent> page);
/// 查询启用的登记事件
///
/// @return 登记事件列表
List<GaoRegistrationEvent> listEnabled();
/// 查询登记事件要求的角色
///
/// @param registrationEventId 登记事件 ID
/// @return 角色代码列表
List<String> listRoleCode(String registrationEventId);
}
@@ -0,0 +1,26 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
///
/// GAO 用户服务
///
/// @author Codex
/// @since 2026-07-30
public interface GaoUserService extends BaseService<GaoUser, String> {
/// 按用户 ID 查询 GAO 用户
///
/// @param userId 用户 ID
/// @return GAO 用户
GaoUser getByUserId(String userId);
/// 分页查询并填充用户详情
///
/// @param page 分页请求
/// @return GAO 用户分页
PageResult<GaoUser> pageWithUser(Page<GaoUser> page);
}
@@ -0,0 +1,243 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerInviteUpdateReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerSearchReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStatReq;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
///
/// 客户服务实现
///
/// @author Codex
/// @since 2026-07-27
@Service
@RequiredArgsConstructor
public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustomer, String> implements GaoCustomerService {
private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final GaoCustomerMapper mapper;
private final AttachmentService attachmentService;
@Override
protected BaseMapper<GaoCustomer, String> mapper() {
return mapper;
}
@Override
public void create(GaoCustomer entity) {
checkEntity(entity, false);
super.create(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity));
loadTransient(entity);
}
@Override
public void update(GaoCustomer entity) {
checkEntity(entity, true);
super.update(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity));
}
@Override
public GaoCustomer get(String id) {
GaoCustomer customer = super.get(id);
loadTransient(customer);
return customer;
}
@Override
public GaoCustomer getByCode(String code) {
TimiException.required(code, "not found customer.code");
GaoCustomer customer = mapper.selectByCode(code);
TimiException.required(customer, "not found customer");
loadTransient(customer);
return customer;
}
@Override
public GaoCustomer getNullableByCode(String code) {
TimiException.required(code, "not found customer.code");
GaoCustomer customer = mapper.selectByCode(code);
loadTransient(customer);
return customer;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoCustomer getOrQuickCreateByCode(String code) {
GaoCustomer customer = getNullableByCode(code);
if (customer != null) {
return customer;
}
customer = new GaoCustomer();
customer.setCode(code);
customer.setName(code);
create(customer);
return customer;
}
@Override
public List<GaoCustomer> search(GaoCustomerSearchReq req) {
if (req == null) {
req = new GaoCustomerSearchReq();
}
List<GaoCustomer> list = mapper.search(req.getKeyword(), req.getIntroducerCustomerId());
list.forEach(this::loadTransient);
return list;
}
@Override
public List<GaoCustomer> listInvited(String introducerCustomerId) {
TimiException.required(introducerCustomerId, "not found introducerCustomerId");
List<GaoCustomer> list = mapper.selectInvitedList(introducerCustomerId);
list.forEach(this::loadTransient);
return list;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void saveInviteRelation(GaoCustomerInviteUpdateReq req) {
TimiException.required(req, "not found invite req");
TimiException.required(req.getCustomerId(), "not found invite req.customerId");
GaoCustomer customer = super.get(req.getCustomerId());
customer.setIntroducerCustomerId(req.getIntroducerCustomerId());
checkEntity(customer, true);
super.update(customer);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteInviteRelation(String customerId) {
TimiException.required(customerId, "not found customerId");
GaoCustomer customer = super.get(customerId);
customer.setIntroducerCustomerId(null);
super.update(customer);
}
@Override
public List<GaoCustomerDailyStatView> statDailyTrend(GaoCustomerTrendStatReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getBeginCreatedAt(), "not found req.beginCreatedAt");
TimiException.required(req.getEndCreatedAt(), "not found req.endCreatedAt");
TimiException.requiredTrue(req.getEndCreatedAt() >= req.getBeginCreatedAt(), "req.endCreatedAt lt req.beginCreatedAt");
ZoneId zoneId = ZoneId.systemDefault();
LocalDate beginDate = Instant.ofEpochMilli(req.getBeginCreatedAt()).atZone(zoneId).toLocalDate();
LocalDate endDate = Instant.ofEpochMilli(req.getEndCreatedAt()).atZone(zoneId).toLocalDate();
checkDailyStatRange(beginDate, endDate);
long beginCreatedAt = beginDate.atStartOfDay(zoneId).toInstant().toEpochMilli();
long endCreatedAt = endDate.plusDays(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1;
Map<Integer, Long> newCustomerCountMap = new HashMap<>();
for (GaoCustomerDailyStatView view : mapper.statDailyNewCustomer(beginCreatedAt, endCreatedAt)) {
newCustomerCountMap.put(view.getDateValue(), TimiJava.defaultIfNull(view.getNewCustomerCount(), 0L));
}
long totalCustomerCount = TimiJava.defaultIfNull(mapper.countCreatedBefore(beginCreatedAt), 0L);
List<GaoCustomerDailyStatView> list = new ArrayList<>();
for (LocalDate date = beginDate; !date.isAfter(endDate); date = date.plusDays(1)) {
int dateValue = Integer.parseInt(DAY_FORMATTER.format(date));
long newCustomerCount = TimiJava.defaultIfNull(newCustomerCountMap.get(dateValue), 0L);
totalCustomerCount += newCustomerCount;
GaoCustomerDailyStatView view = new GaoCustomerDailyStatView();
view.setDateValue(dateValue);
view.setNewCustomerCount(newCustomerCount);
view.setTotalCustomerCount(totalCustomerCount);
list.add(view);
}
return list;
}
private void checkDailyStatRange(LocalDate beginDate, LocalDate endDate) {
long days = ChronoUnit.DAYS.between(beginDate, endDate) + 1;
TimiException.requiredTrue(days <= 366, "daily stat range too large");
}
private void checkEntity(GaoCustomer entity, boolean requireId) {
TimiException.required(entity, "not found customer");
if (requireId) {
TimiException.required(entity.getId(), "not found customer.id");
}
TimiException.required(entity.getCode(), "not found customer.code");
TimiException.required(entity.getName(), "not found customer.name");
GaoCustomer dbCustomer = mapper.selectByCode(entity.getCode());
if (dbCustomer != null && !dbCustomer.getId().equals(entity.getId())) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("客户编码已存在");
}
if (TimiJava.isNotEmpty(entity.getIntroducerCustomerId())) {
TimiException.requiredTrue(!entity.getIntroducerCustomerId().equals(entity.getId()), "customer.introducerCustomerId invalid");
TimiException.required(super.get(entity.getIntroducerCustomerId()), "not found introducer customer");
}
}
private void loadTransient(GaoCustomer customer) {
if (customer == null) {
return;
}
if (TimiJava.isNotEmpty(customer.getIntroducerCustomerId())) {
customer.setIntroducerCustomer(super.get(customer.getIntroducerCustomerId()));
}
customer.setInvitedCount(mapper.countInvited(customer.getId()));
customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId(), GaoCustomer.AttachType.PHOTO.name()));
}
private void saveAttachment(String customerId, List<Attachment> attachmentList) {
if (attachmentList == null) {
return;
}
for (Attachment attachment : attachmentList) {
attachment.setBizType(Attachment.BizType.GAO_CUSTOMER);
attachment.setBizId(customerId);
attachment.setAttachType(GaoCustomer.AttachType.PHOTO.name());
}
BizUpdateReq req = new BizUpdateReq();
req.setBizType(Attachment.BizType.GAO_CUSTOMER);
req.setBizId(customerId);
req.setItems(attachmentList);
attachmentService.updateByBiz(req);
}
private List<Attachment> resolveAttachmentList(GaoCustomer customer) {
if (customer.getAttachmentIdList() != null) {
return buildPhotoAttachmentList(customer.getAttachmentIdList());
}
return customer.getAttachmentList();
}
private List<Attachment> buildPhotoAttachmentList(List<String> attachmentIdList) {
List<Attachment> list = new ArrayList<>();
for (String attachmentId : attachmentIdList) {
if (attachmentId == null || attachmentId.isBlank()) {
continue;
}
Attachment attachment = new Attachment();
attachment.setTempFileId(attachmentId.trim());
attachment.setAttachType(GaoCustomer.AttachType.PHOTO.name());
list.add(attachment);
}
return list;
}
}
@@ -0,0 +1,325 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRecord;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventRecordService;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventPeriodStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRegisterStatView;
import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordPeriodReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordRangeReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordStatReq;
import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordTrendStatReq;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
///
/// 登记事件记录服务实现
///
/// @author Codex
/// @since 2026-07-27
@Service
@RequiredArgsConstructor
public class GaoRegistrationEventRecordServiceImplement extends AbstractEntityService<GaoRegistrationEventRecord, String> implements GaoRegistrationEventRecordService {
private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final GaoRegistrationEventRecordMapper mapper;
private final AttachmentService attachmentService;
private final RoleChecker roleChecker;
private final UserLoginService userLoginService;
private final UserService userService;
private final GaoCustomerService customerService;
private final GaoRegistrationEventService registrationEventService;
@Override
protected BaseMapper<GaoRegistrationEventRecord, String> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoRegistrationEventRecord createRecord(GaoRegistrationEventRecordCreateReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId");
String customerId = resolveCustomerId(req);
GaoRegistrationEvent registrationEvent = registrationEventService.get(req.getRegistrationEventId());
checkRegistrationEventPermission(registrationEvent);
if (Boolean.TRUE.equals(registrationEvent.getRequirePhoto())) {
TimiException.requiredTrue(req.getAttachmentIdList() != null && !req.getAttachmentIdList().isEmpty(), "required record photo");
}
GaoRegistrationEventRecord record = new GaoRegistrationEventRecord();
record.setCustomerId(customerId);
record.setRegistrationEventId(req.getRegistrationEventId());
record.setOperatorUserId(userLoginService.getRequireLoginUserId());
record.setRemark(req.getRemark());
record.setRegisteredAt(TimiJava.defaultIfNull(req.getRegisteredAt(), Time.now()));
super.create(record);
saveAttachment(record.getId(), buildPhotoAttachmentList(req.getAttachmentIdList()));
return get(record.getId());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoRegistrationEventRecord quickRegister(GaoQuickRegistrationEventRecordReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getCustomerCode(), "not found req.customerCode");
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId");
GaoCustomer customer = customerService.getOrQuickCreateByCode(req.getCustomerCode());
GaoRegistrationEventRecordCreateReq recordReq = new GaoRegistrationEventRecordCreateReq();
recordReq.setCustomerId(customer.getId());
recordReq.setRegistrationEventId(req.getRegistrationEventId());
recordReq.setRemark(req.getRemark());
recordReq.setAttachmentIdList(req.getAttachmentIdList());
return createRecord(recordReq);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoRegistrationEventRecord entity) {
TimiException.required(entity, "not found record");
TimiException.required(entity.getId(), "not found record.id");
TimiException.required(entity.getCustomerId(), "not found record.customerId");
TimiException.required(entity.getRegistrationEventId(), "not found record.registrationEventId");
customerService.get(entity.getCustomerId());
GaoRegistrationEvent registrationEvent = registrationEventService.get(entity.getRegistrationEventId());
checkRegistrationEventPermission(registrationEvent);
entity.setRegisteredAt(TimiJava.defaultIfNull(entity.getRegisteredAt(), Time.now()));
super.update(entity);
saveAttachment(entity.getId(), resolveAttachmentList(entity));
}
@Override
public GaoRegistrationEventRecord get(String id) {
GaoRegistrationEventRecord record = super.get(id);
loadTransient(record);
return record;
}
@Override
public List<GaoRegistrationEventRecord> listByRegistrationEventAndPeriod(GaoRegistrationEventRecordPeriodReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId");
TimiException.required(req.getPeriodType(), "not found req.periodType");
TimiException.required(req.getPeriodValue(), "not found req.periodValue");
List<GaoRegistrationEventRecord> list = mapper.selectByRegistrationEventAndPeriod(req.getRegistrationEventId(), req.getPeriodType(), req.getPeriodValue());
list.forEach(this::loadTransient);
return list;
}
@Override
public List<GaoRegistrationEventRecord> listByRange(GaoRegistrationEventRecordRangeReq req) {
TimiException.required(req, "not found req");
checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
List<GaoRegistrationEventRecord> list = mapper.selectByRange(req);
list.forEach(this::loadTransient);
return list;
}
@Override
public List<GaoRegistrationEventPeriodStatView> statByRegistrationEventAndPeriodType(String registrationEventId, GaoRegistrationEventRecordPeriodReq.PeriodType periodType) {
TimiException.required(registrationEventId, "not found registrationEventId");
TimiException.required(periodType, "not found periodType");
return mapper.statByRegistrationEventAndPeriodType(registrationEventId, periodType);
}
@Override
public List<GaoRegistrationEventRecord> listLatestByCustomerIdListAndRegistrationEventId(List<String> customerIdList, String registrationEventId) {
TimiException.required(registrationEventId, "not found registrationEventId");
if (TimiJava.isEmpty(customerIdList)) {
return Collections.emptyList();
}
List<GaoRegistrationEventRecord> list = mapper.selectLatestByCustomerIdListAndRegistrationEventId(customerIdList, registrationEventId);
list.forEach(this::loadTransient);
return list;
}
@Override
public List<GaoCustomerRegisterStatView> statByCustomerId(String customerId) {
TimiException.required(customerId, "not found customerId");
return mapper.statByCustomerId(customerId);
}
@Override
public List<GaoCustomerRegisterStatView> statByCustomerAndRange(GaoRegistrationEventRecordStatReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getCustomerId(), "not found req.customerId");
checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
return mapper.statByCustomerIdAndRange(req.getCustomerId(), req.getBeginRegisteredAt(), req.getEndRegisteredAt());
}
@Override
public List<GaoCustomerRegisterStatView> statAllCustomerByRange(GaoRegistrationEventRecordStatReq req) {
TimiException.required(req, "not found req");
checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
return mapper.statAllCustomerByRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
}
@Override
public List<GaoRegistrationEventRecordDailyStatView> statDailyByCustomerAndRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getCustomerId(), "not found req.customerId");
customerService.get(req.getCustomerId());
return buildDailyTrend(req, true);
}
@Override
public List<GaoRegistrationEventRecordDailyStatView> statDailyByRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req) {
TimiException.required(req, "not found req");
return buildDailyTrend(req, false);
}
@Override
public List<GaoRegistrationEventRecord> listLatestByCustomerId(String customerId) {
TimiException.required(customerId, "not found customerId");
List<GaoRegistrationEventRecord> list = mapper.selectLatestByCustomerId(customerId);
list.forEach(this::loadTransient);
return list;
}
private String resolveCustomerId(GaoRegistrationEventRecordCreateReq req) {
if (TimiJava.isNotEmpty(req.getCustomerId())) {
customerService.get(req.getCustomerId());
return req.getCustomerId();
}
TimiException.required(req.getCustomerCode(), "not found req.customerCode");
GaoCustomer customer = customerService.getByCode(req.getCustomerCode());
return customer.getId();
}
private void checkRegistrationEventPermission(GaoRegistrationEvent registrationEvent) {
List<String> roleCodeList = registrationEventService.listRoleCode(registrationEvent.getId());
if (roleCodeList != null && !roleCodeList.isEmpty()) {
roleChecker.checkAny(ModuleCode.GAO, roleCodeList.toArray(String[]::new));
}
}
private void checkStatRange(Long beginRegisteredAt, Long endRegisteredAt) {
TimiException.required(beginRegisteredAt, "not found beginRegisteredAt");
TimiException.required(endRegisteredAt, "not found endRegisteredAt");
TimiException.requiredTrue(endRegisteredAt >= beginRegisteredAt, "endRegisteredAt lt beginRegisteredAt");
}
private List<GaoRegistrationEventRecordDailyStatView> buildDailyTrend(GaoRegistrationEventRecordTrendStatReq req, boolean byCustomer) {
TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId");
registrationEventService.get(req.getRegistrationEventId());
checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt());
ZoneId zoneId = ZoneId.systemDefault();
LocalDate beginDate = Instant.ofEpochMilli(req.getBeginRegisteredAt()).atZone(zoneId).toLocalDate();
LocalDate endDate = Instant.ofEpochMilli(req.getEndRegisteredAt()).atZone(zoneId).toLocalDate();
checkDailyStatRange(beginDate, endDate);
long beginRegisteredAt = beginDate.atStartOfDay(zoneId).toInstant().toEpochMilli();
long endRegisteredAt = endDate.plusDays(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1;
Map<Integer, Long> dailyCountMap = new HashMap<>();
List<GaoRegistrationEventRecordDailyStatView> dbList = byCustomer
? mapper.statDailyCountByCustomerIdAndRegistrationEventIdRange(req.getCustomerId(), req.getRegistrationEventId(), beginRegisteredAt, endRegisteredAt)
: mapper.statDailyCountByRegistrationEventIdRange(req.getRegistrationEventId(), beginRegisteredAt, endRegisteredAt);
for (GaoRegistrationEventRecordDailyStatView view : dbList) {
dailyCountMap.put(view.getDateValue(), TimiJava.defaultIfNull(view.getDailyCount(), 0L));
}
long totalCount = byCustomer
? TimiJava.defaultIfNull(mapper.countBeforeByCustomerIdAndRegistrationEventId(req.getCustomerId(), req.getRegistrationEventId(), beginRegisteredAt), 0L)
: TimiJava.defaultIfNull(mapper.countBeforeByRegistrationEventId(req.getRegistrationEventId(), beginRegisteredAt), 0L);
List<GaoRegistrationEventRecordDailyStatView> list = new ArrayList<>();
for (LocalDate date = beginDate; !date.isAfter(endDate); date = date.plusDays(1)) {
int dateValue = Integer.parseInt(DAY_FORMATTER.format(date));
long dailyCount = TimiJava.defaultIfNull(dailyCountMap.get(dateValue), 0L);
totalCount += dailyCount;
GaoRegistrationEventRecordDailyStatView view = new GaoRegistrationEventRecordDailyStatView();
view.setDateValue(dateValue);
view.setDailyCount(dailyCount);
view.setTotalCount(totalCount);
list.add(view);
}
return list;
}
private void loadTransient(GaoRegistrationEventRecord record) {
if (record == null) {
return;
}
record.setCustomer(customerService.get(record.getCustomerId()));
record.setRegistrationEvent(registrationEventService.get(record.getRegistrationEventId()));
if (TimiJava.isNotEmpty(record.getOperatorUserId())) {
record.setOperator(userService.get(record.getOperatorUserId()));
}
record.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_REGISTRATION_EVENT_RECORD, record.getId(), GaoRegistrationEventRecord.AttachType.PHOTO.name()));
}
private void checkDailyStatRange(LocalDate beginDate, LocalDate endDate) {
long days = ChronoUnit.DAYS.between(beginDate, endDate) + 1;
TimiException.requiredTrue(days <= 366, "daily stat range too large");
}
private void saveAttachment(String recordId, List<Attachment> attachmentList) {
if (attachmentList == null) {
return;
}
for (Attachment attachment : attachmentList) {
attachment.setBizType(Attachment.BizType.GAO_REGISTRATION_EVENT_RECORD);
attachment.setBizId(recordId);
attachment.setAttachType(GaoRegistrationEventRecord.AttachType.PHOTO.name());
}
BizUpdateReq req = new BizUpdateReq();
req.setBizType(Attachment.BizType.GAO_REGISTRATION_EVENT_RECORD);
req.setBizId(recordId);
req.setItems(attachmentList);
attachmentService.updateByBiz(req);
}
private List<Attachment> resolveAttachmentList(GaoRegistrationEventRecord record) {
if (record.getAttachmentIdList() != null) {
return buildPhotoAttachmentList(record.getAttachmentIdList());
}
return record.getAttachmentList();
}
private List<Attachment> buildPhotoAttachmentList(List<String> attachmentIdList) {
if (attachmentIdList == null) {
return null;
}
List<Attachment> list = new ArrayList<>();
for (String attachmentId : attachmentIdList) {
if (attachmentId == null || attachmentId.isBlank()) {
continue;
}
Attachment attachment = new Attachment();
attachment.setTempFileId(attachmentId.trim());
attachment.setAttachType(GaoRegistrationEventRecord.AttachType.PHOTO.name());
list.add(attachment);
}
return list;
}
}
@@ -0,0 +1,127 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRoleRelation;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventRoleRelationMapper;
import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventMapper;
import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
///
/// 登记事件服务实现
///
/// @author Codex
/// @since 2026-07-27
@Service
@RequiredArgsConstructor
public class GaoRegistrationEventServiceImplement extends AbstractEntityService<GaoRegistrationEvent, String> implements GaoRegistrationEventService {
private final GaoRegistrationEventMapper mapper;
private final GaoRegistrationEventRoleRelationMapper roleRelationMapper;
@Override
protected BaseMapper<GaoRegistrationEvent, String> mapper() {
return mapper;
}
@Override
public PageResult<GaoRegistrationEvent> pageWithRegistrationEventLimit(Page<GaoRegistrationEvent> page) {
PageResult<GaoRegistrationEvent> result = page(page);
if (result.getList() != null) {
result.getList().forEach(this::loadRegistrationEventLimit);
}
return result;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoRegistrationEvent entity) {
checkEntity(entity, false);
super.create(entity);
saveRoleRelation(entity.getId(), entity.getRoleCodeList());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoRegistrationEvent entity) {
checkEntity(entity, true);
super.update(entity);
saveRoleRelation(entity.getId(), entity.getRoleCodeList());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
roleRelationMapper.deleteByRegistrationEventId(id);
super.delete(id);
}
@Override
public GaoRegistrationEvent get(String id) {
GaoRegistrationEvent registrationEvent = super.get(id);
registrationEvent.setRoleCodeList(roleRelationMapper.selectRoleCodeListByRegistrationEventId(id));
return registrationEvent;
}
@Override
public List<GaoRegistrationEvent> listEnabled() {
List<GaoRegistrationEvent> list = mapper.selectEnabledList();
list.forEach(this::loadRegistrationEventLimit);
return list;
}
@Override
public List<String> listRoleCode(String registrationEventId) {
return roleRelationMapper.selectRoleCodeListByRegistrationEventId(registrationEventId);
}
private void checkEntity(GaoRegistrationEvent entity, boolean requireId) {
TimiException.required(entity, "not found registrationEvent");
if (requireId) {
TimiException.required(entity.getId(), "not found registrationEvent.id");
}
TimiException.required(entity.getName(), "not found registrationEvent.name");
if (entity.getEnabled() == null) {
entity.setEnabled(true);
}
if (entity.getRequirePhoto() == null) {
entity.setRequirePhoto(false);
}
if (entity.getSort() == null) {
entity.setSort(0);
}
}
private void saveRoleRelation(String registrationEventId, List<String> roleCodeList) {
roleRelationMapper.deleteByRegistrationEventId(registrationEventId);
if (roleCodeList == null || roleCodeList.isEmpty()) {
return;
}
Set<String> distinctCodeSet = new LinkedHashSet<>(roleCodeList);
for (String roleCode : distinctCodeSet) {
if (roleCode == null || roleCode.isBlank()) {
continue;
}
GaoRegistrationEventRoleRelation relation = new GaoRegistrationEventRoleRelation();
relation.setRegistrationEventId(registrationEventId);
relation.setRoleCode(roleCode.trim());
roleRelationMapper.insert(relation);
}
}
private void loadRegistrationEventLimit(GaoRegistrationEvent registrationEvent) {
registrationEvent.setRoleCodeList(roleRelationMapper.selectRoleCodeListByRegistrationEventId(registrationEvent.getId()));
}
}
@@ -0,0 +1,172 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.api.modules.gao.mapper.GaoUserMapper;
import com.imyeyu.api.modules.gao.service.GaoUserService;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.mapper.UserMapper;
import com.imyeyu.api.modules.user.service.RoleService;
import com.imyeyu.api.modules.user.service.UserService;
import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeReq;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
///
/// GAO 用户服务实现
///
/// @author Codex
/// @since 2026-07-30
@Service
@RequiredArgsConstructor
public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, String> implements GaoUserService {
private final GaoUserMapper mapper;
private final UserMapper userMapper;
private final UserService userService;
private final RoleService roleService;
@Override
protected BaseMapper<GaoUser, String> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoUser entity) {
prepareUser(entity);
checkEntity(entity, false);
super.create(entity);
saveUserRole(entity);
loadUser(entity);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoUser entity) {
checkEntity(entity, true);
updateUser(entity);
super.update(entity);
saveUserRole(entity);
}
@Override
public GaoUser get(String id) {
GaoUser gaoUser = super.get(id);
loadUser(gaoUser);
return gaoUser;
}
@Override
public GaoUser getByUserId(String userId) {
TimiException.required(userId, "not found userId");
GaoUser gaoUser = mapper.selectByUserId(userId);
TimiException.required(gaoUser, "not found gao user");
loadUser(gaoUser);
return gaoUser;
}
@Override
public PageResult<GaoUser> pageWithUser(Page<GaoUser> page) {
PageResult<GaoUser> result = page(page);
loadUser(result.getList());
return result;
}
private void checkEntity(GaoUser entity, boolean requireId) {
TimiException.required(entity, "not found gao user");
if (requireId) {
TimiException.required(entity.getId(), "not found gao user.id");
}
TimiException.required(entity.getUserId(), "not found gao user.userId");
TimiException.required(userMapper.select(entity.getUserId()), "not found user");
GaoUser dbGaoUser = mapper.selectByUserId(entity.getUserId());
if (dbGaoUser != null && !dbGaoUser.getId().equals(entity.getId())) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("GAO 用户已存在");
}
if (entity.getEnabled() == null) {
entity.setEnabled(true);
}
}
private void prepareUser(GaoUser entity) {
TimiException.required(entity, "not found gao user");
if (TimiJava.isNotEmpty(entity.getUserId())) {
return;
}
User user = entity.getUser();
TimiException.required(user, "not found gao user.user");
userService.create(user);
entity.setUserId(user.getId());
}
private void updateUser(GaoUser entity) {
User reqUser = entity.getUser();
if (reqUser == null) {
return;
}
User user = userService.get(entity.getUserId());
if (reqUser.getName() != null) {
user.setName(reqUser.getName());
}
if (reqUser.getNick() != null) {
user.setNick(reqUser.getNick());
}
if (reqUser.getPhoneNo() != null) {
user.setPhoneNo(reqUser.getPhoneNo());
}
if (reqUser.getEmail() != null) {
user.setEmail(reqUser.getEmail());
}
userService.update(user);
}
private void saveUserRole(GaoUser entity) {
if (entity.getRoleIdList() == null) {
return;
}
UserRoleAuthorizeReq req = new UserRoleAuthorizeReq();
req.setUserId(entity.getUserId());
req.setModuleCode(ModuleCode.GAO);
req.setRoleIdList(entity.getRoleIdList());
roleService.authorizeUserRole(req);
}
private void loadUser(GaoUser gaoUser) {
if (gaoUser == null) {
return;
}
gaoUser.setUser(userMapper.select(gaoUser.getUserId()));
gaoUser.setRoleList(roleService.listByUserId(gaoUser.getUserId(), ModuleCode.GAO));
}
private void loadUser(List<GaoUser> gaoUserList) {
if (gaoUserList == null || gaoUserList.isEmpty()) {
return;
}
List<String> userIdList = gaoUserList.stream().map(GaoUser::getUserId).distinct().toList();
Map<String, User> userMap = userMapper.listByIdList(new ArrayList<>(userIdList)).stream()
.collect(Collectors.toMap(User::getId, Function.identity(), (left, right) -> left));
for (GaoUser gaoUser : gaoUserList) {
gaoUser.setUser(userMap.get(gaoUser.getUserId()));
List<Role> roleList = roleService.listByUserId(gaoUser.getUserId(), ModuleCode.GAO);
gaoUser.setRoleList(roleList);
}
}
}
@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户每日统计视图
///
/// @author Codex
/// @since 2026-07-28
@Data
public class GaoCustomerDailyStatView {
/// 日期值,格式为 yyyyMMdd
private Integer dateValue;
/// 当日新增客户数
private Long newCustomerCount;
/// 截止当日客户总数
private Long totalCustomerCount;
}
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户邀请关系更新请求
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoCustomerInviteUpdateReq {
/// 客户 ID
private String customerId;
/// 介绍人客户 ID
private String introducerCustomerId;
}
@@ -0,0 +1,30 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户登记统计视图
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoCustomerRegisterStatView {
/// 客户 ID
private String customerId;
/// 客户姓名
private String customerName;
/// 登记事件 ID
private String registrationEventId;
/// 登记事件名称
private String registrationEventName;
/// 数量
private Long count;
/// 最后登记时间
private Long latestRegisteredAt;
}
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户搜索请求
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoCustomerSearchReq {
/// 关键字,匹配编码、姓名、手机号、电话、住址、病症、条例部位
private String keyword;
/// 介绍人客户 ID
private String introducerCustomerId;
}
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 客户趋势统计请求
///
/// @author Codex
/// @since 2026-07-28
@Data
public class GaoCustomerTrendStatReq {
/// 开始创建时间
private Long beginCreatedAt;
/// 结束创建时间
private Long endCreatedAt;
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
///
/// 快速录入并登记请求
///
/// @author Codex
/// @since 2026-07-28
@Data
public class GaoQuickRegistrationEventRecordReq {
/// 客户编码
private String customerCode;
/// 登记登记事件 ID
private String registrationEventId;
/// 备注
private String remark;
/// 临时照片附件 ID 列表
private List<String> attachmentIdList = new ArrayList<>();
}
@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件周期统计视图
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoRegistrationEventPeriodStatView {
/// 登记事件 ID
private String registrationEventId;
/// 周期值
private Integer periodValue;
/// 数量
private Long count;
}
@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
///
/// 登记登记事件记录创建请求
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoRegistrationEventRecordCreateReq {
/// 客户 ID
private String customerId;
/// 客户编码,扫码登记可直接传这个
private String customerCode;
/// 登记事件 ID
private String registrationEventId;
/// 备注
private String remark;
/// 登记时间
private Long registeredAt;
/// 临时照片附件 ID 列表
private List<String> attachmentIdList = new ArrayList<>();
}
@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件记录每日统计视图
///
/// @author Codex
/// @since 2026-07-28
@Data
public class GaoRegistrationEventRecordDailyStatView {
/// 日期值,格式为 yyyyMMdd
private Integer dateValue;
/// 当日登记数
private Long dailyCount;
/// 截止当日累计登记数
private Long totalCount;
}
@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件记录按周期查询请求
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoRegistrationEventRecordPeriodReq {
///
/// 周期类型
///
/// @author Codex
/// @since 2026-07-27
public enum PeriodType {
DAY,
MONTH,
YEAR
}
/// 登记事件 ID
private String registrationEventId;
/// 周期类型
private PeriodType periodType;
/// 周期值,日为 yyyyMMdd,月为 yyyyMM,年为 yyyy
private Integer periodValue;
}
@@ -0,0 +1,24 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件记录范围查询请求
///
/// @author Codex
/// @since 2026-07-28
@Data
public class GaoRegistrationEventRecordRangeReq {
/// 客户 ID,为空查询全部客户
private String customerId;
/// 登记事件 ID,为空查询全部登记事件
private String registrationEventId;
/// 开始登记时间
private Long beginRegisteredAt;
/// 结束登记时间
private Long endRegisteredAt;
}
@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件记录统计请求
///
/// @author Codex
/// @since 2026-07-28
@Data
public class GaoRegistrationEventRecordStatReq {
/// 客户 ID,全部客户统计时不传
private String customerId;
/// 开始登记时间
private Long beginRegisteredAt;
/// 结束登记时间
private Long endRegisteredAt;
}
@@ -0,0 +1,24 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
///
/// 登记事件记录趋势统计请求
///
/// @author Codex
/// @since 2026-07-28
@Data
public class GaoRegistrationEventRecordTrendStatReq {
/// 客户 ID,全部客户趋势时不传
private String customerId;
/// 登记事件 ID
private String registrationEventId;
/// 开始登记时间
private Long beginRegisteredAt;
/// 结束登记时间
private Long endRegisteredAt;
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
///
/// 登记事件保存请求
///
/// @author Codex
/// @since 2026-07-27
@Data
public class GaoRegistrationEventSaveReq {
/// 登记事件
private GaoRegistrationEvent registrationEvent;
/// 可登记角色代码列表
private List<String> roleCodeList = new ArrayList<>();
}