diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml deleted file mode 100644 index 22e3b6c..0000000 --- a/.idea/data_source_mapping.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index eb4a267..1d0a883 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -3,6 +3,7 @@ + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5e66368..cf9e69d 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.imyeyu.timiserverapi TimiServerAPI - 1.0.8 + 1.0.9 jar TimiServerAPI imyeyu.com API @@ -173,12 +173,12 @@ com.imyeyu.spring timi-spring - 0.0.18 + 0.0.19 com.imyeyu.network timi-network - 0.0.13 + 0.0.14 com.imyeyu.lang @@ -188,12 +188,12 @@ com.imyeyu.java timi-java - 0.0.6 + 0.0.7 com.imyeyu.utils timi-utils - 0.0.12 + 0.0.13 com.imyeyu.font.icon diff --git a/src/main/java/com/imyeyu/api/annotation/RequireCoreRole.java b/src/main/java/com/imyeyu/api/annotation/RequireCoreRole.java deleted file mode 100644 index 6e3da68..0000000 --- a/src/main/java/com/imyeyu/api/annotation/RequireCoreRole.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.imyeyu.api.annotation; - -import com.imyeyu.api.bean.RoleMatchMode; -import com.imyeyu.api.bean.CoreRoleCode; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/// -/// 要求 CORE 模块角色 -/// -/// @author Codex -/// @since 2026-07-26 -@Target({ElementType.METHOD, ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface RequireCoreRole { - - /// 所需角色 - /// - /// @return 角色列表 - CoreRoleCode[] value(); - - /// 匹配模式 - /// - /// @return 匹配模式 - RoleMatchMode mode() default RoleMatchMode.ALL; -} diff --git a/src/main/java/com/imyeyu/api/annotation/RequireCoreRoleInterceptor.java b/src/main/java/com/imyeyu/api/annotation/RequireCoreRoleInterceptor.java deleted file mode 100644 index 3342560..0000000 --- a/src/main/java/com/imyeyu/api/annotation/RequireCoreRoleInterceptor.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.imyeyu.api.annotation; - -import com.imyeyu.api.bean.ModuleCode; -import com.imyeyu.api.bean.RoleMatchMode; -import com.imyeyu.api.bean.CoreRoleCode; -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; - -/// -/// CORE 角色注解处理器 -/// -/// @author Codex -/// @since 2026-07-27 -@Component -public class RequireCoreRoleInterceptor implements HandlerInterceptor { - - private final RoleChecker roleChecker; - - public RequireCoreRoleInterceptor(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; - } - RequireCoreRole annotation = handlerMethod.getMethodAnnotation(RequireCoreRole.class); - if (annotation == null || TimiJava.isEmpty(annotation.value())) { - return true; - } - String[] roleCodeList = java.util.Arrays.stream(annotation.value()).map(CoreRoleCode::name).toArray(String[]::new); - if (annotation.mode() == RoleMatchMode.ANY) { - roleChecker.checkAny(ModuleCode.CORE, roleCodeList); - } else { - roleChecker.checkAll(ModuleCode.CORE, roleCodeList); - } - return true; - } -} diff --git a/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java b/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java index 2de1037..fba6068 100644 --- a/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java +++ b/src/main/java/com/imyeyu/api/bean/CorePermissionCode.java @@ -25,9 +25,9 @@ public enum CorePermissionCode implements BuiltinPermissionCode { ROLE_PERMISSION_READ("ROLE_PERMISSION:READ", "角色权限读取", false, true), ROLE_PERMISSION_DELETE("ROLE_PERMISSION:DELETE", "角色权限删除", false, true), - ROLE_RELATION_CREATE("ROLE_RELATION:CREATE", "角色关联创建", false, true), - ROLE_RELATION_READ("ROLE_RELATION:READ", "角色关联读取", false, true), - ROLE_RELATION_DELETE("ROLE_RELATION:DELETE", "角色关联删除", false, true), + ROLE_PERMISSION_INHERIT_CREATE("ROLE_PERMISSION_INHERIT:CREATE", "角色权限继承创建", false, true), + ROLE_PERMISSION_INHERIT_READ("ROLE_PERMISSION_INHERIT:READ", "角色权限继承读取", false, true), + ROLE_PERMISSION_INHERIT_DELETE("ROLE_PERMISSION_INHERIT:DELETE", "角色权限继承删除", false, true), SETTING_CREATE("SETTING:CREATE", "设置创建", false, true), SETTING_READ("SETTING:READ", "设置读取", false, true), diff --git a/src/main/java/com/imyeyu/api/bean/CoreRoleCode.java b/src/main/java/com/imyeyu/api/bean/CoreRoleCode.java index 64aa870..7e246b9 100644 --- a/src/main/java/com/imyeyu/api/bean/CoreRoleCode.java +++ b/src/main/java/com/imyeyu/api/bean/CoreRoleCode.java @@ -13,9 +13,7 @@ public enum CoreRoleCode implements BuiltinRoleCode { SYSTEM("系统", true, null), - ADMIN("管理员", false, SYSTEM.name()), - - USER_ROLE_ADMIN("授权管理", false, ADMIN.name()); + ADMIN("管理员", false, SYSTEM.name()); final String defaultName; final boolean protectedRole; @@ -40,4 +38,5 @@ public enum CoreRoleCode implements BuiltinRoleCode { public String getParentCode() { return parentCode; } + } diff --git a/src/main/java/com/imyeyu/api/config/WebConfig.java b/src/main/java/com/imyeyu/api/config/WebConfig.java index d457568..6be5503 100644 --- a/src/main/java/com/imyeyu/api/config/WebConfig.java +++ b/src/main/java/com/imyeyu/api/config/WebConfig.java @@ -9,7 +9,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.imyeyu.api.annotation.EnableSettingInterceptor; import com.imyeyu.api.annotation.RequestRateLimitInterceptor; import com.imyeyu.api.annotation.RequireCorePermissionInterceptor; -import com.imyeyu.api.annotation.RequireCoreRoleInterceptor; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor; import com.imyeyu.api.modules.gao.annotation.RequireGaoRoleInterceptor; import com.imyeyu.api.modules.journal.util.JournalAPIInterceptor; @@ -44,7 +43,6 @@ public class WebConfig implements WebMvcConfigurer { private final JournalAPIInterceptor journalAPIInterceptor; private final EnableSettingInterceptor enableSettingInterceptor; private final RequireGaoRoleInterceptor requireGaoRoleInterceptor; - private final RequireCoreRoleInterceptor requireCoreRoleInterceptor; private final RequestRateLimitInterceptor requestRateLimitInterceptor; private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor; private final RequireCorePermissionInterceptor requireCorePermissionInterceptor; @@ -59,7 +57,6 @@ public class WebConfig implements WebMvcConfigurer { registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH); registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH); registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**"); - registry.addInterceptor(requireCoreRoleInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireGaoRoleInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**"); diff --git a/src/main/java/com/imyeyu/api/modules/common/controller/AttachmentController.java b/src/main/java/com/imyeyu/api/modules/common/controller/AttachmentController.java index 28712f7..10ed758 100644 --- a/src/main/java/com/imyeyu/api/modules/common/controller/AttachmentController.java +++ b/src/main/java/com/imyeyu/api/modules/common/controller/AttachmentController.java @@ -3,8 +3,6 @@ package com.imyeyu.api.modules.common.controller; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.TempFileService; -import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq; -import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq; import com.imyeyu.api.modules.common.vo.attach.TempFileResp; import com.imyeyu.java.TimiJava; import com.imyeyu.network.Network; @@ -183,30 +181,6 @@ public class AttachmentController { return service.pageByBizId(bizType, bizId, attachTypeList, page); } - /** - * 按业务差分更新附件 - * - * @param req 更新请求 - */ - @AOPLog - @RequestRateLimit - @PostMapping("/update/biz") - public void updateByBiz(@RequestBody BizUpdateReq req) { - service.updateByBiz(req); - } - - /** - * 按业务删除全部附件 - * - * @param req 删除请求 - */ - @AOPLog - @RequestRateLimit - @PostMapping("/delete/biz") - public void deleteByBiz(@RequestBody BizDeleteReq req) { - service.deleteByBiz(req); - } - @AOPLog @IgnoreGlobalReturn @GetMapping({"/read/{id}", "/download/{id}"}) diff --git a/src/main/java/com/imyeyu/api/modules/common/controller/MultilingualController.java b/src/main/java/com/imyeyu/api/modules/common/controller/MultilingualController.java index db002a3..1fb0994 100644 --- a/src/main/java/com/imyeyu/api/modules/common/controller/MultilingualController.java +++ b/src/main/java/com/imyeyu/api/modules/common/controller/MultilingualController.java @@ -2,9 +2,7 @@ package com.imyeyu.api.modules.common.controller; import com.fasterxml.jackson.annotation.JsonView; import com.imyeyu.api.annotation.RequireCorePermission; -import com.imyeyu.api.annotation.RequireCoreRole; import com.imyeyu.api.bean.CorePermissionCode; -import com.imyeyu.api.bean.CoreRoleCode; import com.imyeyu.api.modules.common.entity.Multilingual; import com.imyeyu.api.modules.common.service.MultilingualService; import com.imyeyu.spring.annotation.AOPLog; @@ -40,7 +38,6 @@ public class MultilingualController { */ @JsonView(ResponseView.Admin.class) @RequestRateLimit - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_READ) @PostMapping("/detail") public Multilingual detail(@RequestParam String id) { @@ -56,7 +53,6 @@ public class MultilingualController { @AOPLog @JsonView(ResponseView.Admin.class) @RequestRateLimit - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_CREATE) @PostMapping("/create") public Multilingual create(@RequestBody Multilingual multilingual) { @@ -71,7 +67,6 @@ public class MultilingualController { */ @AOPLog @RequestRateLimit - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_UPDATE) @PostMapping("/update") public void update(@RequestBody Multilingual multilingual) { @@ -85,7 +80,6 @@ public class MultilingualController { */ @AOPLog @RequestRateLimit - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_DELETE) @PostMapping("/delete") public void delete(@RequestParam String id) { diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java index 6b1c10d..3ef00fe 100644 --- a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java +++ b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java @@ -1,9 +1,10 @@ package com.imyeyu.api.modules.common.entity; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.imyeyu.api.TimiServerAPI; import com.imyeyu.api.modules.common.bean.attachment.AccessKey; import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.java.TimiJava; import com.imyeyu.java.ref.Ref; import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.annotation.table.Transient; @@ -33,7 +34,7 @@ public class Attachment extends UUIDEntity { GAO_CUSTOMER, - GAO_REGISTRATION_EVENT_RECORD, + GAO_EVENT_RECORD, TEMP_FILE, @@ -54,7 +55,7 @@ public class Attachment extends UUIDEntity { protected String mimeType; - protected JsonNode metadata; + protected ObjectNode metadata; protected Long size; @@ -82,6 +83,14 @@ public class Attachment extends UUIDEntity { @Transient protected MultipartFile file; + public String buildMongoName() { + StringBuilder result = new StringBuilder(bizType.name()); + if (TimiJava.isNotEmpty(attachType)) { + result.append('_').append(attachType.toUpperCase()); + } + return result.append('_').append(name).toString(); + } + public InputStream openInputStream() { AttachmentService service = TimiServerAPI.applicationContext.getBean(AttachmentService.class); return service.getInputStreamByMongoId(mongoId); diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java index 60e7d61..6c5d57f 100644 --- a/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/AttachmentMapper.java @@ -4,9 +4,9 @@ import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.RawMapper; -import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import java.util.Collection; import java.util.List; /** @@ -32,7 +32,7 @@ public interface AttachmentMapper extends BaseMapper, RawMap List selectByBizId(Attachment.BizType bizType, String bizId, List attachTypes, Page page); - List selectByBizIdList(@Param("bizType") Attachment.BizType bizType, @Param("bizIdList") List bizIdList, @Param("attachTypes") List attachTypes); + List selectByBizIdList(Attachment.BizType bizType, Collection bizIdList, List attachTypes); long countByBizId(Attachment.BizType bizType, String bizId, List attachTypes); diff --git a/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java b/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java index ddd367c..b89df9c 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/AttachmentService.java @@ -1,13 +1,12 @@ package com.imyeyu.api.modules.common.service; import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq; -import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq; import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; import java.io.InputStream; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -19,6 +18,13 @@ import java.util.Map; */ public interface AttachmentService extends BaseService { + /// + /// 克隆附件,复制业务属性和文件内容并生成新的附件记录 + /// + /// @param id 源附件 ID + /// @return 克隆后的附件 + Attachment clone(String id); + /** * 按需获取或创建缩略图附件。 *

当附件为图片或视频时,若指定尺寸的缩略图已存在则直接返回缓存, @@ -52,14 +58,14 @@ public interface AttachmentService extends BaseService { List listByBizId(Attachment.BizType bizType, String bizId, String... attachTypes); /** - * 根据多个业务 ID 批量获取附件。 + * 根据多个业务 ID 批量获取附件 * * @param bizType 业务类型 * @param bizIdList 业务 ID 列表 * @param attachTypes 附件类型,可为 null * @return 按业务 ID 分组的附件列表 */ - Map> mapByBizIdList(Attachment.BizType bizType, List bizIdList, String... attachTypes); + Map> mapByBizIdList(Attachment.BizType bizType, Collection bizIdList, String... attachTypes); /** * 根据业务获取所有附件 @@ -84,10 +90,8 @@ public interface AttachmentService extends BaseService { *

  • 请求有、数据库有、无 tempFileId → 仅更新元数据
  • *
  • 请求有、数据库有、有 tempFileId → 更新文件与元数据
  • * - * - * @param req 差分更新请求 */ - void updateByBiz(BizUpdateReq req); + void updateByBizId(Attachment.BizType bizType, String bizId, List list); - void deleteByBiz(BizDeleteReq req); + void deleteByBizId(Attachment.BizType bizType, String bizId); } diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java index 4346370..701d4db 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/AttachmentServiceImplement.java @@ -8,8 +8,6 @@ import com.imyeyu.api.modules.common.bean.attachment.Metadata; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.mapper.AttachmentMapper; import com.imyeyu.api.modules.common.service.AttachmentService; -import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq; -import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq; import com.imyeyu.api.util.JavaCV; import com.imyeyu.io.IO; import com.imyeyu.java.TimiJava; @@ -44,6 +42,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; @@ -71,6 +70,88 @@ public class AttachmentServiceImplement extends AbstractEntityService> mapByBizIdList(Attachment.BizType bizType, List bizIdList, String... attachTypes) { - if (bizIdList == null || bizIdList.isEmpty()) { - return Map.of(); - } - List normalizedBizIdList = bizIdList.stream() - .filter(TimiJava::isNotEmpty) - .distinct() - .toList(); - if (normalizedBizIdList.isEmpty()) { - return Map.of(); - } - return mapper.selectByBizIdList(bizType, new ArrayList<>(normalizedBizIdList), List.of(attachTypes)).stream() - .collect(Collectors.groupingBy(Attachment::getBizId)); + public Map> mapByBizIdList(Attachment.BizType bizType, Collection bizIdList, String... attachTypes) { + return mapper.selectByBizIdList(bizType, bizIdList, List.of(attachTypes)).stream().collect(Collectors.groupingBy(Attachment::getBizId)); } @Override @@ -348,16 +347,14 @@ public class AttachmentServiceImplement extends AbstractEntityService items = req.getItems(); + public void updateByBizId(Attachment.BizType bizType, String bizId, List list) { + list = TimiJava.defaultIfNull(list, new ArrayList<>()); // 数据库现有附件 List dbList = mapper.selectAllByBizId(bizType, bizId); Set dbIds = dbList.stream().map(Attachment::getId).collect(Collectors.toSet()); // 请求附件 ID - Set reqIds = items.stream().map(Attachment::getId).filter(Objects::nonNull).collect(Collectors.toSet()); + Set reqIds = list.stream().map(Attachment::getId).filter(Objects::nonNull).collect(Collectors.toSet()); // 数据库有、请求无:删除 for (Attachment attach : dbList) { if (!reqIds.contains(attach.getId())) { @@ -365,7 +362,7 @@ public class AttachmentServiceImplement extends AbstractEntityService attachmentList = mapper.selectAllByBizId(req.getBizType(), req.getBizId()); + public void deleteByBizId(Attachment.BizType bizType, String bizId) { + List attachmentList = mapper.selectAllByBizId(bizType, bizId); for (Attachment attachment : attachmentList) { delete(attachment.getId()); } diff --git a/src/main/java/com/imyeyu/api/modules/common/vo/attach/BizDeleteReq.java b/src/main/java/com/imyeyu/api/modules/common/vo/attach/BizDeleteReq.java deleted file mode 100644 index 7ebb47a..0000000 --- a/src/main/java/com/imyeyu/api/modules/common/vo/attach/BizDeleteReq.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.imyeyu.api.modules.common.vo.attach; - -import com.imyeyu.api.modules.common.entity.Attachment; -import lombok.Data; - -/** - * 业务附件差分更新请求 - * - * @author 夜雨 - * @since 2026-03-11 00:00 - */ -@Data -public class BizDeleteReq { - - /** 业务类型 */ - private Attachment.BizType bizType; - - /** 业务 ID */ - private String bizId; -} diff --git a/src/main/java/com/imyeyu/api/modules/common/vo/attach/BizUpdateReq.java b/src/main/java/com/imyeyu/api/modules/common/vo/attach/BizUpdateReq.java deleted file mode 100644 index 6b90f1f..0000000 --- a/src/main/java/com/imyeyu/api/modules/common/vo/attach/BizUpdateReq.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.imyeyu.api.modules.common.vo.attach; - -import com.imyeyu.api.modules.common.entity.Attachment; -import lombok.Data; - -import java.util.ArrayList; -import java.util.List; - -/** - * 业务附件差分更新请求 - * - * @author 夜雨 - * @since 2026-03-11 00:00 - */ -@Data -public class BizUpdateReq { - - /** 业务类型 */ - private Attachment.BizType bizType; - - /** 业务 ID */ - private String bizId; - - /** 附件项列表 */ - private List items = new ArrayList<>(); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/annotation/RequireGaoRoleInterceptor.java b/src/main/java/com/imyeyu/api/modules/gao/annotation/RequireGaoRoleInterceptor.java index 600db09..4d24acc 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/annotation/RequireGaoRoleInterceptor.java +++ b/src/main/java/com/imyeyu/api/modules/gao/annotation/RequireGaoRoleInterceptor.java @@ -8,6 +8,7 @@ import com.imyeyu.java.TimiJava; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.lang.NonNull; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @@ -32,6 +33,9 @@ public class RequireGaoRoleInterceptor implements HandlerInterceptor { return true; } RequireGaoRole annotation = handlerMethod.getMethodAnnotation(RequireGaoRole.class); + if (annotation == null) { + annotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequireGaoRole.class); + } if (annotation == null || TimiJava.isEmpty(annotation.value())) { return true; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java index cfc07be..9015b38 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java +++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java @@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.bean; import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.modules.user.bean.BuiltinPermissionCode; +import lombok.Getter; /// /// GAO 模块权限枚举 @@ -10,58 +11,88 @@ import com.imyeyu.api.modules.user.bean.BuiltinPermissionCode; /// @since 2026-07-26 public enum GaoPermissionCode implements BuiltinPermissionCode { - CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建"), + STORE_CREATE("STORE:CREATE", "门店创建", false, true), - CUSTOMER_READ("CUSTOMER:READ", "客户读取"), + STORE_READ("STORE:READ", "门店读取", true, true), - CUSTOMER_EXPORT("CUSTOMER:EXPORT", "客户导出", true), + STORE_EXPORT("STORE:EXPORT", "门店导出", false, true), - CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改"), + STORE_UPDATE("STORE:UPDATE", "门店修改", true, true), - CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除"), + STORE_DELETE("STORE:DELETE", "门店删除", false, true), - REGISTRATION_EVENT_CREATE("REGISTRATION_EVENT:CREATE", "登记事件创建"), + CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建", false, false), - REGISTRATION_EVENT_READ("REGISTRATION_EVENT:READ", "登记事件读取"), + CUSTOMER_READ("CUSTOMER:READ", "客户读取", false, false), - REGISTRATION_EVENT_UPDATE("REGISTRATION_EVENT:UPDATE", "登记事件修改"), + CUSTOMER_EXPORT("CUSTOMER:EXPORT", "客户导出", true, false), - REGISTRATION_EVENT_DELETE("REGISTRATION_EVENT:DELETE", "登记事件删除"), + CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改", false, false), - REGISTRATION_EVENT_RECORD_CREATE("REGISTRATION_EVENT_RECORD:CREATE", "登记事件记录创建"), + CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除", false, false), - REGISTRATION_EVENT_RECORD_READ("REGISTRATION_EVENT_RECORD:READ", "登记事件记录读取"), + EVENT_CREATE("EVENT:CREATE", "登记事件创建", false, false), - REGISTRATION_EVENT_RECORD_EXPORT("REGISTRATION_EVENT_RECORD:EXPORT", "登记事件记录导出", true), + EVENT_READ("EVENT:READ", "登记事件读取", false, false), - REGISTRATION_EVENT_RECORD_UPDATE("REGISTRATION_EVENT_RECORD:UPDATE", "登记事件记录修改"), + EVENT_UPDATE("EVENT:UPDATE", "登记事件修改", false, false), - REGISTRATION_EVENT_RECORD_DELETE("REGISTRATION_EVENT_RECORD:DELETE", "登记事件记录删除"), + EVENT_DELETE("EVENT:DELETE", "登记事件删除", false, false), - STAT_READ("STAT:READ", "统计读取"), + EVENT_RECORD_CREATE("EVENT_RECORD:CREATE", "登记事件记录创建", false, false), - QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建"), + EVENT_RECORD_READ("EVENT_RECORD:READ", "登记事件记录读取", false, false), - USER_CREATE("USER:CREATE", "用户创建", true), + EVENT_RECORD_EXPORT("EVENT_RECORD:EXPORT", "登记事件记录导出", true, false), - USER_READ("USER:READ", "用户读取", true), + EVENT_RECORD_UPDATE("EVENT_RECORD:UPDATE", "登记事件记录修改", false, false), - USER_UPDATE("USER:UPDATE", "用户修改", true), + EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false), - USER_DELETE("USER:DELETE", "用户删除", true); + POINT_ACCOUNT_READ("POINT_ACCOUNT:READ", "客户积分账户读取", true, true), + + POINT_LEDGER_READ("POINT_LEDGER:READ", "客户积分流水读取", true, true), + + POINT_RULE_CREATE("POINT_RULE:CREATE", "客户积分规则创建", true, true), + + POINT_RULE_READ("POINT_RULE:READ", "客户积分规则读取", true, true), + + POINT_RULE_UPDATE("POINT_RULE:UPDATE", "客户积分规则修改", true, true), + + POINT_RULE_DELETE("POINT_RULE:DELETE", "客户积分规则删除", true, true), + + POINT_ADJUST("POINT:ADJUST", "客户积分人工调整", true, true), + + POINT_SOURCE_VALUE("POINT:SOURCE_VALUE", "登记提交自定义积分值", true, true), + + STAT_READ("STAT:READ", "统计读取", false, false), + + QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建", false, false), + + USER_CREATE("USER:CREATE", "用户创建", true, false), + + USER_READ("USER:READ", "用户读取", true, false), + + USER_UPDATE("USER:UPDATE", "用户修改", true, false), + + USER_DELETE("USER:DELETE", "用户删除", true, false); final String value; final String defaultName; + + /// `true` 为初始化时授权给店长角色 + @Getter final boolean grantToStoreManager; - GaoPermissionCode(String value, String defaultName) { - this(value, defaultName, false); - } + /// `true` 为初始化时授权给全局管理员角色 + @Getter + final boolean grantToGlobalManager; - GaoPermissionCode(String value, String defaultName, boolean grantToStoreManager) { + GaoPermissionCode(String value, String defaultName, boolean grantToStoreManager, boolean grantToGlobalManager) { this.value = value; this.defaultName = defaultName; this.grantToStoreManager = grantToStoreManager; + this.grantToGlobalManager = grantToGlobalManager; } @Override @@ -88,11 +119,4 @@ public enum GaoPermissionCode implements BuiltinPermissionCode { public boolean isGrantToAdmin() { return false; } - - /// true 为初始化时授权给店长角色 - /// - /// @return 是否授权给店长角色 - public boolean isGrantToStoreManager() { - return grantToStoreManager; - } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java index b76855e..3bd2282 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java +++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java @@ -12,12 +12,14 @@ import lombok.AllArgsConstructor; @AllArgsConstructor public enum GaoRoleCode implements BuiltinRoleCode { - /// 店长 - STORE_MANAGER("店长"), + /// 全局管理员 + GLOBAL_MANAGER("全局管理员", null), - EMPLOYEE("员工"); + /// 店长 + STORE_MANAGER("店长", GLOBAL_MANAGER.name()); final String defaultName; + final String parentCode; @Override public ModuleCode getModuleCode() { @@ -36,6 +38,7 @@ public enum GaoRoleCode implements BuiltinRoleCode { @Override public String getParentCode() { - return null; + return parentCode; } + } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerChartController.java new file mode 100644 index 0000000..3bec34d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerChartController.java @@ -0,0 +1,47 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.service.GaoCustomerChartService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/// GAO 客户报表接口 +/// +/// @author Codex +/// @since 2026-08-11 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/customer/chart") +public class GaoCustomerChartController { + + private final GaoCustomerChartService service; + + /// 查询客户登记每日统计,可同时支撑登记趋势和登记事件结构图表 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/event/record/daily") + public List eventRecordDaily(@RequestBody GaoCustomerChartReq req) { + return service.eventRecordDailyStat(req); + } + + /// 查询客户登记日历记录 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/event/record/calendar") + public List eventRecordCalendar(@RequestBody GaoCustomerChartReq req) { + return service.eventRecordCalendar(req); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerController.java index 480a64b..e36ffbc 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerController.java @@ -1,29 +1,32 @@ package com.imyeyu.api.modules.gao.controller; import com.fasterxml.jackson.annotation.JsonView; +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.gao.annotation.RequireGaoPermission; 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.service.GaoStoreService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView; import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; -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.api.modules.gao.vo.GaoCustomerTrendStateReq; +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.network.Network; +import com.imyeyu.spring.TimiSpring; 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 com.imyeyu.spring.util.ResponseView; +import com.imyeyu.utils.Time; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; @@ -33,28 +36,24 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; -import java.net.URLEncoder; 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 +/// @author 夜雨 +/// @since 2026-07-27 14:12 @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 UserService userService; + private final RoleChecker roleChecker; + private final AttachmentService attachmentService; private final GaoCustomerService service; - private final GaoRegistrationEventRecordService registrationEventRecordService; - private final GaoRegistrationEventService registrationEventService; + private final GaoStoreService gaoStoreService; + private final UserLoginService userLoginService; @AOPLog @RequestRateLimit @@ -63,74 +62,32 @@ public class GaoCustomerController { @JsonView(ResponseView.Public.class) public PageResult list(@RequestBody Page page) { PageResult result = service.page(page); - fillLastSignInAt(result.getList()); + + List idList = result.getList().stream().map(GaoCustomer::getId).distinct().toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, idList); + for (GaoCustomer customer : result.getList()) { + customer.setAttachmentList(attachmentMap.get(customer.getId())); + } return result; } - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/search") - @JsonView(ResponseView.Public.class) - public List search(@RequestBody GaoCustomerSearchReq req) { - return service.search(req); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/search/page") - @JsonView(ResponseView.Public.class) - public PageResult searchPage(@RequestBody Page page) { - return service.searchPage(page); - } - - @AOPLog - @RequestRateLimit - @IgnoreGlobalReturn - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_EXPORT) - @PostMapping("/export") - public void export(@RequestBody GaoCustomerSearchReq req, HttpServletResponse resp) throws IOException { - String fileName = URLEncoder.encode("客户列表.xlsx", "UTF-8").replace("+", "%20"); - resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - resp.setHeader("Content-Disposition", "attachment; filename=\"gao-customer.xlsx\"; filename*=UTF-8''" + fileName); - resp.getOutputStream().write(service.exportSearchExcel(req)); - } - @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) @PostMapping("/detail") @JsonView(ResponseView.Public.class) public GaoCustomer detail(@RequestParam String id) { - return service.get(id); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/detail/code") - @JsonView(ResponseView.Public.class) - public GaoCustomer detailByCode(@RequestParam String code) { - return service.getByCode(code); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/detail/code/nullable") - @JsonView(ResponseView.Public.class) - public GaoCustomer nullableDetailByCode(@RequestParam String code) { - return service.getNullableByCode(code); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/invited/list") - @JsonView(ResponseView.Public.class) - public List invitedList(@RequestParam String introducerCustomerId) { - return service.listInvited(introducerCustomerId); + GaoCustomer customer = service.get(id); + if (TimiJava.isNotEmpty(customer.getIntroducerCustomerId())) { + GaoCustomer introducerCustomer = service.get(customer.getIntroducerCustomerId()); + introducerCustomer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, introducerCustomer.getId())); + customer.setIntroducerCustomer(introducerCustomer); + } + if (TimiJava.isNotEmpty(customer.getCreatorUserId())) { + customer.setCreatorUser(userService.get(customer.getCreatorUserId())); + } + customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); + return customer; } @AOPLog @@ -138,17 +95,12 @@ public class GaoCustomerController { @RequireGaoPermission(GaoPermissionCode.CUSTOMER_CREATE) @PostMapping("/create") @JsonView(ResponseView.Public.class) - public GaoCustomer create(@RequestBody GaoCustomer customer) { + public String create(@RequestBody GaoCustomer customer) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + customer.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId()); + } service.create(customer); - return customer; - } - - @AOPLog - @RequestRateLimit - @PostMapping("/quick/register") - @JsonView(ResponseView.Public.class) - public GaoRegistrationEventRecord quickRegister(@RequestBody GaoQuickRegistrationEventRecordReq req) { - return registrationEventRecordService.quickRegister(req); + return customer.getId(); } @AOPLog @@ -156,7 +108,10 @@ public class GaoCustomerController { @RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE) @PostMapping("/update") public void update(@RequestBody GaoCustomer customer) { - service.update(customer); + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + customer.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId()); + } + service.updateWithAttachment(customer); } @AOPLog @@ -169,27 +124,78 @@ public class GaoCustomerController { @AOPLog @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE) - @PostMapping("/invite/create") - public void createInvite(@RequestBody GaoCustomerInviteUpdateReq req) { - service.saveInviteRelation(req); + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) + @PostMapping("/find/id") + @JsonView(ResponseView.Public.class) + public GaoCustomer findById(@RequestParam String id) { + GaoCustomer customer = service.get(id); + TimiException.required(customer, "not found customer"); + customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); + return customer; } @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/invite/detail") + @PostMapping("/find/code") @JsonView(ResponseView.Public.class) - public GaoCustomer inviteDetail(@RequestParam String customerId) { - return service.get(customerId); + public GaoCustomer findByCode(@RequestParam String storeId, @RequestParam String code) { + GaoCustomer customer = service.getByCode(storeId, code); + if (customer == null) { + return null; + } + customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); + return customer; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) + @PostMapping("/find/name") + @JsonView(ResponseView.Public.class) + public GaoCustomer findByName(@RequestParam String storeId, @RequestParam String name) { + GaoCustomer customer = service.getByName(storeId, name); + if (customer == null) { + return null; + } + customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); + return customer; + } + + @AOPLog + @RequestRateLimit + @IgnoreGlobalReturn + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_EXPORT) + @PostMapping("/export") + public void export(@RequestBody Page page) throws IOException { + HttpServletResponse resp = TimiSpring.getResponse(); + String contentDisposition = Network.getFileDownloadHeader("客户列表-%s.xlsx".formatted(Time.serialize.format(Time.now()))); + resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + resp.setHeader("Content-Disposition", contentDisposition); + resp.getOutputStream().write(service.export(page)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) + @PostMapping("/invited/list") + @JsonView(ResponseView.Public.class) + public List listInvited(@RequestParam String introducerCustomerId) { + List result = service.listInvited(introducerCustomerId); + List idList = result.stream().map(GaoCustomer::getId).distinct().toList(); + Map> attachMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, idList); + for (GaoCustomer customer : result) { + customer.setAttachmentList(attachMap.get(customer.getId())); + } + return result; } @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.CUSTOMER_UPDATE) - @PostMapping("/invite/update") - public void updateInvite(@RequestBody GaoCustomerInviteUpdateReq req) { - service.saveInviteRelation(req); + @PostMapping("/invite/save") + public void saveInvite(@RequestParam String introducerCustomerId, @RequestParam String customerId) { + service.saveInviteRelation(introducerCustomerId, customerId); } @AOPLog @@ -200,46 +206,21 @@ public class GaoCustomerController { service.deleteInviteRelation(customerId); } - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/stat/daily") /// 按天统计客户新增数和累计总数 - public List dailyStat(@RequestBody GaoCustomerTrendStatReq req) { - return service.statDailyTrend(req); - } - @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) - @PostMapping("/stat/gender") - /// 统计客户性别分布 - public List genderStat() { - return service.statGender(); + @PostMapping("/state/daily") + public List dailyStat(@RequestBody GaoCustomerTrendStateReq req) { + return service.stateDailyTrend(req); } - private void fillLastSignInAt(List 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 customerIdList = customerList.stream().map(GaoCustomer::getId).toList(); - Map 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()); - } - } + /// 统计客户性别分布 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) + @PostMapping("/state/gender") + public List genderStat(@RequestParam String storeId) { + return service.stateGender(storeId); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java new file mode 100644 index 0000000..72234c3 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java @@ -0,0 +1,105 @@ +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.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.service.GaoEventService; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +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; + +/// GAO 登记事件接口 +/// +/// @author 夜雨 +/// @since 2026-07-27 14:13 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/event") +public class GaoEventController { + + private final GaoEventService service; + private final RoleChecker roleChecker; + private final GaoStoreService gaoStoreService; + private final UserLoginService userLoginService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_READ) + @PostMapping("/list") + public PageResult list(@RequestBody Page page) { + PageResult result = service.page(page); + Map> roleMap = service.mapRoleCodeListByEventIdList(result.getList().stream().map(GaoEvent::getId).toList()); + for (GaoEvent event : result.getList()) { + event.setRoleCodeList(roleMap.get(event.getId())); + } + return result; + } + + @AOPLog + @RequestRateLimit + @PostMapping("/list/valid") + public List listValid(@RequestParam String storeId) { + return service.listValid(storeId); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_READ) + @PostMapping("/detail") + public GaoEvent detail(@RequestParam String id) { + return service.get(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_CREATE) + @PostMapping("/create") + public void create(@RequestBody GaoEvent event) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + event.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId()); + } + service.create(event); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoEvent event) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + event.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId()); + } + service.update(event); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE) + @PostMapping("/sort") + public void sort(@RequestBody List idList) { + service.sort(idList); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_DELETE) + @PostMapping("/delete") + public void delete(@RequestParam String id) { + service.delete(id); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java new file mode 100644 index 0000000..2a97e6c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java @@ -0,0 +1,191 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.fasterxml.jackson.annotation.JsonView; +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.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; +import com.imyeyu.api.modules.gao.service.GaoEventService; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +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.network.Network; +import com.imyeyu.spring.TimiSpring; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.IgnoreGlobalReturn; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.util.ResponseView; +import com.imyeyu.utils.Time; +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.util.List; +import java.util.Map; + +/// GAO 登记事件记录接口 +/// +/// @author 夜雨 +/// @since 2026-07-27 14:11 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/event/record") +public class GaoEventRecordController { + + private final UserService userService; + private final RoleChecker roleChecker; + private final GaoEventService eventService; + private final AttachmentService attachmentService; + private final GaoCustomerService customerService; + private final GaoStoreService gaoStoreService; + private final GaoEventRecordService service; + private final UserLoginService userLoginService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_READ) + @PostMapping("/list") + @JsonView(ResponseView.Public.class) + public PageResult list(@RequestBody GaoEventRecordPage page) { + PageResult result = service.pageByRange(page); + { + List customerIdList = result.getList().stream().map(GaoEventRecord::getCustomerId).toList(); + Map eventMap = eventService.mapByIdList(result.getList().stream().map(GaoEventRecord::getEventId).toList()); + Map customerMap = customerService.mapByIdList(customerIdList); + Map> attachMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList); + for (GaoEventRecord record : result.getList()) { + record.setEvent(eventMap.get(record.getEventId())); + record.setCustomer(customerMap.get(record.getCustomerId())); + record.getCustomer().setAttachmentList(attachMap.get(record.getCustomerId())); + } + } + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_READ) + @PostMapping("/detail") + @JsonView(ResponseView.Public.class) + public GaoEventRecord detail(@RequestParam String id) { + GaoEventRecord record = service.get(id); + record.setCustomer(customerService.get(record.getCustomerId())); + record.getCustomer().setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, record.getCustomerId())); + record.setEvent(eventService.get(record.getEventId())); + record.setOperator(userService.get(record.getOperatorUserId())); + record.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, record.getId())); + return record; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_CREATE) + @PostMapping("/create") + public void create(@RequestBody GaoEventRecord record) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + record.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId()); + } + service.create(record); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_CREATE) + @PostMapping("/create/batch") + public void createBatch(@RequestBody List recordList) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + String storeId = gaoStoreService.getBelongIdByRequiredLoginUserId(); + for (GaoEventRecord record : recordList) { + record.setStoreId(storeId); + } + } + service.createBatch(recordList); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_CREATE) + @PostMapping("/create/customer") + public void createWithCustomer(@RequestBody GaoCustomer customer) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + String storeId = gaoStoreService.getBelongIdByRequiredLoginUserId(); + for (GaoEventRecord record : TimiJava.safeIterable(customer.getEventRecordList())) { + record.setStoreId(storeId); + } + } + service.createWithCustomer(customer); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoEventRecord record) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + record.setStoreId(gaoStoreService.getBelongIdByRequiredLoginUserId()); + } + service.update(record); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_DELETE) + @PostMapping("/delete") + public void delete(@RequestParam String id) { + service.delete(id); + } + + @AOPLog + @RequestRateLimit + @IgnoreGlobalReturn + @RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_EXPORT) + @PostMapping("/export") + public void export(@RequestBody GaoEventRecordPage page) throws IOException { + HttpServletResponse resp = TimiSpring.getResponse(); + String contentDisposition = Network.getFileDownloadHeader("登记事件记录-%s.xlsx".formatted(Time.serialize.format(Time.now()))); + resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + resp.setHeader("Content-Disposition", contentDisposition); + resp.getOutputStream().write(service.export(page)); + } + + /// 按时间范围查询客户登记排行榜 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/rank") + public PageResult eventRank(@RequestBody GaoEventRecordRankPage page) { + PageResult result = service.pageRankByRange(page); + List customerIdList = result.getList().stream().map(GaoCustomerEventRankView::getCustomerId).distinct().toList(); + if (customerIdList.isEmpty()) { + return result; + } + Map customerMap = customerService.mapByIdList(customerIdList); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList); + for (GaoCustomerEventRankView view : result.getList()) { + GaoCustomer customer = customerMap.get(view.getCustomerId()); + if (customer != null) { + customer.setAttachmentList(attachmentMap.get(view.getCustomerId())); + } + view.setCustomer(customer); + } + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointAccountController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointAccountController.java new file mode 100644 index 0000000..537e0a1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointAccountController.java @@ -0,0 +1,47 @@ +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.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoPointAccount; +import com.imyeyu.api.modules.gao.service.GaoPointAccountService; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.user.service.RoleChecker; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/// GAO 客户积分账户接口 +/// +/// @author Codex +/// @since 2026-08-11 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/customer/point") +public class GaoPointAccountController { + + private final RoleChecker roleChecker; + private final GaoStoreService storeService; + private final GaoPointAccountService accountService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ) + @PostMapping("/account/detail") + public GaoPointAccount detail(@RequestParam String customerId, @RequestParam(required = false) String storeId) { + storeId = resolveStoreId(storeId); + return accountService.getByCustomerId(storeId, customerId); + } + + private String resolveStoreId(String requestedStoreId) { + if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + return requestedStoreId; + } + return storeService.getBelongIdByRequiredLoginUserId(); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointLedgerController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointLedgerController.java new file mode 100644 index 0000000..412c761 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointLedgerController.java @@ -0,0 +1,63 @@ +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.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.api.modules.gao.service.GaoPointLedgerService; +import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest; +import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +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.RestController; + +/// GAO 客户积分流水接口 +/// +/// @author Codex +/// @since 2026-08-11 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/customer/point") +public class GaoPointLedgerController { + + private final RoleChecker roleChecker; + private final GaoStoreService storeService; + private final GaoPointLedgerService ledgerService; + private final GaoPointTriggerService triggerService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_LEDGER_READ) + @PostMapping("/ledger/list") + public PageResult list(@RequestBody GaoPointLedgerPage page) { + TimiException.required(page, "not found page"); + page.setStoreId(resolveStoreId(page.getStoreId())); + return ledgerService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_ADJUST) + @PostMapping("/adjust") + public void adjust(@RequestBody GaoPointAdjustRequest request) { + request.setStoreId(resolveStoreId(request.getStoreId())); + triggerService.adjust(request); + } + + private String resolveStoreId(String requestedStoreId) { + if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + return requestedStoreId; + } + return storeService.getBelongIdByRequiredLoginUserId(); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointRuleController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointRuleController.java new file mode 100644 index 0000000..02c6e9d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPointRuleController.java @@ -0,0 +1,108 @@ +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.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.api.modules.gao.service.GaoPointRuleService; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +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; + +/// GAO 客户积分规则接口 +/// +/// @author Codex +/// @since 2026-08-11 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/customer/point") +public class GaoPointRuleController { + + private final RoleChecker roleChecker; + private final GaoStoreService storeService; + private final GaoPointRuleService ruleService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ) + @PostMapping("/rule/list") + public PageResult list(@RequestBody Page page) { + TimiException.required(page, "not found page"); + if (!isGlobalManager()) { + GaoPointRule example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoPointRule()); + example.setStoreId(resolveStoreId(null)); + page.setEqualsExample(example); + } + return ruleService.page(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ) + @PostMapping("/rule/detail") + public GaoPointRule detail(@RequestParam String id) { + GaoPointRule rule = ruleService.get(id); + TimiException.required(rule, "not found point rule"); + checkStore(rule.getStoreId()); + return rule; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE) + @PostMapping("/rule/create") + public String create(@RequestBody GaoPointRule rule) { + rule.setStoreId(resolveStoreId(rule.getStoreId())); + ruleService.create(rule); + return rule.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_RULE_UPDATE) + @PostMapping("/rule/update") + public void update(@RequestBody GaoPointRule rule) { + rule.setStoreId(resolveStoreId(rule.getStoreId())); + ruleService.update(rule); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.POINT_RULE_DELETE) + @PostMapping("/rule/delete") + public void delete(@RequestParam String id) { + GaoPointRule rule = ruleService.get(id); + TimiException.required(rule, "not found point rule"); + checkStore(rule.getStoreId()); + ruleService.delete(id); + } + + private String resolveStoreId(String requestedStoreId) { + if (isGlobalManager()) { + return requestedStoreId; + } + return storeService.getBelongIdByRequiredLoginUserId(); + } + + private void checkStore(String storeId) { + if (!isGlobalManager()) { + TimiException.requiredTrue(resolveStoreId(null).equals(storeId), "无权操作其他门店数据"); + } + } + + private boolean isGlobalManager() { + return roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRegistrationEventController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRegistrationEventController.java deleted file mode 100644 index 0063ad6..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRegistrationEventController.java +++ /dev/null @@ -1,90 +0,0 @@ -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 list(@RequestBody Page page) { - return service.pageWithRegistrationEventLimit(page); - } - - @AOPLog - @RequestRateLimit - @PostMapping("/enabled/list") - public List 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_UPDATE) - @PostMapping("/sort") - public void sort(@RequestBody List idList) { - service.sort(idList); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_DELETE) - @PostMapping("/delete") - public void delete(@RequestParam String id) { - service.delete(id); - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRegistrationEventRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRegistrationEventRecordController.java deleted file mode 100644 index 6cdfc4a..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRegistrationEventRecordController.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.imyeyu.api.modules.gao.controller; - -import com.fasterxml.jackson.annotation.JsonView; -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.GaoCustomerRegistrationRankView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView; -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 com.imyeyu.spring.util.ResponseView; -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.net.URLEncoder; -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") - @JsonView(ResponseView.Public.class) - public PageResult list(@RequestBody Page page) { - return service.page(page); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ) - @PostMapping("/range/page") - public PageResult rangePage(@RequestBody Page page) { - return service.pageByRange(page); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ) - @PostMapping("/detail") - @JsonView(ResponseView.Public.class) - public GaoRegistrationEventRecord detail(@RequestParam String id) { - return service.get(id); - } - - @AOPLog - @RequestRateLimit - @PostMapping("/create") - @JsonView(ResponseView.Public.class) - public List create(@RequestBody GaoRegistrationEventRecordCreateReq req) { - return service.createRecords(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") - @JsonView(ResponseView.Public.class) - public List periodList(@RequestBody GaoRegistrationEventRecordPeriodReq req) { - return service.listByRegistrationEventAndPeriod(req); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ) - @PostMapping("/range/list") - @JsonView(ResponseView.Public.class) - public List rangeList(@RequestBody GaoRegistrationEventRecordRangeReq req) { - return service.listByRange(req); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_READ) - @PostMapping("/range/calendar/list") - public List calendarRangeList(@RequestBody GaoRegistrationEventRecordRangeReq req) { - return service.listCalendarByRange(req); - } - - @AOPLog - @RequestRateLimit - @IgnoreGlobalReturn - @RequireGaoPermission(GaoPermissionCode.REGISTRATION_EVENT_RECORD_EXPORT) - @PostMapping("/range/export") - public void rangeExport(@RequestBody GaoRegistrationEventRecordRangeReq req, HttpServletResponse resp) throws IOException { - String fileName = URLEncoder.encode("登记事件记录.xlsx", "UTF-8").replace("+", "%20"); - resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - resp.setHeader("Content-Disposition", "attachment; filename=\"gao-registration-event-record.xlsx\"; filename*=UTF-8''" + fileName); - resp.getOutputStream().write(service.exportRangeExcel(req)); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.STAT_READ) - @PostMapping("/period/stat") - public List periodStat(@RequestBody GaoRegistrationEventRecordPeriodReq req) { - return service.statByRegistrationEventAndPeriodType(req.getRegistrationEventId(), req.getPeriodType()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.STAT_READ) - @PostMapping("/customer/stat") - public List customerStat(@RequestParam String customerId) { - return service.statByCustomerId(customerId); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.STAT_READ) - @PostMapping("/customer/range/stat") - /// 按时间段统计单个客户各登记事件记录数 - public List customerRangeStat(@RequestBody GaoRegistrationEventRecordStatReq req) { - return service.statByCustomerAndRange(req); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.STAT_READ) - @PostMapping("/all-customer/range/stat") - /// 按时间段统计全部客户各登记事件记录数 - public List allCustomerRangeStat(@RequestBody GaoRegistrationEventRecordStatReq req) { - return service.statAllCustomerByRange(req); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.STAT_READ) - @PostMapping("/customer/registration-rank") - /// 按时间范围查询客户登记排行榜 - public List customerRegistrationRank(@RequestBody(required = false) GaoRegistrationEventRecordRangeReq req) { - return service.listCustomerRegistrationRank(req); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.STAT_READ) - @PostMapping("/customer/registration-event/daily/stat") - /// 按天统计单个客户单个登记事件记录趋势 - public List customerRegistrationEventDailyStat(@RequestBody GaoRegistrationEventRecordTrendStatReq req) { - return service.statDailyByCustomerAndRegistrationEvent(req); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.STAT_READ) - @PostMapping("/all-customer/registration-event/daily/stat") - /// 按天统计全部客户单个登记事件记录趋势 - public List allCustomerRegistrationEventDailyStat(@RequestBody GaoRegistrationEventRecordTrendStatReq req) { - return service.statDailyByRegistrationEvent(req); - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRoleController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRoleController.java index cff4cd3..c9a1839 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRoleController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoRoleController.java @@ -4,7 +4,8 @@ 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.api.modules.user.service.AuthorizationScopeService; +import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.RequestRateLimit; import lombok.RequiredArgsConstructor; @@ -14,23 +15,23 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; -/// /// GAO 角色接口 /// -/// @author Codex -/// @since 2026-07-28 +/// @author 夜雨 +/// @since 2026-07-28 14:15 @RestController +@RequireGaoRole(GaoRoleCode.STORE_MANAGER) @RequiredArgsConstructor @RequestMapping("/gao/role") -@RequireGaoRole(GaoRoleCode.STORE_MANAGER) public class GaoRoleController { - private final RoleService roleService; + private final UserLoginService userLoginService; + private final AuthorizationScopeService authorizationScopeService; @AOPLog @RequestRateLimit @PostMapping("/available/list") public List availableList() { - return roleService.listByModuleCode(ModuleCode.GAO); + return authorizationScopeService.listGrantableRole(userLoginService.getRequireLoginUserId(), ModuleCode.GAO); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreBusinessDayController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreBusinessDayController.java new file mode 100644 index 0000000..68dad8a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreBusinessDayController.java @@ -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.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; +import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; +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-08-06 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/store/business/day") +@RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER) +public class GaoStoreBusinessDayController { + + private final GaoStoreBusinessDayService service; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_READ) + @PostMapping("/list") + public PageResult list(@RequestBody Page page) { + return service.page(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_READ) + @PostMapping("/calendar") + public List calendar(@RequestParam String storeId, @RequestParam Integer beginDateValue, @RequestParam Integer endDateValue) { + return service.listByDateRange(storeId, beginDateValue, endDateValue); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_READ) + @PostMapping("/detail") + public GaoStoreBusinessDay detail(@RequestParam String id) { + return service.get(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_CREATE) + @PostMapping("/create") + public String create(@RequestBody GaoStoreBusinessDay businessDay) { + service.create(businessDay); + return businessDay.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoStoreBusinessDay businessDay) { + service.update(businessDay); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_DELETE) + @PostMapping("/delete") + public void delete(@RequestParam String id) { + service.delete(id); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreChartController.java new file mode 100644 index 0000000..88a004e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreChartController.java @@ -0,0 +1,68 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.service.GaoStoreChartService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq; +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.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-08-11 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/store/chart") +public class GaoStoreChartController { + + private final GaoStoreChartService service; + + /// 查询客户增长趋势 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/daily") + public List customerDaily(@RequestBody GaoStoreChartReq req) { + return service.customerDailyTrend(req); + } + + /// 查询客户性别统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/gender") + public List customerGender(@RequestParam String storeId) { + return service.customerGender(storeId); + } + + /// 查询登记事件每日统计,可同时支撑登记趋势和事件结构图表 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/event/record/daily") + public List eventRecordDaily(@RequestBody GaoStoreChartReq req) { + return service.eventRecordDailyStat(req); + } + + /// 查询登记事件日历记录 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/event/record/calendar") + public List eventRecordCalendar(@RequestBody GaoStoreChartReq req) { + return service.eventRecordCalendar(req); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java new file mode 100644 index 0000000..21b0d7e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java @@ -0,0 +1,114 @@ +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.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.network.Network; +import com.imyeyu.spring.TimiSpring; +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 com.imyeyu.utils.Time; +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.util.List; + +/// GAO 门店接口 +/// +/// @author Codex +/// @since 2026-08-06 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/store") +public class GaoStoreController { + + private final GaoStoreService service; + private final UserLoginService userLoginService; + + @AOPLog + @RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER) + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_READ) + @PostMapping("/list") + public PageResult list(@RequestBody Page page) { + return service.page(page); + } + + @AOPLog + @RequireGaoRole(GaoRoleCode.STORE_MANAGER) + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_READ) + @PostMapping("/detail") + public GaoStore detail(@RequestParam String id) { + return service.get(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_CREATE) + @PostMapping("/create") + public String create(@RequestBody GaoStore store) { + service.create(store); + return store.getId(); + } + + @AOPLog + @RequireGaoRole(GaoRoleCode.STORE_MANAGER) + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoStore store) { + service.update(store); + } + + @AOPLog + @RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER) + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_DELETE) + @PostMapping("/delete") + public void delete(@RequestParam String id) { + service.delete(id); + } + + @AOPLog + @RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER) + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_UPDATE) + @PostMapping("/sort") + public void sort(@RequestBody List idList) { + service.sort(idList); + } + + @AOPLog + @RequestRateLimit + @IgnoreGlobalReturn + @RequireGaoPermission(GaoPermissionCode.STORE_EXPORT) + @PostMapping("/export") + public void export(@RequestBody Page page) throws IOException { + HttpServletResponse resp = TimiSpring.getResponse(); + String contentDisposition = Network.getFileDownloadHeader("门店列表-%s.xlsx".formatted(Time.serialize.format(Time.now()))); + resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + resp.setHeader("Content-Disposition", contentDisposition); + resp.getOutputStream().write(service.export(page)); + } + + @AOPLog + @RequestRateLimit + @PostMapping("/user") + public GaoStore getLoginStore() { + return service.getByUserId(userLoginService.getRequireLoginUserId()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java index 1d38644..0452932 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java @@ -2,14 +2,23 @@ package com.imyeyu.api.modules.gao.controller; import com.fasterxml.jackson.annotation.JsonView; 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.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.GaoStoreService; 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.service.RoleChecker; import com.imyeyu.api.modules.user.service.RoleService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.api.modules.user.service.UserRoleService; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.TimiJava; import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.RequestRateLimit; import com.imyeyu.spring.bean.Page; @@ -23,20 +32,25 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; -/// /// GAO 用户接口 /// -/// @author Codex -/// @since 2026-07-30 +/// @author 夜雨 +/// @since 2026-07-30 14:15 @RestController @RequiredArgsConstructor @RequestMapping("/gao/user") -@RequireGaoRole(GaoRoleCode.STORE_MANAGER) public class GaoUserController { - private final GaoUserService service; + private final UserService userService; private final RoleService roleService; + private final RoleChecker roleChecker; + private final GaoUserService service; + private final UserRoleService userRoleService; + private final GaoStoreService gaoStoreService; + private final UserLoginService userLoginService; + private final AttachmentService attachmentService; @AOPLog @RequestRateLimit @@ -44,7 +58,22 @@ public class GaoUserController { @PostMapping("/list") @JsonView(ResponseView.Public.class) public PageResult list(@RequestBody Page page) { - return service.pageWithUser(page); + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + // 非全局管理只能看所在店用户 + page.setEqualsExample(TimiJava.defaultIfNull(page.getEqualsExample(), new GaoUser())); + page.getEqualsExample().setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId()); + } + PageResult result = service.page(page); + { + List userIdList = result.getList().stream().map(GaoUser::getUserId).toList(); + Map userMap = userService.mapByIdList(userIdList); + Map> roleMap = userRoleService.mapRoleListByUserIdList(userIdList); + for (GaoUser gaoUser : result.getList()) { + gaoUser.setUser(userMap.get(gaoUser.getUserId())); + gaoUser.getUser().setRoleEntityList(roleMap.get(gaoUser.getUserId())); + } + } + return result; } @AOPLog @@ -53,16 +82,14 @@ public class GaoUserController { @PostMapping("/detail") @JsonView(ResponseView.Public.class) public GaoUser detail(@RequestParam String id) { - return service.get(id); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.USER_READ) - @PostMapping("/detail/user") - @JsonView(ResponseView.Public.class) - public GaoUser detailByUserId(@RequestParam String userId) { - return service.getByUserId(userId); + GaoUser result = service.get(id); + { + User user = userService.get(result.getUserId()); + user.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.USER, user.getId())); + user.setRoleEntityList(userRoleService.listRoleByUserId(user.getId())); + result.setUser(user); + } + return result; } @AOPLog @@ -71,6 +98,9 @@ public class GaoUserController { @PostMapping("/create") @JsonView(ResponseView.Public.class) public GaoUser create(@RequestBody GaoUser gaoUser) { + if (!roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + gaoUser.setStoreId(gaoStoreService.getByUserId(userLoginService.getRequireLoginUserId()).getId()); + } service.create(gaoUser); return service.get(gaoUser.getId()); } @@ -85,6 +115,17 @@ public class GaoUserController { return service.get(gaoUser.getId()); } + @AOPLog + @RequestRateLimit + @RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER) + @RequireGaoPermission(GaoPermissionCode.USER_UPDATE) + @PostMapping("/transfer/store") + @JsonView(ResponseView.Public.class) + public GaoUser transferStore(@RequestBody GaoUser gaoUser) { + service.transferStore(gaoUser); + return service.get(gaoUser.getId()); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.USER_DELETE) diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomer.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomer.java index a37ed88..36573a3 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomer.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomer.java @@ -1,11 +1,11 @@ package com.imyeyu.api.modules.gao.entity; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.JsonNode; import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.bean.Gender; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.java.TimiJava; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; @@ -48,6 +48,9 @@ public class GaoCustomer extends UUIDEntity { /// 客户编码 private String code; + /// 门店 ID + private String storeId; + /// 姓名 private String name; @@ -55,7 +58,7 @@ public class GaoCustomer extends UUIDEntity { private Gender gender; /// 年龄 - private Integer age; + private Long age; /// 出生日期,数据库只保存公历日期 @JsonFormat(pattern = "yyyy-MM-dd") @@ -64,15 +67,6 @@ public class GaoCustomer extends UUIDEntity { /// 出生日期录入时使用的历法 private BirthdateCalendar birthdateCalendar; - @JsonIgnore - @Transient - private boolean birthdateProvided; - - public void setBirthdate(LocalDate birthdate) { - this.birthdate = birthdate; - this.birthdateProvided = true; - } - /// 电话 private String telephone; @@ -91,24 +85,27 @@ public class GaoCustomer extends UUIDEntity { /// 介绍人客户 ID private String introducerCustomerId; + private Long invitedCount; + /// 录入用户 ID private String creatorUserId; - @Transient - private Long lastSignInAt; - @Transient private GaoCustomer introducerCustomer; @Transient private User creatorUser; - private Long invitedCount; - @Transient private List attachmentList; - /// 临时照片附件 ID 列表 @Transient - private List attachmentIdList; + private GaoEventRecord eventRecord; + + @Transient + private List eventRecordList; + + public boolean hasIntroducer() { + return TimiJava.isNotEmpty(introducerCustomerId); + } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java similarity index 75% rename from src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEvent.java rename to src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java index 40a792b..0a354b1 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEvent.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java @@ -2,8 +2,10 @@ package com.imyeyu.api.modules.gao.entity; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.Getter; import java.util.List; @@ -14,7 +16,7 @@ import java.util.List; /// @since 2026-07-27 @Data @EqualsAndHashCode(callSuper = true) -public class GaoRegistrationEvent extends UUIDEntity { +public class GaoEvent extends UUIDEntity { /// /// 登记事件状态 @@ -36,38 +38,44 @@ public class GaoRegistrationEvent extends UUIDEntity { DELETED } - /// /// 登记限制类型 /// - /// @author Codex - /// @since 2026-08-01 + /// @author 夜雨 + /// @since 2026-08-01 14:37 + @Getter + @AllArgsConstructor public enum LimitType { /// 不限制 - NONE, + NONE(null), - /// 每天上午、下午各一次 - DAILY_HALF_DAY, + /// 每天上午、下午各 N 次 + DAILY_HALF_DAY(1L), - /// 每个自然日一次 - NATURAL_DAY, + /// 每个自然日 N 次 + NATURAL_DAY(1L), /// 每隔 N 小时一次 - INTERVAL_HOUR, + INTERVAL_HOUR(1L), - /// 每个自然月一次 - NATURAL_MONTH, + /// 每个自然月 N 次 + NATURAL_MONTH(1L), /// 事件总登记次数上限 - TOTAL_LIMIT, + TOTAL_LIMIT(1L), /// 单个客户总登记次数上限 - CUSTOMER_TOTAL_LIMIT + CUSTOMER_TOTAL_LIMIT(1L); + + final Long min; } /// 登记事件名称 private String name; + /// 门店 ID + private String storeId; + /// 说明 private String description; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEventRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java similarity index 66% rename from src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEventRecord.java rename to src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java index ec92999..39f4739 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEventRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java @@ -16,7 +16,7 @@ import java.util.List; /// @since 2026-07-27 @Data @EqualsAndHashCode(callSuper = true) -public class GaoRegistrationEventRecord extends UUIDEntity { +public class GaoEventRecord extends UUIDEntity { /// /// 登记事件记录附件类型 @@ -31,8 +31,11 @@ public class GaoRegistrationEventRecord extends UUIDEntity { /// 客户 ID private String customerId; + /// 门店 ID + private String storeId; + /// 登记登记事件 ID - private String registrationEventId; + private String eventId; /// 操作管理员用户 ID private String operatorUserId; @@ -43,11 +46,21 @@ public class GaoRegistrationEventRecord extends UUIDEntity { /// 登记时间 private Long registeredAt; + /// 登记日期值,格式为 yyyyMMdd + private Integer registeredDateValue; + + /// 营业日递增序号 + private Long businessDayNo; + + /// 本次登记提交的积分值,只参与积分触发,不落库 + @Transient + private Long pointValue; + @Transient private GaoCustomer customer; @Transient - private GaoRegistrationEvent registrationEvent; + private GaoEvent event; @Transient private User operator; @@ -55,7 +68,7 @@ public class GaoRegistrationEventRecord extends UUIDEntity { @Transient private List attachmentList; - /// 临时照片附件 ID 列表 + /// 客户 ID 列表,批量登记时使用 @Transient - private List attachmentIdList; + private List customerIdList; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEventRoleRelation.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRoleRelation.java similarity index 76% rename from src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEventRoleRelation.java rename to src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRoleRelation.java index 0922068..e5f0ba7 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoRegistrationEventRoleRelation.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRoleRelation.java @@ -11,10 +11,10 @@ import lombok.EqualsAndHashCode; /// @since 2026-07-28 @Data @EqualsAndHashCode(callSuper = true) -public class GaoRegistrationEventRoleRelation extends UUIDEntity { +public class GaoEventRoleRelation extends UUIDEntity { /// 登记事件 ID - private String registrationEventId; + private String eventId; /// 角色代码 private String roleCode; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointAccount.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointAccount.java new file mode 100644 index 0000000..87c06eb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointAccount.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 客户积分账户 +/// +/// @author Codex +/// @since 2026-08-11 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPointAccount extends UUIDEntity { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 当前可用积分余额 + private Long balance; + + /// 累计获得积分 + private Long totalEarned; + + /// 累计撤销积分 + private Long totalRevoked; + + /// 累计人工调整积分,可正可负 + private Long totalAdjusted; + + /// 乐观锁版本 + private Long version; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java new file mode 100644 index 0000000..550c558 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java @@ -0,0 +1,86 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 客户积分流水 +/// +/// @author Codex +/// @since 2026-08-11 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPointLedger extends UUIDEntity { + + /// 流水变化类型 + public enum ChangeType { + + /// 登记事件获得积分 + EARN, + + /// 删除登记记录撤销积分 + REVOKE, + + /// 人工调整积分 + ADJUST + } + + /// 流水方向 + public enum Direction { + + /// 增加积分 + INCREASE, + + /// 减少积分 + DECREASE + } + + /// 客户积分账户 ID + private String accountId; + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 变化类型 + private ChangeType changeType; + + /// 方向 + private Direction direction; + + /// 本次变化积分,增加为正,减少为负 + private Long pointDelta; + + /// 变化前余额 + private Long balanceBefore; + + /// 变化后余额 + private Long balanceAfter; + + /// 命中的积分规则 ID + private String ruleId; + + /// 登记事件 ID + private String eventId; + + /// 登记记录 ID + private String eventRecordId; + + /// 来源快照 + private JsonNode sourcePayload; + + /// 幂等键 + private String idempotencyKey; + + /// 备注 + private String remark; + + /// 操作用户 ID + private String operatorUserId; + + /// 发生时间 + private Long occurredAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java new file mode 100644 index 0000000..b37d46c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java @@ -0,0 +1,96 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 客户积分规则 +/// +/// @author Codex +/// @since 2026-08-11 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPointRule extends UUIDEntity { + + /// 规则状态 + public enum Status { + + /// 草稿,不参与触发 + DRAFT, + + /// 启用,参与触发 + ACTIVE, + + /// 停用,不参与触发 + INACTIVE, + + /// 已删除,不参与触发 + DELETED + } + + /// 触发类型 + public enum TriggerType { + + /// 每登记一次触发一次 + EVERY_TIME, + + /// 客户首次登记该事件触发一次 + FIRST_TIME, + + /// 客户累计登记达到 N 次后每次触发 + AFTER_COUNT_EVERY_TIME, + + /// 使用登记接口提交的积分值触发 + SOURCE_VALUE + } + + /// 积分值模式 + public enum PointMode { + + /// 使用规则固定积分值 + FIXED, + + /// 使用登记接口提交的积分值 + SOURCE_VALUE + } + + /// 门店 ID + private String storeId; + + /// 登记事件 ID + private String eventId; + + /// 规则名称 + private String name; + + /// 规则状态 + private Status status; + + /// 触发类型 + private TriggerType triggerType; + + /// 积分值模式 + private PointMode pointMode; + + /// 固定积分值 + private Long pointValue; + + /// 条件配置 + private JsonNode conditionJson; + + /// 生效开始时间戳,毫秒,空为不限制 + private Long beginAt; + + /// 生效结束时间戳,毫秒,空为不限制 + private Long endAt; + + /// 优先级,数值小优先 + private Integer priority; + + /// true 为命中后停止继续匹配后续规则 + private Boolean exclusive; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStore.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStore.java new file mode 100644 index 0000000..f9042b0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStore.java @@ -0,0 +1,57 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// +/// GAO 门店 +/// +/// @author Codex +/// @since 2026-08-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoStore extends UUIDEntity { + + /// + /// 门店状态 + /// + /// @author Codex + /// @since 2026-08-06 + public enum Status { + + /// 可用 + ACTIVE, + + /// 停用 + INACTIVE, + + /// 已删除 + DELETED + } + + /// 门店编码 + private String code; + + /// 门店名称 + private String name; + + /// 状态 + private Status status; + + /// 电话 + private String telephone; + + /// 地址 + private String address; + + /// 备注 + private String remark; + + /// 排序值 + private Integer sort; + + public boolean isActivated() { + return status == Status.ACTIVE; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStoreBusinessDay.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStoreBusinessDay.java new file mode 100644 index 0000000..6192ef0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStoreBusinessDay.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// +/// GAO 门店营业日 +/// +/// @author Codex +/// @since 2026-08-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoStoreBusinessDay extends UUIDEntity { + + /// + /// 营业状态 + /// + /// @author Codex + /// @since 2026-08-06 + public enum OpenStatus { + + /// 营业 + OPEN, + + /// 休息 + CLOSED + } + + /// 门店 ID + private String storeId; + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 营业状态 + private OpenStatus openStatus; + + /// 营业日递增序号,仅营业日有值 + private Long businessDayNo; + + /// 原因 + private String reason; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoUser.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoUser.java index fd012ce..133ebdd 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoUser.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoUser.java @@ -1,14 +1,11 @@ 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 用户 /// @@ -21,6 +18,9 @@ public class GaoUser extends UUIDEntity { /// 用户 ID private String userId; + /// 门店 ID + private String storeId; + /// true 为启用 private Boolean enabled; @@ -29,10 +29,4 @@ public class GaoUser extends UUIDEntity { @Transient private User user; - - @Transient - private List roleList; - - @Transient - private List roleIdList; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java index 3c6aa26..cde3fac 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerMapper.java @@ -1,12 +1,12 @@ 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.api.modules.gao.vo.GaoCustomerDailyStateView; import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; import com.imyeyu.spring.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; +import java.util.Collection; import java.util.List; /// @@ -16,41 +16,23 @@ import java.util.List; /// @since 2026-07-27 public interface GaoCustomerMapper extends BaseMapper { - @Select("SELECT * FROM `gao_customer` WHERE `code` = #{code} AND `deleted_at` IS NULL LIMIT 1") - GaoCustomer selectByCode(@Param("code") String code); - - @Select("SELECT * FROM `gao_customer` WHERE `name` = #{name} AND `deleted_at` IS NULL LIMIT 1") - GaoCustomer selectByName(@Param("name") String name); - - List selectByIdList(@Param("idList") List idList); - - List search(@Param("keyword") String keyword, @Param("introducerCustomerId") String introducerCustomerId); - - Long countSearch(@Param("keyword") String keyword, @Param("introducerCustomerId") String introducerCustomerId); - - List searchPage(@Param("keyword") String keyword, @Param("introducerCustomerId") String introducerCustomerId, @Param("offset") Long offset, @Param("size") Long size); - - List selectInvitedList(@Param("introducerCustomerId") String introducerCustomerId); - - Long countInvited(@Param("introducerCustomerId") String introducerCustomerId); - - void updateInvitedCountDelta(@Param("customerId") String customerId, @Param("delta") Long delta); + List selectByIdList(Collection idList); /// 统计每日新增客户数 /// /// @param beginCreatedAt 开始创建时间 /// @param endCreatedAt 结束创建时间 /// @return 每日新增客户数 - List statDailyNewCustomer(@Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt); + List stateDailyNewCustomer(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt); /// 统计开始时间前客户总数 /// /// @param beginCreatedAt 开始创建时间 /// @return 客户总数 - Long countCreatedBefore(@Param("beginCreatedAt") Long beginCreatedAt); + Long countCreatedBefore(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt); /// 统计客户性别分布 /// /// @return 性别统计列表 - List statGender(); + List stateGender(@Param("storeId") String storeId); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java new file mode 100644 index 0000000..259b345 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// +/// 登记事件 Mapper +/// +/// @author 夜雨 +/// @since 2026-07-27 15:53 +public interface GaoEventMapper extends BaseMapper { + + List selectByIdList(Collection idList); + + List selectValidList(@Param("storeId") String storeId, @Param("now") Long now); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java new file mode 100644 index 0000000..cbd2f00 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java @@ -0,0 +1,80 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; +import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// 登记事件记录 Mapper +/// +/// @author 夜雨 +/// @since 2026-07-27 14:11 +public interface GaoEventRecordMapper extends BaseMapper { + + Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt); + + /// 按门店和登记日期统计登记记录数 + /// + /// @param storeId 门店 ID + /// @param registeredDateValue 登记日期值,格式为 yyyyMMdd + /// @return 登记记录数 + Long countByStoreIdAndRegisteredDateValue(@Param("storeId") String storeId, @Param("registeredDateValue") Integer registeredDateValue); + + /// 同步门店指定日期的登记记录营业日序号快照 + /// + /// @param storeId 门店 ID + /// @param registeredDateValue 登记日期值,格式为 yyyyMMdd + /// @param businessDayNo 营业日序号 + void updateBusinessDayNoByStoreIdAndRegisteredDateValue(@Param("storeId") String storeId, @Param("registeredDateValue") Integer registeredDateValue, @Param("businessDayNo") Long businessDayNo); + + long countByRange(GaoEventRecordPage page); + + /// 按任意时间范围查询登记事件记录 + /// + /// @return 登记事件记录列表 + List selectByRange(GaoEventRecordPage page); + + /// 统计客户登记排行榜总数 + /// + /// @param page 排行榜分页请求 + /// @return 客户数 + long countRankByRange(GaoEventRecordRankPage page); + + /// 分页查询客户登记排行榜 + /// + /// @param page 排行榜分页请求 + /// @return 客户登记排行榜 + List selectRankByRange(GaoEventRecordRankPage page); + + /// 按时间范围和事件批量统计每日登记数 + /// + /// @param req 门店报表查询请求 + /// @return 每日登记数 + List stateDailyByRange(GaoStoreChartReq req); + + /// 按时间范围查询登记日历记录 + /// + /// @param req 门店报表查询请求 + /// @return 日历记录列表 + List selectCalendarByRange(GaoStoreChartReq req); + + /// 按客户、时间范围和事件批量统计每日登记数 + /// + /// @param req 客户报表查询请求 + /// @return 每日登记数 + List stateCustomerDailyByRange(GaoCustomerChartReq req); + + /// 按客户和时间范围查询登记日历记录 + /// + /// @param req 客户报表查询请求 + /// @return 日历记录列表 + List selectCustomerCalendarByRange(GaoCustomerChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRoleRelationMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRoleRelationMapper.java new file mode 100644 index 0000000..ec5cc81 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRoleRelationMapper.java @@ -0,0 +1,16 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation; +import com.imyeyu.spring.mapper.BaseMapper; + +import java.util.Collection; +import java.util.List; + +/// 登记事件角色关联 Mapper +/// +/// @author 夜雨 +/// @since 2026-07-28 14:23 +public interface GaoEventRoleRelationMapper extends BaseMapper { + + List selectAllByEventIdList(Collection idList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointAccountMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointAccountMapper.java new file mode 100644 index 0000000..4392dc3 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointAccountMapper.java @@ -0,0 +1,41 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoPointAccount; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +/// GAO 客户积分账户 Mapper +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoPointAccountMapper extends BaseMapper { + + /// 查询客户当前有效积分账户 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @return 积分账户 + GaoPointAccount selectByStoreIdAndCustomerId(@Param("storeId") String storeId, @Param("customerId") String customerId); + + /// 创建客户积分账户,已存在时保持原账户不变 + /// + /// @param account 积分账户 + void insertIfAbsent(@Param("account") GaoPointAccount account); + + /// 锁定积分账户 + /// + /// @param id 账户 ID + /// @return 积分账户 + GaoPointAccount selectForUpdate(@Param("id") String id); + + /// 原子更新积分账户余额和统计值 + /// + /// @param id 账户 ID + /// @param delta 本次余额变化 + /// @param totalEarnedDelta 累计获得积分变化 + /// @param totalRevokedDelta 累计撤销积分变化 + /// @param totalAdjustedDelta 累计人工调整积分变化 + /// @param updatedAt 更新时间 + /// @return 更新行数 + int updateBalance(@Param("id") String id, @Param("delta") long delta, @Param("totalEarnedDelta") long totalEarnedDelta, @Param("totalRevokedDelta") long totalRevokedDelta, @Param("totalAdjustedDelta") long totalAdjustedDelta, @Param("updatedAt") long updatedAt); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java new file mode 100644 index 0000000..e1934fd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java @@ -0,0 +1,45 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 客户积分流水 Mapper +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoPointLedgerMapper extends BaseMapper { + + /// 按幂等键查询流水 + /// + /// @param idempotencyKey 幂等键 + /// @return 流水 + GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey); + + /// 查询登记记录产生的原始获得积分流水 + /// + /// @param eventRecordId 登记记录 ID + /// @return 获得积分流水 + List selectEarnByEventRecordId(@Param("eventRecordId") String eventRecordId); + + /// 批量查询幂等键对应的流水 + /// + /// @param idempotencyKeyList 幂等键列表 + /// @return 已存在流水 + List selectByIdempotencyKeyList(@Param("idempotencyKeyList") List idempotencyKeyList); + + /// 查询积分流水总数 + /// + /// @param page 查询条件 + /// @return 流水总数 + long countByQuery(GaoPointLedgerPage page); + + /// 分页查询积分流水 + /// + /// @param page 查询条件 + /// @return 流水列表 + List selectByQuery(GaoPointLedgerPage page); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java new file mode 100644 index 0000000..15a5807 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java @@ -0,0 +1,22 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 客户积分规则 Mapper +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoPointRuleMapper extends BaseMapper { + + /// 查询当前时间可匹配的启用规则 + /// + /// @param storeId 门店 ID + /// @param eventId 登记事件 ID + /// @param now 当前时间戳 + /// @return 按优先级排序的规则列表 + List selectActiveByStoreIdAndEventId(@Param("storeId") String storeId, @Param("eventId") String eventId, @Param("now") long now); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventMapper.java deleted file mode 100644 index 5ccd039..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventMapper.java +++ /dev/null @@ -1,19 +0,0 @@ -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.Param; - -import java.util.List; - -/// -/// 登记事件 Mapper -/// -/// @author Codex -/// @since 2026-07-27 -public interface GaoRegistrationEventMapper extends BaseMapper { - - List selectByIdList(@Param("idList") List idList); - - List selectEnabledList(@Param("now") Long now); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventRecordMapper.java deleted file mode 100644 index 71c1dc3..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventRecordMapper.java +++ /dev/null @@ -1,157 +0,0 @@ -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.GaoCustomerRegistrationDayStatView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView; -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 { - - /// 按登记事件查询客户最后登记事件记录 - /// - /// @param customerIdList 客户 ID 列表 - /// @param registrationEventId 登记事件 ID - /// @return 最后登记事件记录列表 - List selectByRegistrationEventAndPeriod(@Param("registrationEventId") String registrationEventId, @Param("periodType") GaoRegistrationEventRecordPeriodReq.PeriodType periodType, @Param("periodValue") Integer periodValue); - - /// 统计客户指定登记事件在时间范围内的登记数 - /// - /// @param customerId 客户 ID - /// @param registrationEventId 登记事件 ID - /// @param beginRegisteredAt 开始登记时间 - /// @param endRegisteredAt 结束登记时间 - /// @return 登记数 - Long countByCustomerIdAndRegistrationEventIdRange(@Param("customerId") String customerId, @Param("registrationEventId") String registrationEventId, @Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt); - - /// 统计客户指定登记事件总登记数 - /// - /// @param customerId 客户 ID - /// @param registrationEventId 登记事件 ID - /// @return 登记数 - Long countByCustomerIdAndRegistrationEventId(@Param("customerId") String customerId, @Param("registrationEventId") String registrationEventId); - - /// 统计登记事件总登记数 - /// - /// @param registrationEventId 登记事件 ID - /// @return 登记数 - Long countByRegistrationEventId(@Param("registrationEventId") String registrationEventId); - - /// 按周期统计登记事件记录 - /// - /// @param registrationEventId 登记事件 ID - /// @param periodType 周期类型 - /// @return 统计结果 - List statByRegistrationEventAndPeriodType(@Param("registrationEventId") String registrationEventId, @Param("periodType") GaoRegistrationEventRecordPeriodReq.PeriodType periodType); - - /// 统计客户所有登记 - /// - /// @param customerId 客户 ID - /// @return 统计结果 - List statByCustomerId(@Param("customerId") String customerId); - - /// 按时间段统计单个客户登记 - /// - /// @param customerId 客户 ID - /// @param beginRegisteredAt 开始登记时间 - /// @param endRegisteredAt 结束登记时间 - /// @return 统计结果 - List statByCustomerIdAndRange(@Param("customerId") String customerId, @Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt); - - /// 按时间段统计全部客户登记 - /// - /// @param beginRegisteredAt 开始登记时间 - /// @param endRegisteredAt 结束登记时间 - /// @return 统计结果 - List statAllCustomerByRange(@Param("beginRegisteredAt") Long beginRegisteredAt, @Param("endRegisteredAt") Long endRegisteredAt); - - /// 按时间范围统计全部客户每日登记 - /// - /// @param req 范围查询请求 - /// @return 每个有登记客户的每日统计 - List statCustomerRegistrationDay(@Param("req") GaoRegistrationEventRecordRangeReq req); - - /// 统计单个客户单个登记事件的每日登记事件记录数 - /// - /// @param customerId 客户 ID - /// @param registrationEventId 登记事件 ID - /// @param beginRegisteredAt 开始登记时间 - /// @param endRegisteredAt 结束登记时间 - /// @return 每日统计 - List 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 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 selectLatestByCustomerIdListAndRegistrationEventId(@Param("customerIdList") List customerIdList, @Param("registrationEventId") String registrationEventId); - - /// 查询客户全部登记事件的最后登记事件记录 - /// - /// @param customerId 客户 ID - /// @return 最后登记事件记录列表 - List selectLatestByCustomerId(@Param("customerId") String customerId); - - /// 按任意时间范围查询登记事件记录 - /// - /// @param req 范围查询请求 - /// @return 登记事件记录列表 - List selectByRange(@Param("req") GaoRegistrationEventRecordRangeReq req); - - /// 按任意时间范围查询日历轻量记录 - /// - /// @param req 范围查询请求 - /// @return 日历轻量记录列表 - List selectCalendarByRange(@Param("req") GaoRegistrationEventRecordRangeReq req); - - /// 按任意时间范围统计登记事件记录 - /// - /// @param req 范围查询请求 - /// @return 登记事件记录数 - Long countByRange(@Param("req") GaoRegistrationEventRecordRangeReq req); - - /// 按任意时间范围分页查询登记事件记录 - /// - /// @param req 范围查询请求 - /// @param offset 偏移量 - /// @param size 每页条数 - /// @return 登记事件记录列表 - List selectPageByRange(@Param("req") GaoRegistrationEventRecordRangeReq req, @Param("offset") Long offset, @Param("size") Long size); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventRoleRelationMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventRoleRelationMapper.java deleted file mode 100644 index 0a66c9d..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoRegistrationEventRoleRelationMapper.java +++ /dev/null @@ -1,36 +0,0 @@ -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 { - - @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 selectRoleCodeListByRegistrationEventId(@Param("registrationEventId") String registrationEventId); - - @Select({ - "" - }) - List selectByRegistrationEventIdList(@Param("registrationEventIdList") List registrationEventIdList); - - @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); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreBusinessDayMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreBusinessDayMapper.java new file mode 100644 index 0000000..0ea3a57 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreBusinessDayMapper.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/// +/// GAO 门店营业日 Mapper +/// +/// @author Codex +/// @since 2026-08-06 +public interface GaoStoreBusinessDayMapper extends BaseMapper { + + /// 按门店和日期查询营业日 + /// + /// @param storeId 门店 ID + /// @param dateValue 日期值,格式为 yyyyMMdd + /// @return 营业日 + @Select("SELECT * FROM `gao_store_business_day` WHERE `store_id` = #{storeId} AND `date_value` = #{dateValue} AND `deleted_at` IS NULL LIMIT 1") + GaoStoreBusinessDay selectByStoreIdAndDateValue(@Param("storeId") String storeId, @Param("dateValue") Integer dateValue); + + /// 查询指定日期前最后一个营业日序号 + /// + /// @param storeId 门店 ID + /// @param dateValue 日期值,格式为 yyyyMMdd + /// @return 营业日序号 + @Select("SELECT MAX(`business_day_no`) FROM `gao_store_business_day` WHERE `store_id` = #{storeId} AND `date_value` < #{dateValue} AND `open_status` = 'OPEN' AND `deleted_at` IS NULL") + Long selectLastBusinessDayNoBefore(@Param("storeId") String storeId, @Param("dateValue") Integer dateValue); + + /// 查询指定日期及之后的营业日 + /// + /// @param storeId 门店 ID + /// @param beginDateValue 开始日期值,格式为 yyyyMMdd + /// @return 营业日列表 + @Select("SELECT * FROM `gao_store_business_day` WHERE `store_id` = #{storeId} AND `date_value` >= #{beginDateValue} AND `open_status` = 'OPEN' AND `deleted_at` IS NULL ORDER BY `date_value` ASC") + List selectOpenListFromDate(@Param("storeId") String storeId, @Param("beginDateValue") Integer beginDateValue); + + /// 查询日期范围内的营业日 + /// + /// @param storeId 门店 ID + /// @param beginDateValue 开始日期值,格式为 yyyyMMdd + /// @param endDateValue 结束日期值,格式为 yyyyMMdd + /// @return 营业日列表 + @Select("SELECT * FROM `gao_store_business_day` WHERE `store_id` = #{storeId} AND `date_value` >= #{beginDateValue} AND `date_value` <= #{endDateValue} AND `deleted_at` IS NULL ORDER BY `date_value` ASC") + List selectByStoreIdAndDateRange(@Param("storeId") String storeId, @Param("beginDateValue") Integer beginDateValue, @Param("endDateValue") Integer endDateValue); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreMapper.java new file mode 100644 index 0000000..0dcb790 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreMapper.java @@ -0,0 +1,24 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// +/// GAO 门店 Mapper +/// +/// @author Codex +/// @since 2026-08-06 +public interface GaoStoreMapper extends BaseMapper { + + List selectByIdList(@Param("idList") Collection idList); + + /// 查询用户所属的可用门店 + /// + /// @param userId 用户 ID + /// @return 门店 + GaoStore selectByUserId(@Param("userId") String userId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java index 88d550c..dc94f6e 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java @@ -2,8 +2,6 @@ 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 @@ -11,7 +9,4 @@ import org.apache.ibatis.annotations.Select; /// @author Codex /// @since 2026-07-30 public interface GaoUserMapper extends BaseMapper { - - @Select("SELECT * FROM `gao_user` WHERE `user_id` = #{userId} AND `deleted_at` IS NULL LIMIT 1") - GaoUser selectByUserId(@Param("userId") String userId); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerChartService.java new file mode 100644 index 0000000..252a50f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerChartService.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; + +import java.util.List; + +/// 客户报表服务 +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoCustomerChartService { + + /// 查询客户登记每日统计 + List eventRecordDailyStat(GaoCustomerChartReq req); + + /// 查询客户登记日历记录 + List eventRecordCalendar(GaoCustomerChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java index 928e23f..2c15d7a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerService.java @@ -1,60 +1,50 @@ 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.GaoCustomerDailyStateView; import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; -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.GaoCustomerTrendStateReq; import com.imyeyu.spring.bean.Page; -import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; import java.io.IOException; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -/// /// 客户服务 /// -/// @author Codex -/// @since 2026-07-27 +/// @author 夜雨 +/// @since 2026-07-27 14:12 public interface GaoCustomerService extends BaseService { + void updateWithAttachment(GaoCustomer customer); + /// 按编码查询客户 /// + /// @param storeId 门店 ID /// @param code 客户编码 /// @return 客户 - GaoCustomer getByCode(String code); + GaoCustomer getByCode(String storeId, String code); - /// 按编码查询客户,不存在时返回 null + /// 按姓名查询客户 /// - /// @param code 客户编码 + /// @param storeId 门店 ID + /// @param name 姓名 /// @return 客户 - GaoCustomer getNullableByCode(String code); + GaoCustomer getByName(String storeId, String name); - /// 按编码获取或快速创建客户 - /// - /// @param code 客户编码 - /// @return 客户 - GaoCustomer getOrQuickCreateByCode(String code); + List listByIdList(Collection idList); - /// 搜索客户 - /// - /// @param req 搜索请求 - /// @return 客户列表 - List search(GaoCustomerSearchReq req); - - /// 分页搜索客户 - /// - /// @param page 分页搜索请求 - /// @return 客户分页结果 - PageResult searchPage(Page page); + default Map mapByIdList(Collection idList) { + return listByIdList(idList).stream().collect(Collectors.toMap(GaoCustomer::getId, item -> item)); + } /// 按搜索条件导出客户 Excel /// - /// @param req 搜索请求 /// @return xlsx 文件字节 - byte[] exportSearchExcel(GaoCustomerSearchReq req) throws IOException; + byte[] export(Page page) throws IOException; /// 查询邀请的客户 /// @@ -64,8 +54,9 @@ public interface GaoCustomerService extends BaseService { /// 保存邀请关系 /// - /// @param req 邀请关系请求 - void saveInviteRelation(GaoCustomerInviteUpdateReq req); + /// @param introducerCustomerId 邀请来源客户 ID + /// @param customerId 客户 ID + void saveInviteRelation(String introducerCustomerId, String customerId); /// 删除邀请关系 /// @@ -76,10 +67,11 @@ public interface GaoCustomerService extends BaseService { /// /// @param req 趋势统计请求 /// @return 每日趋势列表 - List statDailyTrend(GaoCustomerTrendStatReq req); + List stateDailyTrend(GaoCustomerTrendStateReq req); /// 统计客户性别分布 /// + /// @param storeId 门店 ID /// @return 性别统计列表 - List statGender(); + List stateGender(String storeId); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java new file mode 100644 index 0000000..3fb4fb8 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java @@ -0,0 +1,46 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +import com.imyeyu.java.TimiJava; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.io.IOException; +import java.util.List; + +/// 登记事件记录服务 +/// +/// @author 夜雨 +/// @since 2026-07-27 14:09 +public interface GaoEventRecordService extends BaseService { + + default void createBatch(List recordList) { + for (GaoEventRecord record : TimiJava.safeIterable(recordList)) { + create(record); + } + } + + void createWithCustomer(GaoCustomer customer); + + /// 按任意时间范围查询登记事件记录 + /// + /// @param req 范围查询请求 + /// @return 登记事件记录列表 + PageResult pageByRange(GaoEventRecordPage req); + + /// 按时间范围查询客户登记排行榜 + /// + /// @param page 范围查询请求,不传时间为全部时间 + /// @return 客户登记排行榜 + PageResult pageRankByRange(GaoEventRecordRankPage page); + + /// 按任意时间范围导出登记事件记录 Excel + /// + /// @param page 范围查询请求 + /// @return xlsx 文件字节 + byte[] export(GaoEventRecordPage page) throws IOException; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java new file mode 100644 index 0000000..6331a21 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.spring.service.BaseService; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/// +/// 登记事件服务 +/// +/// @author Codex +/// @since 2026-07-27 +public interface GaoEventService extends BaseService { + + Map mapByIdList(Collection idList); + + Map> mapRoleCodeListByEventIdList(Collection eventIdList); + + /// 查询启用的登记事件 + /// + /// @param storeId 门店 ID + /// @return 登记事件列表 + List listValid(String storeId); + + /// 查询登记事件要求的角色 + /// + /// @param eventId 登记事件 ID + /// @return 角色代码列表 + List listRoleCode(String eventId); + + /// 按客户端提交的顺序更新登记事件排序 + /// + /// @param idList 登记事件 ID 列表 + void sort(List idList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointAccountService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointAccountService.java new file mode 100644 index 0000000..c824b8a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointAccountService.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoPointAccount; +import com.imyeyu.spring.service.BaseService; + +/// GAO 客户积分账户服务 +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoPointAccountService extends BaseService { + + /// 查询客户当前有效积分账户 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @return 积分账户,不存在时返回 null + GaoPointAccount getByCustomerId(String storeId, String customerId); + + /// 获取并锁定客户积分账户,不存在时创建 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @return 已锁定的积分账户 + GaoPointAccount getOrCreateForUpdate(String storeId, String customerId); + + /// 原子更新账户余额 + /// + /// @param accountId 账户 ID + /// @param delta 本次余额变化 + /// @param totalEarnedDelta 累计获得积分变化 + /// @param totalRevokedDelta 累计撤销积分变化 + /// @param totalAdjustedDelta 累计人工调整积分变化 + /// @return 更新行数 + int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java new file mode 100644 index 0000000..e544369 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java @@ -0,0 +1,46 @@ +package com.imyeyu.api.modules.gao.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest; +import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +/// GAO 客户积分流水服务 +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoPointLedgerService extends BaseService { + + /// 写入登记获得积分流水 + /// + /// @param record 登记记录 + /// @param rule 命中规则 + /// @param pointDelta 积分变化 + /// @param sourcePayload 来源快照 + /// @return 积分流水 + GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload); + + /// 写入删除登记记录的撤销流水 + /// + /// @param originLedger 原始获得积分流水 + /// @param record 登记记录 + /// @return 撤销流水 + GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record); + + /// 写入人工调整流水 + /// + /// @param request 人工调整请求 + /// @param operatorUserId 操作用户 ID + /// @return 调整流水 + GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId); + + /// 分页查询积分流水 + /// + /// @param page 查询条件 + /// @return 分页结果 + PageResult pageByQuery(GaoPointLedgerPage page); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java new file mode 100644 index 0000000..5dbc7ff --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 客户积分规则服务 +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoPointRuleService extends BaseService { + + /// 查询当前时间可匹配的启用规则 + /// + /// @param storeId 门店 ID + /// @param eventId 登记事件 ID + /// @param now 当前时间戳 + /// @return 规则列表 + List listActive(String storeId, String eventId, long now); + + /// 创建积分规则 + /// + /// @param rule 积分规则 + void create(GaoPointRule rule); + + /// 修改积分规则 + /// + /// @param rule 积分规则 + void update(GaoPointRule rule); + + /// 删除积分规则 + /// + /// @param id 规则 ID + void delete(String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java new file mode 100644 index 0000000..b2e47db --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest; + +/// GAO 客户积分触发服务 +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoPointTriggerService { + + /// 在登记记录创建成功后触发积分 + /// + /// @param record 登记记录 + void onEventRecordCreated(GaoEventRecord record); + + /// 撤销登记记录产生的全部获得积分 + /// + /// @param record 登记记录 + void revokeByEventRecord(GaoEventRecord record); + + /// 执行人工调整 + /// + /// @param request 人工调整请求 + /// @return 调整流水 + GaoPointLedger adjust(GaoPointAdjustRequest request); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoRegistrationEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoRegistrationEventRecordService.java deleted file mode 100644 index 6ad2a85..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoRegistrationEventRecordService.java +++ /dev/null @@ -1,127 +0,0 @@ -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.GaoCustomerRegistrationRankView; -import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView; -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.bean.Page; -import com.imyeyu.spring.bean.PageResult; -import com.imyeyu.spring.service.BaseService; - -import java.io.IOException; -import java.util.List; - -/// -/// 登记事件记录服务 -/// -/// @author Codex -/// @since 2026-07-27 -public interface GaoRegistrationEventRecordService extends BaseService { - - /// 批量创建登记事件记录 - /// - /// @param req 批量创建请求 - /// @return 创建后的登记事件记录列表 - List createRecords(GaoRegistrationEventRecordCreateReq req); - - /// 快速录入客户并创建登记事件记录 - /// - /// @param req 快速录入请求 - /// @return 创建后的登记事件记录 - GaoRegistrationEventRecord quickRegister(GaoQuickRegistrationEventRecordReq req); - - /// 按任意时间范围查询登记事件记录 - /// - /// @param req 范围查询请求 - /// @return 登记事件记录列表 - List listByRange(GaoRegistrationEventRecordRangeReq req); - - /// 按任意时间范围查询日历轻量记录 - /// - /// @param req 范围查询请求 - /// @return 日历轻量记录列表 - List listCalendarByRange(GaoRegistrationEventRecordRangeReq req); - - /// 按任意时间范围分页查询登记事件记录 - /// - /// @param page 分页参数 - /// @return 登记事件记录分页结果 - PageResult pageByRange(Page page); - - /// 按任意时间范围导出登记事件记录 Excel - /// - /// @param req 范围查询请求 - /// @return xlsx 文件字节 - byte[] exportRangeExcel(GaoRegistrationEventRecordRangeReq req) throws IOException; - - /// 按周期查询登记事件记录 - /// - /// @param req 周期请求 - /// @return 登记事件记录列表 - List listByRegistrationEventAndPeriod(GaoRegistrationEventRecordPeriodReq req); - - /// 按周期统计登记事件记录 - /// - /// @param registrationEventId 登记事件 ID - /// @param periodType 周期类型 - /// @return 统计列表 - List statByRegistrationEventAndPeriodType(String registrationEventId, GaoRegistrationEventRecordPeriodReq.PeriodType periodType); - - /// 查询客户指定登记事件的最后登记事件记录 - /// - /// @param customerIdList 客户 ID 列表 - /// @param registrationEventId 登记事件 ID - /// @return 每个客户的最后登记事件记录 - List listLatestByCustomerIdListAndRegistrationEventId(List customerIdList, String registrationEventId); - - /// 统计客户所有登记 - /// - /// @param customerId 客户 ID - /// @return 统计结果 - List statByCustomerId(String customerId); - - /// 按时间段统计单个客户登记 - /// - /// @param req 统计请求 - /// @return 统计结果 - List statByCustomerAndRange(GaoRegistrationEventRecordStatReq req); - - /// 按时间段统计全部客户登记 - /// - /// @param req 统计请求 - /// @return 统计结果 - List statAllCustomerByRange(GaoRegistrationEventRecordStatReq req); - - /// 按时间范围查询客户登记排行榜 - /// - /// @param req 范围查询请求,不传时间为全部时间 - /// @return 客户登记排行榜 - List listCustomerRegistrationRank(GaoRegistrationEventRecordRangeReq req); - - /// 按时间段统计单个客户单个登记事件每日趋势 - /// - /// @param req 趋势统计请求 - /// @return 每日趋势列表 - List statDailyByCustomerAndRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req); - - /// 按时间段统计全部客户单个登记事件每日趋势 - /// - /// @param req 趋势统计请求 - /// @return 每日趋势列表 - List statDailyByRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req); - - /// 查询客户全部登记事件的最后登记事件记录 - /// - /// @param customerId 客户 ID - /// @return 每个登记事件的最后登记事件记录 - List listLatestByCustomerId(String customerId); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoRegistrationEventService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoRegistrationEventService.java deleted file mode 100644 index f6f609c..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoRegistrationEventService.java +++ /dev/null @@ -1,38 +0,0 @@ -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 { - - /// 分页查询登记事件并填充可登记角色 - /// - /// @param page 分页参数 - /// @return 登记事件分页 - PageResult pageWithRegistrationEventLimit(Page page); - - /// 查询启用的登记事件 - /// - /// @return 登记事件列表 - List listEnabled(); - - /// 查询登记事件要求的角色 - /// - /// @param registrationEventId 登记事件 ID - /// @return 角色代码列表 - List listRoleCode(String registrationEventId); - - /// 按客户端提交的顺序更新登记事件排序 - /// - /// @param idList 登记事件 ID 列表 - void sort(List idList); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreBusinessDayService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreBusinessDayService.java new file mode 100644 index 0000000..fb6eb78 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreBusinessDayService.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; +import com.imyeyu.spring.service.BaseService; + +/// +/// GAO 门店营业日服务 +/// +/// @author Codex +/// @since 2026-08-06 +public interface GaoStoreBusinessDayService extends BaseService { + + /// 解析营业日,不存在时按默认营业日创建 + /// + /// @param storeId 门店 ID + /// @param dateValue 日期值,格式为 yyyyMMdd + /// @return 营业日 + GaoStoreBusinessDay resolveOpenBusinessDay(String storeId, Integer dateValue); + + /// 重算门店营业日序号并同步登记记录快照 + /// + /// @param storeId 门店 ID + /// @param beginDateValue 开始日期值,格式为 yyyyMMdd + void rebuildBusinessDayNo(String storeId, Integer beginDateValue); + + /// 查询日期范围内的营业日 + /// + /// @param storeId 门店 ID + /// @param beginDateValue 开始日期值,格式为 yyyyMMdd + /// @param endDateValue 结束日期值,格式为 yyyyMMdd + /// @return 营业日列表 + java.util.List listByDateRange(String storeId, Integer beginDateValue, Integer endDateValue); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreChartService.java new file mode 100644 index 0000000..da556d5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreChartService.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq; + +import java.util.List; + +/// 门店报表服务 +/// +/// @author Codex +/// @since 2026-08-11 +public interface GaoStoreChartService { + + /// 查询客户每日趋势 + List customerDailyTrend(GaoStoreChartReq req); + + /// 查询客户性别统计 + List customerGender(String storeId); + + /// 查询登记事件每日统计 + List eventRecordDailyStat(GaoStoreChartReq req); + + /// 查询登记事件日历记录 + List eventRecordCalendar(GaoStoreChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java new file mode 100644 index 0000000..43f4fe0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.spring.bean.Page; +import com.imyeyu.spring.service.BaseService; + +import java.io.IOException; +import java.util.List; + +/// +/// GAO 门店服务 +/// +/// @author Codex +/// @since 2026-08-06 +public interface GaoStoreService extends BaseService { + + /// 查询用户所属门店 + /// + /// @param userId 用户 ID + /// @return 门店 + GaoStore getByUserId(String userId); + + String getBelongIdByRequiredLoginUserId(); + + /// 导出门店列表 + /// + /// @param page 分页查询条件 + /// @return xlsx 文件字节 + byte[] export(Page page) throws IOException; + + /// 按客户端提交的顺序更新排序 + /// + /// @param idList 门店 ID 列表 + void sort(List idList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserService.java index a7d3efd..b3de23b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserService.java @@ -1,8 +1,6 @@ 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; /// @@ -12,15 +10,8 @@ import com.imyeyu.spring.service.BaseService; /// @since 2026-07-30 public interface GaoUserService extends BaseService { - /// 按用户 ID 查询 GAO 用户 + /// 调整 GAO 用户所属门店 /// - /// @param userId 用户 ID - /// @return GAO 用户 - GaoUser getByUserId(String userId); - - /// 分页查询并填充用户详情 - /// - /// @param page 分页请求 - /// @return GAO 用户分页 - PageResult pageWithUser(Page page); + /// @param gaoUser GAO 用户,必须包含 ID 和目标门店 ID + void transferStore(GaoUser gaoUser); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java new file mode 100644 index 0000000..e2e1d32 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerChartService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// 客户报表服务实现 +/// +/// @author Codex +/// @since 2026-08-11 +@Service +@RequiredArgsConstructor +public class GaoCustomerChartServiceImplement implements GaoCustomerChartService { + + private final GaoEventRecordMapper eventRecordMapper; + + @Override + public List eventRecordDailyStat(GaoCustomerChartReq req) { + checkRangeReq(req); + return eventRecordMapper.stateCustomerDailyByRange(req); + } + + @Override + public List eventRecordCalendar(GaoCustomerChartReq req) { + checkRangeReq(req); + return eventRecordMapper.selectCustomerCalendarByRange(req); + } + + private void checkRangeReq(GaoCustomerChartReq req) { + TimiException.required(req, "not found req"); + TimiException.required(req.getStoreId(), "not found req.storeId"); + TimiException.required(req.getCustomerId(), "not found req.customerId"); + TimiException.required(req.getBeginAt(), "not found req.beginAt"); + TimiException.required(req.getEndAt(), "not found req.endAt"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "req.endAt lt req.beginAt"); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java index 471526e..8b258ee 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java @@ -4,54 +4,41 @@ import com.fasterxml.jackson.databind.ObjectMapper; 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.util.CustomerXSSFBuilder; +import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView; import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; -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.GaoCustomerTrendStateReq; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; 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.TimiCode; import com.imyeyu.java.bean.timi.TimiException; -import com.imyeyu.spring.TimiSpring; 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 lombok.extern.slf4j.Slf4j; -import org.apache.poi.ss.usermodel.CellStyle; -import org.apache.poi.ss.usermodel.Font; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Instant; import java.time.LocalDate; -import java.time.Period; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; -/// /// 客户服务实现 /// /// @author Codex @@ -62,41 +49,62 @@ import java.util.stream.Collectors; public class GaoCustomerServiceImplement extends AbstractEntityService implements GaoCustomerService { private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); - private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); - private final GaoCustomerMapper mapper; private final ObjectMapper jackson; - private final AttachmentService attachmentService; + private final LoggerService loggerService; private final UserLoginService userLoginService; - private final UserService userService; + private final AttachmentService attachmentService; + + private final GaoCustomerMapper mapper; + + private final CustomerXSSFBuilder customerXSSFBuilder; @Override protected BaseMapper mapper() { return mapper; } + @Transactional(TimiServerDBConfig.ROLLBACKER) @Override - public PageResult page(Page page) { - PageResult result = super.page(page); - loadListTransient(result.getList()); - return result; - } + public void create(GaoCustomer customer) { + TimiException.required(customer.getName(), "not found customer.name"); + TimiException.required(customer.getStoreId(), "not found customer.storeId"); - @Override - public void create(GaoCustomer entity) { Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_CREATE"); try { - logger.setContent(jackson.writeValueAsString(entity)); - checkEntity(entity, false); - entity.setCreatorUserId(userLoginService.getRequireLoginUserId()); - entity.setInvitedCount(0L); - super.create(entity); - saveAttachment(entity.getId(), resolveAttachmentList(entity)); - changeInvitedCount(entity.getIntroducerCustomerId(), 1L); - loadDetailTransient(entity); + logger.setContent(jackson.writeValueAsString(customer)); + { + GaoCustomer byName = getByName(customer.getStoreId(), customer.getName()); + TimiException.requiredNull(byName, "该姓名客户已登记"); + } + if (TimiJava.isNotEmpty(customer.getCode())) { + GaoCustomer byCode = getByCode(customer.getStoreId(), customer.getCode()); + TimiException.requiredNull(byCode, "该编码客户已登记"); + } + if (TimiJava.isNotEmpty(customer.getIntroducerCustomerId())) { + TimiException.requiredTrue(!customer.getIntroducerCustomerId().equals(customer.getId()), "customer.introducerCustomerId invalid"); + GaoCustomer introducerCustomer = super.get(customer.getIntroducerCustomerId()); + TimiException.required(introducerCustomer, "not found introducer customer"); + TimiException.requiredTrue(customer.getStoreId().equals(introducerCustomer.getStoreId()), "介绍人和客户不属于同一门店"); + } + + customer.setBirthdateCalendar(TimiJava.defaultIfNull(customer.getBirthdateCalendar(), GaoCustomer.BirthdateCalendar.SOLAR)); + if (TimiJava.isNotEmpty(customer.getBirthdate())) { + customer.setAge(ChronoUnit.YEARS.between(customer.getBirthdate(), LocalDate.now())); + } + customer.setInvitedCount(0L); + if (customer.hasIntroducer()) { + customer.setInvitedCount(1L); + } + customer.setCreatorUserId(userLoginService.getRequireLoginUserId()); + + super.create(customer); + // 附件 + attachmentService.updateByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId(), customer.getAttachmentList()); + logger.setLevel(Logger.Level.INFO); - logger.setResult(entity.getId()); + logger.setResult(customer.getId()); } catch (TimiException e) { logger.setLevel(Logger.Level.WARN); logger.setException(e.getMessage()); @@ -111,24 +119,46 @@ public class GaoCustomerServiceImplement extends AbstractEntityService search(GaoCustomerSearchReq req) { - req = resolveSearchReq(req); - List list = mapper.search(req.getKeyword(), req.getIntroducerCustomerId()); - loadListTransient(list); - return list; + public GaoCustomer getByCode(String storeId, String code) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(code, "not found code"); + GaoCustomer example = new GaoCustomer(); + example.setStoreId(storeId); + example.setCode(code); + return mapper.selectByExample(example); } @Override - public PageResult searchPage(Page page) { - TimiException.required(page, "not found page"); - TimiException.required(page.getIndex(), "not found page.index"); - TimiException.required(page.getSize(), "not found page.size"); - TimiException.requiredTrue(0 <= page.getIndex(), "page.index lt 0"); - TimiException.requiredTrue(0 < page.getSize(), "page.size lte 0"); - GaoCustomerSearchReq req = resolveSearchReq(page.getEqualsExample()); - Long total = TimiJava.defaultIfNull(mapper.countSearch(req.getKeyword(), req.getIntroducerCustomerId()), 0L); - List list = mapper.searchPage(req.getKeyword(), req.getIntroducerCustomerId(), page.getIndex() * page.getSize(), page.getSize()); - loadListTransient(list); - PageResult result = new PageResult<>(); - result.setTotal(total); - result.setList(list); - return result; + public GaoCustomer getByName(String storeId, String name) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(name, "not found name"); + GaoCustomer example = new GaoCustomer(); + example.setStoreId(storeId); + example.setName(name); + return mapper.selectByExample(example); } @Override - public byte[] exportSearchExcel(GaoCustomerSearchReq req) throws IOException { - List list = search(req); - loadIntroducerCustomer(list); - try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { - Sheet sheet = workbook.createSheet("客户列表"); - CellStyle titleStyle = workbook.createCellStyle(); - Font titleFont = workbook.createFont(); - titleFont.setBold(true); - titleFont.setFontHeightInPoints((short) 14); - titleStyle.setFont(titleFont); - CellStyle headerStyle = workbook.createCellStyle(); - Font headerFont = workbook.createFont(); - headerFont.setBold(true); - headerStyle.setFont(headerFont); + public List listByIdList(Collection idList) { + return mapper.selectByIdList(new HashSet<>(idList)); + } - appendRow(sheet, 0, titleStyle, "客户列表导出"); - appendRow(sheet, 1, null, "生成时间", EXPORT_TIME_FORMATTER.format(Instant.now())); - appendRow(sheet, 2, null, "关键词", req == null || req.getKeyword() == null || req.getKeyword().isBlank() ? "全部" : req.getKeyword().trim()); - appendRow(sheet, 3, null, "记录总数", String.valueOf(list.size())); - appendRow(sheet, 5, headerStyle, "客户编码", "姓名", "性别", "出生日期(公历)", "出生日期历法", "年龄", "电话", "地址", "病症", "备注", "邀请人", "邀请客户数", "创建时间"); - int rowIndex = 6; + @Override + public byte[] export(Page page) throws IOException { + page.setIndex(0); + page.setSize(Long.MAX_VALUE); + List list = mapper.selectByPage(page); + { + List idList = list.stream().map(GaoCustomer::getIntroducerCustomerId).filter(TimiJava::isNotEmpty).distinct().toList(); + Map map = mapByIdList(idList); for (GaoCustomer customer : list) { - appendRow(sheet, rowIndex, null, - text(customer.getCode()), - text(customer.getName()), - formatGender(customer), - text(customer.getBirthdate()), - formatBirthdateCalendar(customer), - customer.getAge() == null ? "" : String.valueOf(customer.getAge()), - text(customer.getTelephone()), - text(customer.getAddress()), - text(customer.getDisease()), - text(customer.getRemark()), - customer.getIntroducerCustomer() == null ? "" : text(customer.getIntroducerCustomer().getName()), - customer.getInvitedCount() == null ? "0" : String.valueOf(customer.getInvitedCount()), - formatExportTime(customer.getCreatedAt()) - ); - rowIndex++; + customer.setIntroducerCustomer(map.get(customer.getIntroducerCustomerId())); } - for (int index = 0; index < 13; index++) { - sheet.autoSizeColumn(index); - sheet.setColumnWidth(index, Math.min(Math.max(sheet.getColumnWidth(index), 10 * 256), 36 * 256)); - } - workbook.write(output); - return output.toByteArray(); } + return customerXSSFBuilder.build(list); } @Override public List listInvited(String introducerCustomerId) { TimiException.required(introducerCustomerId, "not found introducerCustomerId"); - List list = mapper.selectInvitedList(introducerCustomerId); - loadListTransient(list); - return list; + GaoCustomer example = new GaoCustomer(); + example.setIntroducerCustomerId(introducerCustomerId); + return mapper.selectAllByExample(example); } @Transactional(TimiServerDBConfig.ROLLBACKER) @Override - public void saveInviteRelation(GaoCustomerInviteUpdateReq req) { + public void saveInviteRelation(String introducerCustomerId, String customerId) { + TimiException.required(customerId, "not found customerId"); + TimiException.required(introducerCustomerId, "not found introducerCustomerId"); + Logger logger = new Logger(Logger.Module.GAO, "GAO_CUSTOMER_SAVE_INVITE_RELATION"); try { - logger.setContent(jackson.writeValueAsString(req)); - TimiException.required(req, "not found invite req"); - TimiException.required(req.getCustomerId(), "not found invite req.customerId"); - GaoCustomer customer = super.get(req.getCustomerId()); - String oldIntroducerCustomerId = customer.getIntroducerCustomerId(); - customer.setIntroducerCustomerId(req.getIntroducerCustomerId()); - checkEntity(customer, true); - super.update(customer); - syncIntroducerInvitedCount(oldIntroducerCustomerId, customer.getIntroducerCustomerId()); + logger.setContent("introducerCustomerId: %s, customerId: %s".formatted(introducerCustomerId, customerId)); + GaoCustomer customer = get(customerId); + TimiException.required(customer, "not found customer"); + GaoCustomer introducerCustomer = get(introducerCustomerId); + TimiException.required(introducerCustomer, "not found introducerCustomer"); + TimiException.requiredTrue(customer.getStoreId().equals(introducerCustomer.getStoreId()), "介绍人和客户不属于同一门店"); + if (introducerCustomer.getIntroducerCustomerId().equals(customer.getIntroducerCustomerId())) { + throw new TimiException(TimiCode.ARG_BAD, "customer can not upper be introducer customer"); + } + // 存在旧的邀请来源 + if (TimiJava.isNotEmpty(customer.getIntroducerCustomerId())) { + if (customer.getIntroducerCustomerId().equals(introducerCustomerId)) { + return; + } + changeInvitedCount(customer.getIntroducerCustomerId(), -1); + } + customer.setIntroducerCustomerId(introducerCustomerId); + mapper.update(customer); + changeInvitedCount(introducerCustomerId, +1); logger.setLevel(Logger.Level.INFO); - logger.setResult(req.getCustomerId()); } catch (TimiException e) { logger.setLevel(Logger.Level.WARN); logger.setException(e.getMessage()); @@ -321,15 +298,16 @@ public class GaoCustomerServiceImplement extends AbstractEntityService statDailyTrend(GaoCustomerTrendStatReq req) { + public List stateDailyTrend(GaoCustomerTrendStateReq req) { TimiException.required(req, "not found req"); + TimiException.required(req.getStoreId(), "not found req.storeId"); 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 days = ChronoUnit.DAYS.between(beginDate, endDate) + 1; + TimiException.requiredTrue(days <= 366, "daily stat range too large"); + long beginCreatedAt = beginDate.atStartOfDay(zoneId).toInstant().toEpochMilli(); long endCreatedAt = endDate.plusDays(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1; Map newCustomerCountMap = new HashMap<>(); - for (GaoCustomerDailyStatView view : mapper.statDailyNewCustomer(beginCreatedAt, endCreatedAt)) { + for (GaoCustomerDailyStateView view : mapper.stateDailyNewCustomer(req.getStoreId(), beginCreatedAt, endCreatedAt)) { newCustomerCountMap.put(view.getDateValue(), TimiJava.defaultIfNull(view.getNewCustomerCount(), 0L)); } - long totalCustomerCount = TimiJava.defaultIfNull(mapper.countCreatedBefore(beginCreatedAt), 0L); - List list = new ArrayList<>(); + long totalCustomerCount = TimiJava.defaultIfNull(mapper.countCreatedBefore(req.getStoreId(), beginCreatedAt), 0L); + List 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(); + GaoCustomerDailyStateView view = new GaoCustomerDailyStateView(); view.setDateValue(dateValue); view.setNewCustomerCount(newCustomerCount); view.setTotalCustomerCount(totalCustomerCount); @@ -378,219 +360,17 @@ public class GaoCustomerServiceImplement extends AbstractEntityService statGender() { - return mapper.statGender(); - } - - 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 GaoCustomerSearchReq resolveSearchReq(GaoCustomerSearchReq req) { - return req == null ? new GaoCustomerSearchReq() : req; - } - - private GaoCustomer checkEntity(GaoCustomer entity, boolean requireId) { - TimiException.required(entity, "not found customer"); - GaoCustomer dbCustomer = null; - entity.setCode(normalizeText(entity.getCode())); - entity.setIntroducerCustomerId(normalizeText(entity.getIntroducerCustomerId())); - if (entity.getBirthdateCalendar() == null) { - entity.setBirthdateCalendar(GaoCustomer.BirthdateCalendar.SOLAR); - } - refreshAge(entity); - if (requireId) { - TimiException.required(entity.getId(), "not found customer.id"); - dbCustomer = super.get(entity.getId()); - entity.setCreatorUserId(dbCustomer.getCreatorUserId()); - } - TimiException.required(entity.getName(), "not found customer.name"); - entity.setName(entity.getName().trim()); - TimiException.requiredTrue(!entity.getName().isEmpty(), "not found customer.name"); - GaoCustomer duplicateCustomer = mapper.selectByName(entity.getName()); - if (duplicateCustomer != null && !duplicateCustomer.getId().equals(entity.getId())) { - throw new TimiException(TimiCode.ARG_BAD, "客户姓名已存在"); - } - if (TimiJava.isNotEmpty(entity.getCode())) { - duplicateCustomer = mapper.selectByCode(entity.getCode()); - if (duplicateCustomer != null && !duplicateCustomer.getId().equals(entity.getId())) { - throw new TimiException(TimiCode.ARG_BAD, "客户编码已存在"); - } - } - 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"); - } - return dbCustomer; - } - - private void loadDetailTransient(GaoCustomer customer) { - if (customer == null) { - return; - } - refreshAge(customer); - loadIntroducerCustomer(List.of(customer)); - if (TimiJava.isNotEmpty(customer.getCreatorUserId())) { - customer.setCreatorUser(userService.get(customer.getCreatorUserId())); - } - List detailCustomerList = new ArrayList<>(); - detailCustomerList.add(customer); - if (customer.getIntroducerCustomer() != null) { - detailCustomerList.add(customer.getIntroducerCustomer()); - } - loadAttachmentList(detailCustomerList); - } - - private void loadListTransient(List customerList) { - if (customerList != null) { - customerList.forEach(this::refreshAge); - } - loadAttachmentList(customerList); - } - - /// 根据出生日期计算当前周岁。出生日期为空时保留旧数据中的手工年龄。 - private void refreshAge(GaoCustomer customer) { - if (customer.getBirthdate() == null) { - return; - } - LocalDate today = LocalDate.now(); - TimiException.requiredTrue(!customer.getBirthdate().isAfter(today), "customer.birthdate invalid"); - customer.setAge(Period.between(customer.getBirthdate(), today).getYears()); - } - - private void loadIntroducerCustomer(List customerList) { - if (customerList == null || customerList.isEmpty()) { - return; - } - List introducerCustomerIdList = customerList.stream() - .map(GaoCustomer::getIntroducerCustomerId) - .filter(TimiJava::isNotEmpty) - .distinct() - .toList(); - if (introducerCustomerIdList.isEmpty()) { - return; - } - Map introducerCustomerMap = mapper.selectByIdList(new ArrayList<>(introducerCustomerIdList)).stream() - .collect(Collectors.toMap(GaoCustomer::getId, item -> item, (left, right) -> left)); - for (GaoCustomer customer : customerList) { - customer.setIntroducerCustomer(introducerCustomerMap.get(customer.getIntroducerCustomerId())); - } - } - - private void loadAttachmentList(List customerList) { - if (customerList == null || customerList.isEmpty()) { - return; - } - List customerIdList = customerList.stream() - .map(GaoCustomer::getId) - .filter(TimiJava::isNotEmpty) - .distinct() - .toList(); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name()); - for (GaoCustomer customer : customerList) { - customer.setAttachmentList(attachmentMap.getOrDefault(customer.getId(), List.of())); - } - } - - private void syncIntroducerInvitedCount(String oldIntroducerCustomerId, String newIntroducerCustomerId) { - String normalizedOldIntroducerCustomerId = normalizeText(oldIntroducerCustomerId); - String normalizedNewIntroducerCustomerId = normalizeText(newIntroducerCustomerId); - if (Objects.equals(normalizedOldIntroducerCustomerId, normalizedNewIntroducerCustomerId)) { - return; - } - changeInvitedCount(normalizedOldIntroducerCustomerId, -1L); - changeInvitedCount(normalizedNewIntroducerCustomerId, 1L); + public List stateGender(String storeId) { + TimiException.required(storeId, "not found storeId"); + return mapper.stateGender(storeId); } private void changeInvitedCount(String customerId, long delta) { - String normalizedCustomerId = normalizeText(customerId); - if (normalizedCustomerId == null || delta == 0) { + if (delta == 0) { return; } - mapper.updateInvitedCountDelta(normalizedCustomerId, delta); - } - - private String normalizeText(String value) { - if (value == null) { - return null; - } - String trimmedValue = value.trim(); - return trimmedValue.isEmpty() ? null : trimmedValue; - } - - private void saveAttachment(String customerId, List 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 resolveAttachmentList(GaoCustomer customer) { - if (customer.getAttachmentIdList() != null) { - return buildPhotoAttachmentList(customer.getAttachmentIdList()); - } - return customer.getAttachmentList(); - } - - private List buildPhotoAttachmentList(List attachmentIdList) { - List 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; - } - - private void appendRow(Sheet sheet, int rowIndex, CellStyle style, String... values) { - Row row = sheet.createRow(rowIndex); - for (int index = 0; index < values.length; index++) { - row.createCell(index).setCellValue(values[index]); - if (style != null) { - row.getCell(index).setCellStyle(style); - } - } - } - - private String formatGender(GaoCustomer customer) { - if (customer.getGender() == null) { - return ""; - } - return switch (customer.getGender()) { - case MALE -> "男"; - case FEMALE -> "女"; - }; - } - - private String formatBirthdateCalendar(GaoCustomer customer) { - if (customer.getBirthdateCalendar() == null) { - return "公历"; - } - return customer.getBirthdateCalendar() == GaoCustomer.BirthdateCalendar.LUNAR ? "农历" : "公历"; - } - - private String formatExportTime(Long value) { - if (value == null) { - return ""; - } - return EXPORT_TIME_FORMATTER.format(Instant.ofEpochMilli(value)); - } - - private String text(Object value) { - return value == null ? "" : value.toString(); + GaoCustomer customer = get(customerId); + customer.setInvitedCount(customer.getInvitedCount() + delta); + mapper.update(customer); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java new file mode 100644 index 0000000..6202982 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java @@ -0,0 +1,349 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.imyeyu.api.bean.ModuleCode; +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.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; +import com.imyeyu.api.modules.gao.service.GaoEventService; +import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; +import com.imyeyu.api.modules.gao.util.EventRecordXSSFBuilder; +import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.List; + +/// 登记事件记录服务实现 +/// +/// @author 夜雨 +/// @since 2026-07-27 14:11 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoEventRecordServiceImplement extends AbstractEntityService implements GaoEventRecordService { + + private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + private final ObjectMapper jackson; + private final RoleChecker roleChecker; + private final LoggerService loggerService; + private final GaoEventService eventService; + private final UserLoginService userLoginService; + private final AttachmentService attachmentService; + private final GaoCustomerService customerService; + private final GaoStoreBusinessDayService businessDayService; + private final GaoPointTriggerService pointTriggerService; + + private final GaoEventRecordMapper mapper; + + private final EventRecordXSSFBuilder xssfBuilder; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoEventRecord record) { + TimiException.required(record, "not found record"); + + Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_BATCH_CREATE"); + try { + logger.setContent(jackson.writeValueAsString(record)); + + String loginUserId = userLoginService.getRequireLoginUserId(); + Long registeredAt = TimiJava.defaultIfNull(record.getRegisteredAt(), Time.now()); + GaoEvent event = eventService.get(record.getEventId()); + TimiException.required(event.getStoreId(), "not found event.storeId"); + // 检查权限 + checkEventPermission(event); + int registeredDateValue = dateValue(registeredAt); + long businessDayNo = businessDayService.resolveOpenBusinessDay(event.getStoreId(), registeredDateValue).getBusinessDayNo(); + + List customerIdList = record.getCustomerIdList(); + if (TimiJava.isEmpty(customerIdList)) { + customerIdList = List.of(record.getCustomerId()); + } + TimiException.required(customerIdList, "not found customerId or empty customerIdList"); + + GaoEventRecord firstRecord = null; + List logIdList = new ArrayList<>(); + for (int i = 0; i < customerIdList.size(); i++) { + String customerId = customerIdList.get(i); + // 检查事件可登记 + checkEventCreatable(customerId, event, registeredAt); + GaoCustomer customer = customerService.get(customerId); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(event.getStoreId().equals(customer.getStoreId()), "客户和登记事件不属于同一门店"); + + GaoEventRecord dbRecord = new GaoEventRecord(); + dbRecord.setCustomerId(customerId); + dbRecord.setStoreId(event.getStoreId()); + dbRecord.setEventId(record.getEventId()); + dbRecord.setOperatorUserId(loginUserId); + dbRecord.setRemark(record.getRemark()); + dbRecord.setRegisteredAt(registeredAt); + dbRecord.setRegisteredDateValue(registeredDateValue); + dbRecord.setBusinessDayNo(businessDayNo); + dbRecord.setPointValue(record.getPointValue()); + super.create(dbRecord); + // 附件 + if (i == 0) { + // 第一个记录储存附件 + firstRecord = dbRecord; + attachmentService.updateByBizId(Attachment.BizType.GAO_EVENT_RECORD, dbRecord.getId(), record.getAttachmentList()); + } else { + // 其他记录克隆第一个记录附件 + List attachmentList = attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, firstRecord.getId()); + for (Attachment attachment : attachmentList) { + Attachment clone = attachmentService.clone(attachment.getId()); + clone.setId(dbRecord.getId()); + attachmentService.update(clone); + } + } + pointTriggerService.onEventRecordCreated(dbRecord); + logIdList.add(dbRecord.getId()); + } + logger.setLevel(Logger.Level.INFO); + logger.setResult(String.join(",", logIdList)); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("create event record error", e); + throw new TimiException(TimiCode.ERROR, "create event record error", e); + } finally { + loggerService.create(logger); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void createBatch(List recordList) { + for (GaoEventRecord record : TimiJava.safeIterable(recordList)) { + create(record); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoEventRecord record) { + TimiException.required(record, "not found record"); + TimiException.required(record.getId(), "not found record.id"); + + Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_UPDATE"); + try { + logger.setContent(jackson.writeValueAsString(record)); + TimiException.required(record.getCustomerId(), "not found record.customerId"); + TimiException.required(record.getEventId(), "not found record.eventId"); + + record.setRegisteredAt(TimiJava.defaultIfNull(record.getRegisteredAt(), Time.now())); + + GaoEvent event = eventService.get(record.getEventId()); + TimiException.required(event.getStoreId(), "not found event.storeId"); + checkEventPermission(event); + checkEventCreatable(record.getCustomerId(), event, record.getRegisteredAt()); + GaoCustomer customer = customerService.get(record.getCustomerId()); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(event.getStoreId().equals(customer.getStoreId()), "客户和登记事件不属于同一门店"); + int registeredDateValue = dateValue(record.getRegisteredAt()); + record.setStoreId(event.getStoreId()); + record.setRegisteredDateValue(registeredDateValue); + record.setBusinessDayNo(businessDayService.resolveOpenBusinessDay(event.getStoreId(), registeredDateValue).getBusinessDayNo()); + + super.update(record); + attachmentService.updateByBizId(Attachment.BizType.GAO_EVENT_RECORD, record.getId(), record.getAttachmentList()); + logger.setLevel(Logger.Level.INFO); + logger.setResult(record.getId()); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("修改 GAO 登记记录失败", e); + throw new TimiException(TimiCode.ERROR, "修改 GAO 登记记录失败", e); + } finally { + loggerService.create(logger); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(String id) { + TimiException.required(id, "not found record.id"); + Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_DELETE"); + try { + logger.setContent(id); + GaoEventRecord record = get(id); + TimiException.required(record, "not found event record"); + pointTriggerService.revokeByEventRecord(record); + super.delete(id); + logger.setLevel(Logger.Level.INFO); + logger.setResult(id); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("删除 GAO 登记记录失败", e); + throw new TimiException(TimiCode.ERROR, "删除 GAO 登记记录失败", e); + } finally { + loggerService.create(logger); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void createWithCustomer(GaoCustomer customer) { + TimiException.required(customer, "not found customer"); + if (TimiJava.isNotEmpty(customer.getId())) { + // 有 ID 表示给已有客户绑定新编码;编码不能被其他客户占用 + GaoCustomer dbCustomer = customerService.get(customer.getId()); + TimiException.required(dbCustomer, "not found customer"); + if (TimiJava.isNotEmpty(customer.getCode())) { + GaoCustomer byCode = customerService.getByCode(dbCustomer.getStoreId(), customer.getCode()); + TimiException.requiredTrue(byCode == null || dbCustomer.getId().equals(byCode.getId()), "该编码客户已登记"); + dbCustomer.setCode(customer.getCode()); + customerService.update(dbCustomer); + } + customer.setId(dbCustomer.getId()); + } else { + // 无 ID 表示新开客户档案;创建客户只强制姓名唯一,其余字段按请求透传 + customerService.create(customer); + } + if (TimiJava.isEmpty(customer.getEventRecordList())) { + return; + } + for (GaoEventRecord record : TimiJava.safeIterable(customer.getEventRecordList())) { + record.setCustomerId(customer.getId()); + } + createBatch(customer.getEventRecordList()); + } + + @Override + public PageResult pageByRange(GaoEventRecordPage page) { + TimiException.required(page.getStoreId(), "not found page.storeId"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByRange(page)); + result.setList(mapper.selectByRange(page)); + return result; + } + + @Override + public byte[] export(GaoEventRecordPage page) throws IOException { + TimiException.required(page.getStoreId(), "not found page.storeId"); + List list = mapper.selectByRange(page); + return xssfBuilder.build(list); + } + + @Override + public PageResult pageRankByRange(GaoEventRecordRankPage page) { + TimiException.required(page.getStoreId(), "not found page.storeId"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countRankByRange(page)); + result.setList(mapper.selectRankByRange(page)); + return result; + } + + private void checkEventPermission(GaoEvent event) { + List roleCodeList = eventService.listRoleCode(event.getId()); + TimiException.requiredTrue(roleCodeList != null && !roleCodeList.isEmpty(), "登记事件未配置可登记角色"); + roleChecker.checkAny(ModuleCode.GAO, roleCodeList.toArray(String[]::new)); + } + + private void checkEventCreatable(String customerId, GaoEvent event, long registeredAt) { + TimiException.requiredTrue(event.getStatus() == null || event.getStatus() == GaoEvent.Status.ACTIVE, "登记事件不可登记"); + TimiException.requiredTrue(event.getBeginAt() == null || event.getBeginAt() <= registeredAt, "登记事件未到可登记时间"); + TimiException.requiredTrue(event.getEndAt() == null || registeredAt <= event.getEndAt(), "登记事件已过可登记时间"); + GaoEvent.LimitType limitType = TimiJava.defaultIfNull(event.getLimitType(), GaoEvent.LimitType.NONE); + switch (limitType) { + case DAILY_HALF_DAY -> { + long today = Time.today(registeredAt); + long noon = today + Time.D / 2; + long beginAt = today, endAt; + if (today + registeredAt < noon) { + // 上半日 + endAt = noon; + } else { + // 下半日 + beginAt = noon; + endAt = today + Time.D; + } + Long count = mapper.count(event.getStoreId(), customerId, event.getId(), beginAt, endAt); + TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, "该客户当前半日已登记该事件"); + } + case NATURAL_DAY -> { + long beginAt = Time.today(registeredAt); + long endAt = beginAt + Time.D; + Long count = mapper.count(event.getStoreId(), customerId, event.getId(), beginAt, endAt); + TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, "该客户当前半日已登记该事件"); + } + case INTERVAL_HOUR -> { + long beginAt = registeredAt - event.getLimitValue() * Time.H; + Long count = mapper.count(event.getStoreId(), customerId, event.getId(), beginAt, registeredAt); + TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, "该客户登记该事件间隔不足"); + } + case NATURAL_MONTH -> { + ZoneId zone = ZoneId.systemDefault(); + ZonedDateTime zdt = Instant.ofEpochMilli(registeredAt).atZone(zone); + long beginAt = zdt.with(TemporalAdjusters.firstDayOfMonth()).truncatedTo(ChronoUnit.DAYS).toInstant().toEpochMilli(); + long endAt = zdt.with(TemporalAdjusters.firstDayOfNextMonth()).truncatedTo(ChronoUnit.DAYS).toInstant().toEpochMilli(); + Long count = mapper.count(event.getStoreId(), customerId, event.getId(), beginAt, endAt); + TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, "该客户当月已登记该事件"); + } + case TOTAL_LIMIT -> { + Long count = mapper.count(event.getStoreId(), null, event.getId(), null, null); + TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < event.getLimitValue(), "登记事件总次数已达上限"); + } + case CUSTOMER_TOTAL_LIMIT -> { + Long count = mapper.count(event.getStoreId(), customerId, event.getId(), null, null); + TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < event.getLimitValue(), "该客户登记该事件次数已达上限"); + } + } + } + + private int dateValue(long time) { + LocalDate date = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).toLocalDate(); + return Integer.parseInt(DAY_FORMATTER.format(date)); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java new file mode 100644 index 0000000..47a7e7a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java @@ -0,0 +1,224 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation; +import com.imyeyu.api.modules.gao.mapper.GaoEventMapper; +import com.imyeyu.api.modules.gao.mapper.GaoEventRoleRelationMapper; +import com.imyeyu.api.modules.gao.service.GaoEventService; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +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 com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// +/// 登记事件服务实现 +/// +/// @author 夜雨 +/// @since 2026-07-27 09:00 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoEventServiceImplement extends AbstractEntityService implements GaoEventService { + + private final ObjectMapper jackson; + + private final LoggerService loggerService; + + private final GaoEventMapper mapper; + private final GaoEventRoleRelationMapper roleRelationMapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoEvent event) { + TimiException.required(event, "not found event"); + TimiException.required(event.getStoreId(), "not found event.storeId"); + TimiException.required(event.getName(), "not found event.name"); + + Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_CREATE"); + try { + logger.setContent(jackson.writeValueAsString(event)); + + event.setStatus(TimiJava.defaultIfNull(event.getStatus(), GaoEvent.Status.DRAFT)); + event.setRequirePhoto(TimiJava.defaultIfNull(event.getRequirePhoto(), false)); + event.setLimitType(TimiJava.defaultIfNull(event.getLimitType(), GaoEvent.LimitType.NONE)); + event.setSort(TimiJava.defaultIfNull(event.getSort(), 0)); + verifyEvent(event); + super.create(event); + + saveRoleRelation(event.getId(), event.getRoleCodeList()); + logger.setLevel(Logger.Level.INFO); + logger.setResult(event.getId()); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("create event error", e); + throw new TimiException(TimiCode.ERROR, "create event error", e); + } finally { + loggerService.create(logger); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoEvent event) { + Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_UPDATE"); + try { + logger.setContent(jackson.writeValueAsString(event)); + GaoEvent dbEvent = get(event.getId()); + TimiException.requiredTrue(dbEvent.getStoreId().equals(event.getStoreId()), "event.storeId invalid"); + dbEvent.setName(event.getName()); + dbEvent.setDescription(event.getDescription()); + dbEvent.setColor(event.getColor()); + dbEvent.setStatus(event.getStatus()); + dbEvent.setRequirePhoto(event.getRequirePhoto()); + dbEvent.setLimitType(event.getLimitType()); + dbEvent.setLimitValue(event.getLimitValue()); + dbEvent.setBeginAt(event.getBeginAt()); + dbEvent.setEndAt(event.getEndAt()); + + verifyEvent(dbEvent); + mapper.update(dbEvent); + saveRoleRelation(event.getId(), event.getRoleCodeList()); + logger.setLevel(Logger.Level.INFO); + logger.setResult(event.getId()); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("update event error", e); + throw new TimiException(TimiCode.ERROR, "update event error", e); + } finally { + loggerService.create(logger); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(String id) { + Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_DELETE"); + try { + logger.setContent(id); + GaoEvent event = get(id); + event.setStatus(GaoEvent.Status.DELETED); + super.update(event); + logger.setLevel(Logger.Level.INFO); + logger.setResult(id); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("delete event error", e); + throw new TimiException(TimiCode.ERROR, "delete event error", e); + } finally { + loggerService.create(logger); + } + } + + @Override + public Map mapByIdList(Collection idList) { + return mapper.selectByIdList(new HashSet<>(idList)).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity())); + } + + @Override + public Map> mapRoleCodeListByEventIdList(Collection eventIdList) { + return roleRelationMapper.selectAllByEventIdList(new HashSet<>(eventIdList)) + .stream() + .collect(Collectors.groupingBy(GaoEventRoleRelation::getEventId, Collectors.mapping(GaoEventRoleRelation::getRoleCode, Collectors.toList()))); + } + + @Override + public List listValid(String storeId) { + TimiException.required(storeId, "not found storeId"); + return mapper.selectValidList(storeId, Time.now()); + } + + @Override + public List listRoleCode(String eventId) { + GaoEventRoleRelation example = new GaoEventRoleRelation(); + example.setEventId(eventId); + return roleRelationMapper.selectAllByExample(example).stream().map(GaoEventRoleRelation::getRoleCode).toList(); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void sort(List idList) { + if (TimiJava.isEmpty(idList)) { + return; + } + idList = new ArrayList<>(new LinkedHashSet<>(idList)); + Map eventMap = mapper.selectByIdList(idList).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity())); + for (int i = 0; i < idList.size(); i++) { + GaoEvent event = eventMap.get(idList.get(i)); + event.setSort(i); + mapper.update(event); + } + } + + private void verifyEvent(GaoEvent event) { + if (TimiJava.isNotEmpty(event.getLimitType()) && event.getLimitType() != GaoEvent.LimitType.NONE) { + TimiException.requiredTrue(event.getLimitType().getMin() < event.getLimitValue(), "约束值最低 %s".formatted(event.getLimitType().getMin())); + } + if (event.getBeginAt() != null || event.getEndAt() != null) { + TimiException.requiredTrue(event.getBeginAt() == null || event.getEndAt() == null || event.getBeginAt() <= event.getEndAt(), "invalid beginAt or endAt"); + } + } + + private void saveRoleRelation(String eventId, List roleCodeList) { + List dbRoleRelationList = roleRelationMapper.selectAllByEventIdList(List.of(eventId)); + Set roleCodeSet = TimiJava.defaultIfEmpty(roleCodeList, List.of()).stream().filter(TimiJava::isNotEmpty).collect(Collectors.toSet()); + Set retainedRoleCodeSet = new HashSet<>(); + for (GaoEventRoleRelation dbEventRole : dbRoleRelationList) { + String roleCode = dbEventRole.getRoleCode(); + if (roleCode != null) { + roleCode = roleCode.trim(); + } + if (!roleCodeSet.contains(roleCode) || !retainedRoleCodeSet.add(roleCode)) { + roleRelationMapper.delete(dbEventRole.getId()); + } + } + for (String roleCode : roleCodeSet) { + if (retainedRoleCodeSet.contains(roleCode)) { + continue; + } + GaoEventRoleRelation relation = new GaoEventRoleRelation(); + relation.setEventId(eventId); + relation.setRoleCode(roleCode); + roleRelationMapper.insert(relation); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java new file mode 100644 index 0000000..734dc9c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java @@ -0,0 +1,71 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoPointAccount; +import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper; +import com.imyeyu.api.modules.gao.service.GaoPointAccountService; +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.util.UUID; + +/// GAO 客户积分账户服务实现 +/// +/// @author Codex +/// @since 2026-08-11 +@Service +@RequiredArgsConstructor +public class GaoPointAccountServiceImplement extends AbstractEntityService implements GaoPointAccountService { + + private final GaoPointAccountMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public GaoPointAccount getByCustomerId(String storeId, String customerId) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(customerId, "not found customerId"); + return mapper.selectByStoreIdAndCustomerId(storeId, customerId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPointAccount getOrCreateForUpdate(String storeId, String customerId) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(customerId, "not found customerId"); + + GaoPointAccount account = mapper.selectByStoreIdAndCustomerId(storeId, customerId); + if (account == null) { + account = new GaoPointAccount(); + account.setId(UUID.randomUUID().toString()); + account.setStoreId(storeId); + account.setCustomerId(customerId); + account.setBalance(0L); + account.setTotalEarned(0L); + account.setTotalRevoked(0L); + account.setTotalAdjusted(0L); + account.setVersion(0L); + account.setCreatedAt(Time.now()); + mapper.insertIfAbsent(account); + account = mapper.selectByStoreIdAndCustomerId(storeId, customerId); + } + TimiException.required(account, "not found customer point account"); + GaoPointAccount lockedAccount = mapper.selectForUpdate(account.getId()); + TimiException.required(lockedAccount, "not found customer point account"); + return lockedAccount; + } + + @Override + public int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta) { + TimiException.required(accountId, "not found accountId"); + return mapper.updateBalance(accountId, delta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta, Time.now()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java new file mode 100644 index 0000000..8b55c00 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java @@ -0,0 +1,142 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoPointAccount; +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper; +import com.imyeyu.api.modules.gao.service.GaoPointAccountService; +import com.imyeyu.api.modules.gao.service.GaoPointLedgerService; +import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest; +import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +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; + +/// GAO 客户积分流水服务实现 +/// +/// @author Codex +/// @since 2026-08-11 +@Service +@RequiredArgsConstructor +public class GaoPointLedgerServiceImplement extends AbstractEntityService implements GaoPointLedgerService { + + private final GaoPointLedgerMapper mapper; + private final GaoPointAccountService accountService; + private final UserLoginService userLoginService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public void update(GaoPointLedger ledger) { + throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止修改"); + } + + @Override + public void delete(String id) { + throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止删除"); + } + + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止物理删除"); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload) { + TimiException.required(record, "not found event record"); + TimiException.required(rule, "not found point rule"); + TimiException.requiredTrue(0 < pointDelta, "pointDelta invalid"); + String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId()); + return apply(record.getStoreId(), record.getCustomerId(), GaoPointLedger.ChangeType.EARN, pointDelta, rule.getId(), record.getEventId(), record.getId(), sourcePayload, idempotencyKey, rule.getRemark(), record.getOperatorUserId(), record.getRegisteredAt()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record) { + TimiException.required(originLedger, "not found origin ledger"); + TimiException.required(record, "not found event record"); + TimiException.requiredTrue(0 < originLedger.getPointDelta(), "origin ledger pointDelta invalid"); + String idempotencyKey = "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId()); + return apply(originLedger.getStoreId(), originLedger.getCustomerId(), GaoPointLedger.ChangeType.REVOKE, -originLedger.getPointDelta(), originLedger.getRuleId(), originLedger.getEventId(), originLedger.getEventRecordId(), originLedger.getSourcePayload(), idempotencyKey, "撤销登记记录积分", userLoginService.getRequireLoginUserId(), Time.now()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId) { + TimiException.required(request, "not found adjust request"); + TimiException.requiredTrue(TimiJava.isNotEmpty(request.getAdjustRequestId()), "not found adjustRequestId"); + TimiException.required(request.getStoreId(), "not found storeId"); + TimiException.required(request.getCustomerId(), "not found customerId"); + TimiException.required(request.getPointDelta(), "not found pointDelta"); + TimiException.requiredTrue(request.getPointDelta() != 0, "pointDelta invalid"); + TimiException.requiredTrue(TimiJava.isNotEmpty(request.getRemark()), "not found adjust remark"); + TimiException.required(operatorUserId, "not found operatorUserId"); + String idempotencyKey = "MANUAL_ADJUST:%s".formatted(request.getAdjustRequestId()); + return apply(request.getStoreId(), request.getCustomerId(), GaoPointLedger.ChangeType.ADJUST, request.getPointDelta(), null, null, null, null, idempotencyKey, request.getRemark(), operatorUserId, Time.now()); + } + + @Override + public PageResult pageByQuery(GaoPointLedgerPage page) { + TimiException.required(page, "not found page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + return result; + } + + private GaoPointLedger apply(String storeId, String customerId, GaoPointLedger.ChangeType changeType, long pointDelta, String ruleId, String eventId, String eventRecordId, JsonNode sourcePayload, String idempotencyKey, String remark, String operatorUserId, Long occurredAt) { + GaoPointLedger existing = mapper.selectByIdempotencyKey(idempotencyKey); + if (existing != null) { + return existing; + } + + GaoPointAccount account = accountService.getOrCreateForUpdate(storeId, customerId); + existing = mapper.selectByIdempotencyKey(idempotencyKey); + if (existing != null) { + return existing; + } + + long balanceBefore = account.getBalance(); + long balanceAfter = Math.addExact(balanceBefore, pointDelta); + long totalEarnedDelta = changeType == GaoPointLedger.ChangeType.EARN ? pointDelta : 0L; + long totalRevokedDelta = changeType == GaoPointLedger.ChangeType.REVOKE ? -pointDelta : 0L; + long totalAdjustedDelta = changeType == GaoPointLedger.ChangeType.ADJUST ? pointDelta : 0L; + + GaoPointLedger ledger = new GaoPointLedger(); + ledger.setAccountId(account.getId()); + ledger.setStoreId(storeId); + ledger.setCustomerId(customerId); + ledger.setChangeType(changeType); + ledger.setDirection(0 < pointDelta ? GaoPointLedger.Direction.INCREASE : GaoPointLedger.Direction.DECREASE); + ledger.setPointDelta(pointDelta); + ledger.setBalanceBefore(balanceBefore); + ledger.setBalanceAfter(balanceAfter); + ledger.setRuleId(ruleId); + ledger.setEventId(eventId); + ledger.setEventRecordId(eventRecordId); + ledger.setSourcePayload(sourcePayload); + ledger.setIdempotencyKey(idempotencyKey); + ledger.setRemark(remark); + ledger.setOperatorUserId(operatorUserId); + ledger.setOccurredAt(occurredAt); + create(ledger); + int updated = accountService.updateBalance(account.getId(), pointDelta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta); + TimiException.requiredTrue(0 < updated, "积分余额不足"); + return ledger; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRuleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRuleServiceImplement.java new file mode 100644 index 0000000..f8c93b6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRuleServiceImplement.java @@ -0,0 +1,154 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.mapper.GaoPointRuleMapper; +import com.imyeyu.api.modules.gao.service.GaoPointRuleService; +import com.imyeyu.api.modules.gao.service.GaoEventService; +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 lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/// GAO 客户积分规则服务实现 +/// +/// @author Codex +/// @since 2026-08-11 +@Service +@RequiredArgsConstructor +public class GaoPointRuleServiceImplement extends AbstractEntityService implements GaoPointRuleService { + + private final GaoPointRuleMapper mapper; + private final GaoEventService eventService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public List listActive(String storeId, String eventId, long now) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(eventId, "not found eventId"); + return mapper.selectActiveByStoreIdAndEventId(storeId, eventId, now); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoPointRule rule) { + TimiException.required(rule, "not found rule"); + TimiException.required(rule.getStoreId(), "not found rule.storeId"); + TimiException.required(rule.getEventId(), "not found rule.eventId"); + TimiException.required(rule.getName(), "not found rule.name"); + checkEvent(rule.getStoreId(), rule.getEventId()); + rule.setStatus(TimiJava.defaultIfNull(rule.getStatus(), GaoPointRule.Status.DRAFT)); + rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0)); + rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false)); + verifyRule(rule); + super.create(rule); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoPointRule rule) { + TimiException.required(rule, "not found rule"); + TimiException.required(rule.getId(), "not found rule.id"); + TimiException.required(rule.getStoreId(), "not found rule.storeId"); + TimiException.required(rule.getEventId(), "not found rule.eventId"); + TimiException.required(rule.getName(), "not found rule.name"); + GaoPointRule dbRule = get(rule.getId()); + TimiException.required(dbRule, "not found rule"); + TimiException.requiredTrue(dbRule.getStoreId().equals(rule.getStoreId()), "rule.storeId invalid"); + checkEvent(rule.getStoreId(), rule.getEventId()); + rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0)); + rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false)); + verifyRule(rule); + dbRule.setEventId(rule.getEventId()); + dbRule.setName(rule.getName()); + dbRule.setStatus(rule.getStatus()); + dbRule.setTriggerType(rule.getTriggerType()); + dbRule.setPointMode(rule.getPointMode()); + dbRule.setPointValue(rule.getPointValue()); + dbRule.setConditionJson(rule.getConditionJson()); + dbRule.setBeginAt(rule.getBeginAt()); + dbRule.setEndAt(rule.getEndAt()); + dbRule.setPriority(rule.getPriority()); + dbRule.setExclusive(rule.getExclusive()); + dbRule.setRemark(rule.getRemark()); + mapper.update(dbRule); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(String id) { + TimiException.required(id, "not found rule.id"); + GaoPointRule rule = get(id); + TimiException.required(rule, "not found rule"); + rule.setStatus(GaoPointRule.Status.DELETED); + super.update(rule); + super.delete(id); + } + + private void checkEvent(String storeId, String eventId) { + GaoEvent event = eventService.get(eventId); + TimiException.required(event, "not found event"); + TimiException.requiredTrue(storeId.equals(event.getStoreId()), "规则和登记事件不属于同一门店"); + } + + private void verifyRule(GaoPointRule rule) { + TimiException.required(rule.getStatus(), "not found rule.status"); + TimiException.required(rule.getTriggerType(), "not found rule.triggerType"); + TimiException.required(rule.getPointMode(), "not found rule.pointMode"); + TimiException.requiredTrue(rule.getPriority() != null && 0 <= rule.getPriority(), "rule.priority invalid"); + TimiException.requiredTrue(rule.getBeginAt() == null || rule.getEndAt() == null || rule.getBeginAt() <= rule.getEndAt(), "rule.beginAt or rule.endAt invalid"); + + if (rule.getTriggerType() == GaoPointRule.TriggerType.SOURCE_VALUE) { + TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.SOURCE_VALUE, "SOURCE_VALUE 规则必须使用 SOURCE_VALUE 积分值模式"); + TimiException.requiredNull(rule.getPointValue(), "SOURCE_VALUE 规则不能填写固定积分值"); + verifySourceCondition(rule.getConditionJson()); + return; + } + + TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.FIXED, "固定触发规则必须使用 FIXED 积分值模式"); + TimiException.required(rule.getPointValue(), "not found rule.pointValue"); + TimiException.requiredTrue(0 < rule.getPointValue(), "rule.pointValue invalid"); + if (rule.getTriggerType() == GaoPointRule.TriggerType.AFTER_COUNT_EVERY_TIME) { + JsonNode condition = requireObjectCondition(rule.getConditionJson()); + JsonNode threshold = condition.get("threshold"); + TimiException.requiredTrue(threshold != null && threshold.isIntegralNumber() && 0 < threshold.longValue(), "AFTER_COUNT_EVERY_TIME.threshold invalid"); + JsonNode includeCurrent = condition.get("includeCurrent"); + TimiException.requiredTrue(includeCurrent == null || includeCurrent.isNull() || includeCurrent.isBoolean(), "AFTER_COUNT_EVERY_TIME.includeCurrent invalid"); + verifyScope(condition); + } else if (rule.getTriggerType() == GaoPointRule.TriggerType.FIRST_TIME) { + verifyScope(requireObjectCondition(rule.getConditionJson())); + } + } + + private void verifySourceCondition(JsonNode conditionJson) { + JsonNode condition = conditionJson == null ? null : requireObjectCondition(conditionJson); + Long min = condition == null || condition.get("min") == null || condition.get("min").isNull() ? null : condition.get("min").isIntegralNumber() ? condition.get("min").longValue() : null; + Long max = condition == null || condition.get("max") == null || condition.get("max").isNull() ? null : condition.get("max").isIntegralNumber() ? condition.get("max").longValue() : null; + TimiException.requiredTrue(condition == null || min != null || condition.get("min") == null || condition.get("min").isNull(), "SOURCE_VALUE.min invalid"); + TimiException.requiredTrue(condition == null || max != null || condition.get("max") == null || condition.get("max").isNull(), "SOURCE_VALUE.max invalid"); + TimiException.requiredTrue(min == null || 0 < min, "SOURCE_VALUE.min invalid"); + TimiException.requiredTrue(max == null || 0 < max, "SOURCE_VALUE.max invalid"); + TimiException.requiredTrue(min == null || max == null || min <= max, "SOURCE_VALUE.min or SOURCE_VALUE.max invalid"); + } + + private JsonNode requireObjectCondition(JsonNode conditionJson) { + TimiException.requiredTrue(conditionJson != null && conditionJson.isObject(), "rule.conditionJson must be object"); + return conditionJson; + } + + private void verifyScope(JsonNode condition) { + JsonNode scope = condition.get("scope"); + TimiException.requiredTrue(scope == null || scope.isNull() || "CUSTOMER_EVENT".equals(scope.asText()), "rule.conditionJson.scope invalid"); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointTriggerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointTriggerServiceImplement.java new file mode 100644 index 0000000..fef6dea --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointTriggerServiceImplement.java @@ -0,0 +1,159 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper; +import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; +import com.imyeyu.api.modules.gao.service.GaoPointLedgerService; +import com.imyeyu.api.modules.gao.service.GaoPointRuleService; +import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest; +import com.imyeyu.api.modules.user.service.PermissionChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/// GAO 客户积分触发服务实现 +/// +/// @author Codex +/// @since 2026-08-11 +@Service +@RequiredArgsConstructor +public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { + + private final ObjectMapper jackson; + private final PermissionChecker permissionChecker; + private final UserLoginService userLoginService; + private final GaoPointRuleService ruleService; + private final GaoPointLedgerService ledgerService; + private final GaoCustomerService customerService; + private final GaoPointLedgerMapper ledgerMapper; + private final GaoEventRecordMapper eventRecordMapper; + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void onEventRecordCreated(GaoEventRecord record) { + TimiException.required(record, "not found event record"); + TimiException.required(record.getStoreId(), "not found record.storeId"); + TimiException.required(record.getCustomerId(), "not found record.customerId"); + TimiException.required(record.getEventId(), "not found record.eventId"); + + List ruleList = ruleService.listActive(record.getStoreId(), record.getEventId(), Time.now()); + Long count = null; + for (GaoPointRule rule : ruleList) { + Long pointDelta = resolvePointDelta(record, rule, count); + if (pointDelta == null) { + continue; + } + if (count == null) { + count = eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null); + } + JsonNode sourcePayload = sourcePayload(record, rule, pointDelta); + ledgerService.earn(record, rule, pointDelta, sourcePayload); + if (Boolean.TRUE.equals(rule.getExclusive())) { + break; + } + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void revokeByEventRecord(GaoEventRecord record) { + TimiException.required(record, "not found event record"); + List originLedgerList = ledgerMapper.selectEarnByEventRecordId(record.getId()); + if (originLedgerList.isEmpty()) { + return; + } + Set revokedKeySet = new HashSet<>(); + List revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList(); + for (GaoPointLedger ledger : ledgerMapper.selectByIdempotencyKeyList(revokeKeyList)) { + revokedKeySet.add(ledger.getIdempotencyKey()); + } + for (GaoPointLedger originLedger : originLedgerList) { + if (!revokedKeySet.contains(revokeKey(record, originLedger))) { + ledgerService.revoke(originLedger, record); + } + } + } + + @Override + public GaoPointLedger adjust(GaoPointAdjustRequest request) { + TimiException.required(request, "not found adjust request"); + TimiException.required(request.getStoreId(), "not found storeId"); + TimiException.required(request.getCustomerId(), "not found customerId"); + GaoCustomer customer = customerService.get(request.getCustomerId()); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(request.getStoreId().equals(customer.getStoreId()), "客户和积分调整门店不一致"); + return ledgerService.adjust(request, userLoginService.getRequireLoginUserId()); + } + + private Long resolvePointDelta(GaoEventRecord record, GaoPointRule rule, Long count) { + return switch (rule.getTriggerType()) { + case EVERY_TIME -> rule.getPointValue(); + case FIRST_TIME -> { + long currentCount = count == null ? count(record) : count; + yield currentCount == 1 ? rule.getPointValue() : null; + } + case AFTER_COUNT_EVERY_TIME -> { + JsonNode condition = rule.getConditionJson(); + long threshold = condition.get("threshold").longValue(); + boolean includeCurrent = condition.get("includeCurrent") == null || condition.get("includeCurrent").isNull() || condition.get("includeCurrent").asBoolean(); + long currentCount = count == null ? count(record) : count; + yield includeCurrent ? threshold <= currentCount ? rule.getPointValue() : null : threshold < currentCount ? rule.getPointValue() : null; + } + case SOURCE_VALUE -> resolveSourceValue(record, rule); + }; + } + + private Long resolveSourceValue(GaoEventRecord record, GaoPointRule rule) { + if (record.getPointValue() == null) { + return null; + } + permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.POINT_SOURCE_VALUE.getValue()); + long pointValue = record.getPointValue(); + TimiException.requiredTrue(0 < pointValue, "pointValue invalid"); + JsonNode condition = rule.getConditionJson(); + if (condition != null) { + JsonNode min = condition.get("min"); + JsonNode max = condition.get("max"); + TimiException.requiredTrue(min == null || min.isNull() || min.longValue() <= pointValue, "pointValue 小于规则最小值"); + TimiException.requiredTrue(max == null || max.isNull() || pointValue <= max.longValue(), "pointValue 大于规则最大值"); + } + return pointValue; + } + + private JsonNode sourcePayload(GaoEventRecord record, GaoPointRule rule, long pointDelta) { + if (rule.getTriggerType() != GaoPointRule.TriggerType.SOURCE_VALUE) { + return null; + } + ObjectNode payload = jackson.createObjectNode(); + payload.put("submittedPointValue", record.getPointValue()); + payload.put("validatedPointValue", pointDelta); + return payload; + } + + private long count(GaoEventRecord record) { + return TimiJava.defaultIfNull(eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null), 0L); + } + + private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) { + return "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId()); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoRegistrationEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoRegistrationEventRecordServiceImplement.java deleted file mode 100644 index 0a00508..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoRegistrationEventRecordServiceImplement.java +++ /dev/null @@ -1,761 +0,0 @@ -package com.imyeyu.api.modules.gao.service.implement; - -import com.fasterxml.jackson.databind.ObjectMapper; -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.GaoCustomerMapper; -import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventMapper; -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.GaoCustomerRegistrationDayStatView; -import com.imyeyu.api.modules.gao.vo.GaoCustomerRegistrationRankView; -import com.imyeyu.api.modules.gao.vo.GaoQuickRegistrationEventRecordReq; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCalendarView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordCreateReq; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordDailyStatView; -import com.imyeyu.api.modules.gao.vo.GaoRegistrationEventRecordListItemView; -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.system.entity.Logger; -import com.imyeyu.api.modules.system.service.LoggerService; -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.api.modules.user.mapper.UserMapper; -import com.imyeyu.java.TimiJava; -import com.imyeyu.java.bean.timi.TimiCode; -import com.imyeyu.java.bean.timi.TimiException; -import com.imyeyu.spring.TimiSpring; -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 com.imyeyu.utils.Time; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.poi.ss.usermodel.CellStyle; -import org.apache.poi.ss.usermodel.Font; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.io.ByteArrayOutputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -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.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.StringJoiner; -import java.util.function.Function; -import java.util.stream.Collectors; - -/// -/// 登记事件记录服务实现 -/// -/// @author Codex -/// @since 2026-07-27 -@Slf4j -@Service -@RequiredArgsConstructor -public class GaoRegistrationEventRecordServiceImplement extends AbstractEntityService implements GaoRegistrationEventRecordService { - - private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE; - private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); - - private final GaoRegistrationEventRecordMapper mapper; - private final ObjectMapper jackson; - private final GaoCustomerMapper customerMapper; - private final GaoRegistrationEventMapper registrationEventMapper; - private final UserMapper userMapper; - private final AttachmentService attachmentService; - private final LoggerService loggerService; - private final RoleChecker roleChecker; - private final UserLoginService userLoginService; - private final UserService userService; - private final GaoCustomerService customerService; - private final GaoRegistrationEventService registrationEventService; - - @Override - protected BaseMapper mapper() { - return mapper; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public List createRecords(GaoRegistrationEventRecordCreateReq req) { - Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_RECORD_BATCH_CREATE"); - try { - TimiException.required(req, "not found req"); - TimiException.requiredTrue(req.getCustomerIdList() != null && !req.getCustomerIdList().isEmpty(), "customerIdList is empty"); - logger.setContent(jackson.writeValueAsString(req)); - List recordList = new ArrayList<>(); - for (int index = 0; index < req.getCustomerIdList().size(); index++) { - String customerId = req.getCustomerIdList().get(index); - GaoRegistrationEventRecordCreateReq customerReq = copyCreateReq(req); - customerReq.setAttachmentIdList(0 == index && canUseSourceTempFileList(req.getAttachmentIdList()) - ? req.getAttachmentIdList() - : duplicateTempFileList(req.getAttachmentIdList())); - recordList.add(doCreateRecord(customerReq, customerId)); - } - logger.setLevel(Logger.Level.INFO); - logger.setResult(recordList.stream().map(GaoRegistrationEventRecord::getId).collect(Collectors.joining(","))); - return recordList; - } catch (TimiException e) { - logger.setLevel(Logger.Level.WARN); - logger.setException(e.getMessage()); - throw e; - } catch (Exception e) { - logger.setLevel(Logger.Level.ERROR); - logger.setException(TimiJava.serializeThrowable(e)); - log.error("批量新增 GAO 登记记录失败", e); - throw new TimiException(TimiCode.ERROR, "批量新增 GAO 登记记录失败", e); - } finally { - loggerService.create(logger); - } - } - - private List duplicateTempFileList(List sourceIdList) { - if (sourceIdList == null) { - return null; - } - List targetIdList = new ArrayList<>(); - for (String sourceId : sourceIdList) { - if (sourceId == null || sourceId.isBlank()) { - continue; - } - Attachment source = attachmentService.get(sourceId.trim()); - TimiException.required(source, "not found attachment"); - TimiException.required(source.getMongoId(), "not found attachment.mongoId"); - Attachment target = new Attachment(); - target.setBizType(Attachment.BizType.TEMP_FILE); - target.setName(source.getName()); - target.setInputStream(new ByteArrayInputStream(attachmentService.readAllByMongoId(source.getMongoId()))); - attachmentService.create(target); - targetIdList.add(target.getId()); - } - return targetIdList; - } - - private boolean canUseSourceTempFileList(List sourceIdList) { - if (sourceIdList == null || sourceIdList.isEmpty()) { - return true; - } - for (String sourceId : sourceIdList) { - if (sourceId == null || sourceId.isBlank()) { - continue; - } - Attachment source = attachmentService.get(sourceId.trim()); - if (source == null || source.getBizType() != Attachment.BizType.TEMP_FILE) { - return false; - } - } - return true; - } - - private GaoRegistrationEventRecordCreateReq copyCreateReq(GaoRegistrationEventRecordCreateReq source) { - GaoRegistrationEventRecordCreateReq target = new GaoRegistrationEventRecordCreateReq(); - target.setRegistrationEventId(source.getRegistrationEventId()); - target.setRemark(source.getRemark()); - target.setRegisteredAt(source.getRegisteredAt()); - return target; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoRegistrationEventRecord quickRegister(GaoQuickRegistrationEventRecordReq req) { - Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_RECORD_QUICK_REGISTER"); - try { - logger.setContent(jackson.writeValueAsString(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.setCustomerIdList(List.of(customer.getId())); - recordReq.setRegistrationEventId(req.getRegistrationEventId()); - recordReq.setRemark(req.getRemark()); - recordReq.setAttachmentIdList(req.getAttachmentIdList()); - GaoRegistrationEventRecord record = createRecords(recordReq).get(0); - logger.setLevel(Logger.Level.INFO); - logger.setResult(record.getId()); - return record; - } catch (TimiException e) { - logger.setLevel(Logger.Level.WARN); - logger.setException(e.getMessage()); - throw e; - } catch (Exception e) { - logger.setLevel(Logger.Level.ERROR); - logger.setException(TimiJava.serializeThrowable(e)); - log.error("GAO 快速登记失败", e); - throw new TimiException(TimiCode.ERROR, "GAO 快速登记失败", e); - } finally { - loggerService.create(logger); - } - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public void update(GaoRegistrationEventRecord entity) { - Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_RECORD_UPDATE"); - try { - logger.setContent(jackson.writeValueAsString(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)); - logger.setLevel(Logger.Level.INFO); - logger.setResult(entity.getId()); - } catch (TimiException e) { - logger.setLevel(Logger.Level.WARN); - logger.setException(e.getMessage()); - throw e; - } catch (Exception e) { - logger.setLevel(Logger.Level.ERROR); - logger.setException(TimiJava.serializeThrowable(e)); - log.error("修改 GAO 登记记录失败", e); - throw new TimiException(TimiCode.ERROR, "修改 GAO 登记记录失败", e); - } finally { - loggerService.create(logger); - } - } - - @Override - public GaoRegistrationEventRecord get(String id) { - GaoRegistrationEventRecord record = super.get(id); - loadTransient(List.of(record)); - return record; - } - - @Override - public List 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 list = mapper.selectByRegistrationEventAndPeriod(req.getRegistrationEventId(), req.getPeriodType(), req.getPeriodValue()); - loadTransient(list); - return list; - } - - @Override - public List listByRange(GaoRegistrationEventRecordRangeReq req) { - TimiException.required(req, "not found req"); - checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt()); - List list = mapper.selectByRange(req); - loadTransient(list); - return list; - } - - @Override - public List listCalendarByRange(GaoRegistrationEventRecordRangeReq req) { - TimiException.required(req, "not found req"); - checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt()); - return mapper.selectCalendarByRange(req); - } - - @Override - public PageResult pageByRange(Page page) { - TimiException.required(page, "not found page"); - TimiException.required(page.getIndex(), "not found page.index"); - TimiException.required(page.getSize(), "not found page.size"); - TimiException.requiredTrue(0 <= page.getIndex(), "page.index lt 0"); - TimiException.requiredTrue(0 < page.getSize(), "page.size lte 0"); - GaoRegistrationEventRecordRangeReq req = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoRegistrationEventRecordRangeReq()); - checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt()); - Long total = TimiJava.defaultIfNull(mapper.countByRange(req), 0L); - List list = mapper.selectPageByRange(req, page.getIndex() * page.getSize(), page.getSize()); - PageResult result = new PageResult<>(); - result.setTotal(total); - result.setList(list); - return result; - } - - @Override - public byte[] exportRangeExcel(GaoRegistrationEventRecordRangeReq req) throws IOException { - List list = listByRange(req); - try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { - Sheet sheet = workbook.createSheet("登记事件记录"); - CellStyle titleStyle = workbook.createCellStyle(); - Font titleFont = workbook.createFont(); - titleFont.setBold(true); - titleFont.setFontHeightInPoints((short) 14); - titleStyle.setFont(titleFont); - CellStyle headerStyle = workbook.createCellStyle(); - Font headerFont = workbook.createFont(); - headerFont.setBold(true); - headerStyle.setFont(headerFont); - - appendRow(sheet, 0, titleStyle, "登记事件记录导出"); - appendRow(sheet, 1, null, "生成时间", EXPORT_TIME_FORMATTER.format(Instant.now())); - appendRow(sheet, 2, null, "时间范围", "%s ~ %s".formatted(formatExportTime(req.getBeginRegisteredAt()), formatExportTime(req.getEndRegisteredAt()))); - appendRow(sheet, 3, null, "事件筛选", buildEventFilterText(req)); - appendRow(sheet, 4, null, "关键词", req.getKeyword() == null || req.getKeyword().isBlank() ? "全部" : req.getKeyword().trim()); - appendRow(sheet, 5, null, "记录总数", String.valueOf(list.size())); - appendRow(sheet, 7, headerStyle, "客户编码", "客户姓名", "登记事件", "登记时间", "登记人", "备注"); - int rowIndex = 8; - for (GaoRegistrationEventRecord record : list) { - appendRow(sheet, rowIndex, null, - record.getCustomer() == null ? "" : text(record.getCustomer().getCode()), - record.getCustomer() == null ? "" : text(record.getCustomer().getName()), - record.getRegistrationEvent() == null ? "" : text(record.getRegistrationEvent().getName()), - formatExportTime(record.getRegisteredAt()), - getOperatorName(record), - text(record.getRemark()) - ); - rowIndex++; - } - for (int index = 0; index < 6; index++) { - sheet.autoSizeColumn(index); - sheet.setColumnWidth(index, Math.min(Math.max(sheet.getColumnWidth(index), 12 * 256), 40 * 256)); - } - workbook.write(output); - return output.toByteArray(); - } - } - - @Override - public List 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 listLatestByCustomerIdListAndRegistrationEventId(List customerIdList, String registrationEventId) { - TimiException.required(registrationEventId, "not found registrationEventId"); - if (TimiJava.isEmpty(customerIdList)) { - return Collections.emptyList(); - } - List list = mapper.selectLatestByCustomerIdListAndRegistrationEventId(customerIdList, registrationEventId); - loadTransient(list); - return list; - } - - @Override - public List statByCustomerId(String customerId) { - TimiException.required(customerId, "not found customerId"); - return mapper.statByCustomerId(customerId); - } - - @Override - public List 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 statAllCustomerByRange(GaoRegistrationEventRecordStatReq req) { - TimiException.required(req, "not found req"); - checkStatRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt()); - return mapper.statAllCustomerByRange(req.getBeginRegisteredAt(), req.getEndRegisteredAt()); - } - - @Override - public List listCustomerRegistrationRank(GaoRegistrationEventRecordRangeReq req) { - GaoRegistrationEventRecordRangeReq rangeReq = TimiJava.defaultIfNull(req, new GaoRegistrationEventRecordRangeReq()); - if (rangeReq.getBeginRegisteredAt() != null || rangeReq.getEndRegisteredAt() != null) { - checkStatRange(rangeReq.getBeginRegisteredAt(), rangeReq.getEndRegisteredAt()); - } - List dayStatList = mapper.statCustomerRegistrationDay(rangeReq); - Map> customerDayStatMap = dayStatList.stream() - .collect(Collectors.groupingBy(GaoCustomerRegistrationDayStatView::getCustomerId)); - List rankList = new ArrayList<>(); - for (Map.Entry> entry : customerDayStatMap.entrySet()) { - rankList.add(buildCustomerRegistrationRank(entry.getValue())); - } - rankList.sort(Comparator - .comparing(GaoCustomerRegistrationRankView::getTotalRegistrationDays, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(GaoCustomerRegistrationRankView::getTotalRegistrationCount, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(GaoCustomerRegistrationRankView::getLatestRegisteredAt, Comparator.nullsLast(Comparator.reverseOrder()))); - for (int index = 0; index < rankList.size(); index++) { - rankList.get(index).setRank(index + 1); - } - return rankList; - } - - @Override - public List 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 statDailyByRegistrationEvent(GaoRegistrationEventRecordTrendStatReq req) { - TimiException.required(req, "not found req"); - return buildDailyTrend(req, false); - } - - @Override - public List listLatestByCustomerId(String customerId) { - TimiException.required(customerId, "not found customerId"); - List list = mapper.selectLatestByCustomerId(customerId); - loadTransient(list); - return list; - } - - private String resolveCustomerId(String customerId) { - TimiException.required(customerId, "not found customerId"); - customerService.get(customerId); - return customerId; - } - - private GaoRegistrationEventRecord doCreateRecord(GaoRegistrationEventRecordCreateReq req, String customerId) { - TimiException.required(req, "not found req"); - TimiException.required(req.getRegistrationEventId(), "not found req.registrationEventId"); - customerId = resolveCustomerId(customerId); - GaoRegistrationEvent registrationEvent = registrationEventService.get(req.getRegistrationEventId()); - checkRegistrationEventPermission(registrationEvent); - long registeredAt = TimiJava.defaultIfNull(req.getRegisteredAt(), Time.now()); - checkRegistrationEventCreatable(registrationEvent, customerId, registeredAt); - 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(registeredAt); - super.create(record); - saveAttachment(record.getId(), buildPhotoAttachmentList(req.getAttachmentIdList())); - return get(record.getId()); - } - - private void checkRegistrationEventPermission(GaoRegistrationEvent registrationEvent) { - List roleCodeList = registrationEventService.listRoleCode(registrationEvent.getId()); - TimiException.requiredTrue(roleCodeList != null && !roleCodeList.isEmpty(), "登记事件未配置可登记角色"); - roleChecker.checkAny(ModuleCode.GAO, roleCodeList.toArray(String[]::new)); - } - - private void checkRegistrationEventCreatable(GaoRegistrationEvent registrationEvent, String customerId, long registeredAt) { - TimiException.requiredTrue(registrationEvent.getStatus() == null || registrationEvent.getStatus() == GaoRegistrationEvent.Status.ACTIVE, "登记事件不可登记"); - TimiException.requiredTrue(registrationEvent.getBeginAt() == null || registrationEvent.getBeginAt() <= registeredAt, "登记事件未到可登记时间"); - TimiException.requiredTrue(registrationEvent.getEndAt() == null || registeredAt <= registrationEvent.getEndAt(), "登记事件已过可登记时间"); - GaoRegistrationEvent.LimitType limitType = TimiJava.defaultIfNull(registrationEvent.getLimitType(), GaoRegistrationEvent.LimitType.NONE); - switch (limitType) { - case DAILY_HALF_DAY -> checkRegistrationLimitRange(customerId, registrationEvent.getId(), getHalfDayBegin(registeredAt), getHalfDayEnd(registeredAt), "该客户当前半日已登记该事件"); - case NATURAL_DAY -> checkRegistrationLimitRange(customerId, registrationEvent.getId(), getDayBegin(registeredAt), getDayEnd(registeredAt), "该客户当日已登记该事件"); - case INTERVAL_HOUR -> checkIntervalHourRegistrationLimit(registrationEvent, customerId, registeredAt); - case NATURAL_MONTH -> checkRegistrationLimitRange(customerId, registrationEvent.getId(), getMonthBegin(registeredAt), getMonthEnd(registeredAt), "该客户当月已登记该事件"); - case TOTAL_LIMIT -> checkTotalRegistrationLimit(registrationEvent); - case CUSTOMER_TOTAL_LIMIT -> checkCustomerTotalRegistrationLimit(registrationEvent, customerId); - default -> { - } - } - } - - private void checkRegistrationLimitRange(String customerId, String registrationEventId, long beginRegisteredAt, long endRegisteredAt, String message) { - Long count = mapper.countByCustomerIdAndRegistrationEventIdRange(customerId, registrationEventId, beginRegisteredAt, endRegisteredAt); - TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, message); - } - - private void checkIntervalHourRegistrationLimit(GaoRegistrationEvent registrationEvent, String customerId, long registeredAt) { - Integer limitValue = registrationEvent.getLimitValue(); - TimiException.requiredTrue(limitValue != null && 0 < limitValue, "登记事件间隔小时配置错误"); - long beginRegisteredAt = registeredAt - limitValue * 60L * 60L * 1000L + 1; - checkRegistrationLimitRange(customerId, registrationEvent.getId(), beginRegisteredAt, registeredAt, "该客户登记该事件间隔不足"); - } - - private void checkCustomerTotalRegistrationLimit(GaoRegistrationEvent registrationEvent, String customerId) { - Integer limitValue = registrationEvent.getLimitValue(); - TimiException.requiredTrue(limitValue != null && 0 < limitValue, "登记事件单客户总次数配置错误"); - Long count = mapper.countByCustomerIdAndRegistrationEventId(customerId, registrationEvent.getId()); - TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < limitValue, "该客户登记该事件次数已达上限"); - } - - private void checkTotalRegistrationLimit(GaoRegistrationEvent registrationEvent) { - Integer limitValue = registrationEvent.getLimitValue(); - TimiException.requiredTrue(limitValue != null && 0 < limitValue, "登记事件总次数配置错误"); - Long count = mapper.countByRegistrationEventId(registrationEvent.getId()); - TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < limitValue, "登记事件总次数已达上限"); - } - - private long getDayBegin(long registeredAt) { - ZoneId zoneId = ZoneId.systemDefault(); - return Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); - } - - private long getDayEnd(long registeredAt) { - ZoneId zoneId = ZoneId.systemDefault(); - return Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate().plusDays(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1; - } - - private long getHalfDayBegin(long registeredAt) { - ZoneId zoneId = ZoneId.systemDefault(); - int hour = Instant.ofEpochMilli(registeredAt).atZone(zoneId).getHour(); - long dayBegin = getDayBegin(registeredAt); - return hour < 12 ? dayBegin : dayBegin + 12L * 60L * 60L * 1000L; - } - - private long getHalfDayEnd(long registeredAt) { - ZoneId zoneId = ZoneId.systemDefault(); - int hour = Instant.ofEpochMilli(registeredAt).atZone(zoneId).getHour(); - long dayBegin = getDayBegin(registeredAt); - return hour < 12 ? dayBegin + 12L * 60L * 60L * 1000L - 1 : getDayEnd(registeredAt); - } - - private long getMonthBegin(long registeredAt) { - ZoneId zoneId = ZoneId.systemDefault(); - LocalDate date = Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate(); - return date.withDayOfMonth(1).atStartOfDay(zoneId).toInstant().toEpochMilli(); - } - - private long getMonthEnd(long registeredAt) { - ZoneId zoneId = ZoneId.systemDefault(); - LocalDate date = Instant.ofEpochMilli(registeredAt).atZone(zoneId).toLocalDate(); - return date.withDayOfMonth(1).plusMonths(1).atStartOfDay(zoneId).toInstant().toEpochMilli() - 1; - } - - 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 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 dailyCountMap = new HashMap<>(); - List 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 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 GaoCustomerRegistrationRankView buildCustomerRegistrationRank(List dayStatList) { - List sortedDayStatList = dayStatList.stream() - .sorted(Comparator.comparing(GaoCustomerRegistrationDayStatView::getDateValue, Comparator.nullsLast(Comparator.reverseOrder()))) - .toList(); - GaoCustomerRegistrationDayStatView first = sortedDayStatList.get(0); - GaoCustomerRegistrationRankView view = new GaoCustomerRegistrationRankView(); - view.setCustomerId(first.getCustomerId()); - view.setCustomerCode(first.getCustomerCode()); - view.setCustomerName(first.getCustomerName()); - view.setCustomerPhotoAttachmentId(first.getCustomerPhotoAttachmentId()); - view.setTotalRegistrationDays((long) sortedDayStatList.size()); - view.setTotalRegistrationCount(sortedDayStatList.stream().mapToLong(item -> TimiJava.defaultIfNull(item.getDailyCount(), 0L)).sum()); - view.setLatestRegisteredAt(sortedDayStatList.stream().map(GaoCustomerRegistrationDayStatView::getLatestRegisteredAt).filter(Objects::nonNull).max(Long::compareTo).orElse(null)); - view.setRecentContinuousRegistrationDays(countRecentContinuousRegistrationDays(sortedDayStatList)); - return view; - } - - private long countRecentContinuousRegistrationDays(List sortedDayStatList) { - if (sortedDayStatList.isEmpty() || sortedDayStatList.get(0).getDateValue() == null) { - return 0; - } - LocalDate expectedDate = LocalDate.parse(String.valueOf(sortedDayStatList.get(0).getDateValue()), DAY_FORMATTER); - long count = 0; - for (GaoCustomerRegistrationDayStatView item : sortedDayStatList) { - if (item.getDateValue() == null) { - continue; - } - LocalDate date = LocalDate.parse(String.valueOf(item.getDateValue()), DAY_FORMATTER); - if (!date.equals(expectedDate)) { - break; - } - count++; - expectedDate = expectedDate.minusDays(1); - } - return count; - } - - private void loadTransient(List recordList) { - if (recordList == null || recordList.isEmpty()) { - return; - } - List customerIdList = recordList.stream() - .map(GaoRegistrationEventRecord::getCustomerId) - .filter(TimiJava::isNotEmpty) - .distinct() - .toList(); - Map customerMap = customerMapper.selectByIdList(new ArrayList<>(customerIdList)).stream() - .collect(Collectors.toMap(GaoCustomer::getId, Function.identity(), (left, right) -> left)); - - List registrationEventIdList = recordList.stream() - .map(GaoRegistrationEventRecord::getRegistrationEventId) - .filter(TimiJava::isNotEmpty) - .distinct() - .toList(); - Map registrationEventMap = registrationEventMapper.selectByIdList(new ArrayList<>(registrationEventIdList)).stream() - .collect(Collectors.toMap(GaoRegistrationEvent::getId, Function.identity(), (left, right) -> left)); - - List operatorUserIdList = recordList.stream() - .map(GaoRegistrationEventRecord::getOperatorUserId) - .filter(TimiJava::isNotEmpty) - .distinct() - .toList(); - Map userMap = userMapper.listByIdList(new ArrayList<>(operatorUserIdList)).stream() - .collect(Collectors.toMap(com.imyeyu.api.modules.user.entity.User::getId, Function.identity(), (left, right) -> left)); - - List recordIdList = recordList.stream() - .map(GaoRegistrationEventRecord::getId) - .filter(TimiJava::isNotEmpty) - .distinct() - .toList(); - Map> attachmentMap = attachmentService.mapByBizIdList( - Attachment.BizType.GAO_REGISTRATION_EVENT_RECORD, - recordIdList, - GaoRegistrationEventRecord.AttachType.PHOTO.name() - ); - - for (GaoRegistrationEventRecord record : recordList) { - record.setCustomer(customerMap.get(record.getCustomerId())); - record.setRegistrationEvent(registrationEventMap.get(record.getRegistrationEventId())); - record.setOperator(userMap.get(record.getOperatorUserId())); - record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); - } - } - - 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 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 resolveAttachmentList(GaoRegistrationEventRecord record) { - if (record.getAttachmentIdList() != null) { - return buildPhotoAttachmentList(record.getAttachmentIdList()); - } - return record.getAttachmentList(); - } - - private List buildPhotoAttachmentList(List attachmentIdList) { - if (attachmentIdList == null) { - return null; - } - List 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; - } - - private void appendRow(Sheet sheet, int rowIndex, CellStyle style, String... values) { - Row row = sheet.createRow(rowIndex); - for (int index = 0; index < values.length; index++) { - row.createCell(index).setCellValue(values[index]); - if (style != null) { - row.getCell(index).setCellStyle(style); - } - } - } - - private String buildEventFilterText(GaoRegistrationEventRecordRangeReq req) { - if ((req.getRegistrationEventId() == null || req.getRegistrationEventId().isBlank()) && (req.getRegistrationEventIdList() == null || req.getRegistrationEventIdList().isEmpty())) { - return "全部事件"; - } - if (req.getRegistrationEventId() != null && !req.getRegistrationEventId().isBlank()) { - return "事件 ID:" + req.getRegistrationEventId(); - } - StringJoiner joiner = new StringJoiner("、"); - req.getRegistrationEventIdList().stream() - .filter(Objects::nonNull) - .filter(item -> !item.isBlank()) - .forEach(joiner::add); - return "事件 ID:" + joiner; - } - - private String formatExportTime(Long value) { - if (value == null) { - return ""; - } - return EXPORT_TIME_FORMATTER.format(Instant.ofEpochMilli(value)); - } - - private String getOperatorName(GaoRegistrationEventRecord record) { - if (record.getOperator() == null) { - return text(record.getOperatorUserId()); - } - if (record.getOperator().getNick() != null && !record.getOperator().getNick().isBlank()) { - return record.getOperator().getNick(); - } - if (record.getOperator().getName() != null && !record.getOperator().getName().isBlank()) { - return record.getOperator().getName(); - } - return text(record.getOperatorUserId()); - } - - private String text(Object value) { - return value == null ? "" : value.toString(); - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoRegistrationEventServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoRegistrationEventServiceImplement.java deleted file mode 100644 index eda704e..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoRegistrationEventServiceImplement.java +++ /dev/null @@ -1,260 +0,0 @@ -package com.imyeyu.api.modules.gao.service.implement; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.imyeyu.api.config.dbsource.TimiServerDBConfig; -import com.imyeyu.api.modules.gao.entity.GaoRegistrationEvent; -import com.imyeyu.api.modules.gao.entity.GaoRegistrationEventRoleRelation; -import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventMapper; -import com.imyeyu.api.modules.gao.mapper.GaoRegistrationEventRoleRelationMapper; -import com.imyeyu.api.modules.gao.service.GaoRegistrationEventService; -import com.imyeyu.api.modules.system.entity.Logger; -import com.imyeyu.api.modules.system.service.LoggerService; -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 com.imyeyu.utils.Time; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/// -/// 登记事件服务实现 -/// -/// @author 夜雨 -/// @since 2026-07-27 09:00 -@Slf4j -@Service -@RequiredArgsConstructor -public class GaoRegistrationEventServiceImplement extends AbstractEntityService implements GaoRegistrationEventService { - - private final ObjectMapper jackson; - - private final LoggerService loggerService; - - private final GaoRegistrationEventMapper mapper; - private final GaoRegistrationEventRoleRelationMapper roleRelationMapper; - - @Override - protected BaseMapper mapper() { - return mapper; - } - - @Override - public PageResult pageWithRegistrationEventLimit(Page page) { - PageResult result = page(page); - result.setList(result.getList().stream().filter(item -> item.getStatus() != GaoRegistrationEvent.Status.DELETED).toList()); - loadRegistrationEventLimit(result.getList()); - return result; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public void create(GaoRegistrationEvent entity) { - Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_CREATE"); - try { - logger.setContent(jackson.writeValueAsString(entity)); - checkEntity(entity, false); - super.create(entity); - saveRoleRelation(entity.getId(), entity.getRoleCodeList()); - logger.setLevel(Logger.Level.INFO); - logger.setResult(entity.getId()); - } catch (TimiException e) { - logger.setLevel(Logger.Level.WARN); - logger.setException(e.getMessage()); - throw e; - } catch (Exception e) { - logger.setLevel(Logger.Level.ERROR); - logger.setException(TimiJava.serializeThrowable(e)); - log.error("新增 GAO 登记事件失败", e); - throw new TimiException(TimiCode.ERROR, "新增 GAO 登记事件失败", e); - } finally { - loggerService.create(logger); - } - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public void update(GaoRegistrationEvent entity) { - Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_UPDATE"); - try { - logger.setContent(jackson.writeValueAsString(entity)); - Integer sort = entity.getSort(); - checkEntity(entity, true); - GaoRegistrationEvent current = get(entity.getId()); - current.setName(entity.getName()); - current.setDescription(entity.getDescription()); - current.setColor(entity.getColor()); - current.setStatus(entity.getStatus()); - current.setRequirePhoto(entity.getRequirePhoto()); - current.setLimitType(entity.getLimitType()); - current.setLimitValue(entity.getLimitValue()); - current.setBeginAt(entity.getBeginAt()); - current.setEndAt(entity.getEndAt()); - if (sort != null) { - current.setSort(sort); - } - mapper.update(current); - saveRoleRelation(entity.getId(), entity.getRoleCodeList()); - logger.setLevel(Logger.Level.INFO); - logger.setResult(entity.getId()); - } catch (TimiException e) { - logger.setLevel(Logger.Level.WARN); - logger.setException(e.getMessage()); - throw e; - } catch (Exception e) { - logger.setLevel(Logger.Level.ERROR); - logger.setException(TimiJava.serializeThrowable(e)); - log.error("修改 GAO 登记事件失败", e); - throw new TimiException(TimiCode.ERROR, "修改 GAO 登记事件失败", e); - } finally { - loggerService.create(logger); - } - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public void delete(String id) { - Logger logger = new Logger(Logger.Module.GAO, "GAO_REGISTRATION_EVENT_DELETE"); - try { - logger.setContent(id); - GaoRegistrationEvent entity = get(id); - entity.setStatus(GaoRegistrationEvent.Status.DELETED); - super.update(entity); - logger.setLevel(Logger.Level.INFO); - logger.setResult(id); - } catch (TimiException e) { - logger.setLevel(Logger.Level.WARN); - logger.setException(e.getMessage()); - throw e; - } catch (Exception e) { - logger.setLevel(Logger.Level.ERROR); - logger.setException(TimiJava.serializeThrowable(e)); - log.error("删除 GAO 登记事件失败", e); - throw new TimiException(TimiCode.ERROR, "删除 GAO 登记事件失败", e); - } finally { - loggerService.create(logger); - } - } - - @Override - public GaoRegistrationEvent get(String id) { - GaoRegistrationEvent registrationEvent = super.get(id); - registrationEvent.setRoleCodeList(roleRelationMapper.selectRoleCodeListByRegistrationEventId(id)); - return registrationEvent; - } - - @Override - public List listEnabled() { - List list = mapper.selectEnabledList(Time.now()); - loadRegistrationEventLimit(list); - list = list.stream().filter(item -> !TimiJava.isEmpty(item.getRoleCodeList())).toList(); - return list; - } - - @Override - public List listRoleCode(String registrationEventId) { - return roleRelationMapper.selectRoleCodeListByRegistrationEventId(registrationEventId); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public void sort(List idList) { - if (idList == null || idList.isEmpty()) { - return; - } - List distinctIdList = new ArrayList<>(new LinkedHashSet<>(idList)); - Map eventMap = mapper.selectByIdList(distinctIdList).stream().collect(Collectors.toMap(GaoRegistrationEvent::getId, item -> item)); - TimiException.requiredTrue(eventMap.size() == distinctIdList.size(), "not found registrationEvent"); - for (int index = 0; index < distinctIdList.size(); index++) { - GaoRegistrationEvent registrationEvent = eventMap.get(distinctIdList.get(index)); - registrationEvent.setSort(index); - mapper.update(registrationEvent); - } - } - - 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.getStatus() == null) { - entity.setStatus(GaoRegistrationEvent.Status.ACTIVE); - } - if (entity.getRequirePhoto() == null) { - entity.setRequirePhoto(false); - } - if (entity.getLimitType() == null) { - entity.setLimitType(GaoRegistrationEvent.LimitType.NONE); - } - TimiException.requiredTrue(entity.getRoleCodeList() != null && entity.getRoleCodeList() - .stream() - .anyMatch(item -> item != null && !item.isBlank()), "not found registrationEvent.roleCodeList"); - checkRegistrationLimit(entity); - checkRegisterablePeriod(entity); - if (entity.getColor() != null) { - entity.setColor(entity.getColor().trim()); - } - if (entity.getSort() == null) { - entity.setSort(0); - } - } - - private void checkRegistrationLimit(GaoRegistrationEvent entity) { - if (entity.getLimitType() == GaoRegistrationEvent.LimitType.INTERVAL_HOUR || entity.getLimitType() == GaoRegistrationEvent.LimitType.TOTAL_LIMIT || entity.getLimitType() == GaoRegistrationEvent.LimitType.CUSTOMER_TOTAL_LIMIT) { - TimiException.required(entity.getLimitValue(), "not found registrationEvent.limitValue"); - TimiException.requiredTrue(0 < entity.getLimitValue(), "registrationEvent.limitValue lte 0"); - return; - } - entity.setLimitValue(null); - } - - private void checkRegisterablePeriod(GaoRegistrationEvent entity) { - if (entity.getBeginAt() == null && entity.getEndAt() == null) { - return; - } - TimiException.requiredTrue(entity.getBeginAt() == null || entity.getEndAt() == null || entity.getBeginAt() <= entity.getEndAt(), "registrationEvent.beginAt gt registrationEvent.endAt"); - } - - private void saveRoleRelation(String registrationEventId, List roleCodeList) { - roleRelationMapper.deleteByRegistrationEventId(registrationEventId); - if (roleCodeList == null || roleCodeList.isEmpty()) { - return; - } - Set 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(List registrationEventList) { - if (registrationEventList == null || registrationEventList.isEmpty()) { - return; - } - List registrationEventIdList = registrationEventList.stream().map(GaoRegistrationEvent::getId).distinct().toList(); - Map> roleCodeMap = roleRelationMapper.selectByRegistrationEventIdList(new ArrayList<>(registrationEventIdList)) - .stream() - .collect(Collectors.groupingBy(GaoRegistrationEventRoleRelation::getRegistrationEventId, Collectors.mapping(GaoRegistrationEventRoleRelation::getRoleCode, Collectors.toList()))); - for (GaoRegistrationEvent registrationEvent : registrationEventList) { - registrationEvent.setRoleCodeList(roleCodeMap.getOrDefault(registrationEvent.getId(), List.of())); - } - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoResourceScopeResolver.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoResourceScopeResolver.java new file mode 100644 index 0000000..453afdb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoResourceScopeResolver.java @@ -0,0 +1,113 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.bean.CoreRoleCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; +import com.imyeyu.api.modules.user.entity.Role; +import com.imyeyu.api.modules.user.entity.UserRole; +import com.imyeyu.api.modules.user.mapper.RoleMapper; +import com.imyeyu.api.modules.user.mapper.UserRoleMapper; +import com.imyeyu.api.modules.user.service.ModuleResourceScopeResolver; +import com.imyeyu.api.modules.user.service.RoleHierarchyService; +import com.imyeyu.java.TimiJava; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// GAO 资源范围解析器 +/// +/// @author Codex +/// @since 2026-08-07 +@Service +@RequiredArgsConstructor +public class GaoResourceScopeResolver implements ModuleResourceScopeResolver { + + private final RoleMapper roleMapper; + private final UserRoleMapper userRoleMapper; + private final GaoStoreService gaoStoreService; + private final RoleHierarchyService roleHierarchyService; + + @Override + public ModuleCode moduleCode() { + return ModuleCode.GAO; + } + + @Override + public boolean canManageUser(String operatorUserId, String targetUserId) { + if (hasCoreRole(operatorUserId, CoreRoleCode.ADMIN)) { + return true; + } + if (hasGaoRole(operatorUserId, GaoRoleCode.GLOBAL_MANAGER)) { + return true; + } + if (!hasGaoRole(operatorUserId, GaoRoleCode.STORE_MANAGER)) { + return false; + } + GaoStore operatorStore = gaoStoreService.getByUserId(operatorUserId); + GaoStore targetStore = gaoStoreService.getByUserId(targetUserId); + return operatorStore != null && targetStore != null && operatorStore.getId().equals(targetStore.getId()); + } + + @Override + public boolean canManageRole(String operatorUserId, Role role) { + if (hasCoreRole(operatorUserId, CoreRoleCode.ADMIN)) { + return true; + } + if (hasGaoRole(operatorUserId, GaoRoleCode.GLOBAL_MANAGER)) { + return true; + } + if (!hasGaoRole(operatorUserId, GaoRoleCode.STORE_MANAGER)) { + return false; + } + if (canUseOwnerScope(operatorUserId, role.getOwnerType(), role.getOwnerId())) { + return true; + } + return ModuleCode.GAO == role.getModuleCode() + && role.getOwnerType() == AuthOwnerType.MODULE + && (TimiJava.isEmpty(role.getOwnerId()) || ModuleCode.GAO.name().equals(role.getOwnerId())); + } + + @Override + public boolean canUseOwnerScope(String operatorUserId, AuthOwnerType ownerType, String ownerId) { + if (hasCoreRole(operatorUserId, CoreRoleCode.ADMIN)) { + return ownerType == AuthOwnerType.MODULE && ModuleCode.GAO.name().equals(ownerId) || ownerType == AuthOwnerType.STORE; + } + if (hasGaoRole(operatorUserId, GaoRoleCode.GLOBAL_MANAGER)) { + return ownerType == AuthOwnerType.MODULE && ModuleCode.GAO.name().equals(ownerId) || ownerType == AuthOwnerType.STORE; + } + if (!hasGaoRole(operatorUserId, GaoRoleCode.STORE_MANAGER) || ownerType != AuthOwnerType.STORE) { + return false; + } + GaoStore store = gaoStoreService.getByUserId(operatorUserId); + return store != null && store.getId().equals(ownerId); + } + + private boolean hasGaoRole(String userId, GaoRoleCode roleCode) { + if (TimiJava.isEmpty(userId)) { + return false; + } + List roleIdList = userRoleMapper.selectByUserIdList(List.of(userId)).stream().map(UserRole::getRoleId).toList(); + if (TimiJava.isEmpty(roleIdList)) { + return false; + } + // 身份角色只依赖系统、模块级继承,不能依赖当前正在计算的门店作用域 + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(roleIdList); + return roleMapper.selectByIdList(allRoleIdList).stream().anyMatch(role -> ModuleCode.GAO == role.getModuleCode() && roleCode.name().equals(role.getCode())); + } + + private boolean hasCoreRole(String userId, CoreRoleCode roleCode) { + if (TimiJava.isEmpty(userId)) { + return false; + } + List roleIdList = userRoleMapper.selectByUserIdList(List.of(userId)).stream().map(UserRole::getRoleId).toList(); + if (TimiJava.isEmpty(roleIdList)) { + return false; + } + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(roleIdList); + return roleMapper.selectByIdList(allRoleIdList).stream().anyMatch(role -> ModuleCode.CORE == role.getModuleCode() && roleCode.name().equals(role.getCode())); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java new file mode 100644 index 0000000..a359323 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java @@ -0,0 +1,135 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; +import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; +import com.imyeyu.api.modules.gao.mapper.GaoStoreBusinessDayMapper; +import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; +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 lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/// +/// GAO 门店营业日服务实现 +/// +/// @author Codex +/// @since 2026-08-06 +@Service +@RequiredArgsConstructor +public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService implements GaoStoreBusinessDayService { + + private final GaoStoreBusinessDayMapper mapper; + private final GaoEventRecordMapper eventRecordMapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public void create(GaoStoreBusinessDay businessDay) { + prepareBusinessDay(businessDay); + super.create(businessDay); + if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.OPEN) { + rebuildBusinessDayNo(businessDay.getStoreId(), businessDay.getDateValue()); + } + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public void update(GaoStoreBusinessDay businessDay) { + TimiException.required(businessDay, "not found businessDay"); + TimiException.required(businessDay.getId(), "not found businessDay.id"); + GaoStoreBusinessDay dbBusinessDay = get(businessDay.getId()); + TimiException.required(dbBusinessDay, "not found dbBusinessDay"); + TimiException.requiredTrue(dbBusinessDay.getStoreId().equals(businessDay.getStoreId()), "businessDay.storeId invalid"); + TimiException.requiredTrue(dbBusinessDay.getDateValue().equals(businessDay.getDateValue()), "businessDay.dateValue invalid"); + if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.CLOSED) { + checkNoRecord(dbBusinessDay); + } + prepareBusinessDay(businessDay); + super.update(businessDay); + rebuildBusinessDayNo(businessDay.getStoreId(), businessDay.getDateValue()); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public void delete(String id) { + GaoStoreBusinessDay businessDay = get(id); + TimiException.required(businessDay, "not found businessDay"); + if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.OPEN) { + checkNoRecord(businessDay); + } + super.delete(id); + rebuildBusinessDayNo(businessDay.getStoreId(), businessDay.getDateValue()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoStoreBusinessDay resolveOpenBusinessDay(String storeId, Integer dateValue) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(dateValue, "not found dateValue"); + GaoStoreBusinessDay businessDay = mapper.selectByStoreIdAndDateValue(storeId, dateValue); + if (businessDay != null) { + TimiException.requiredTrue(businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.OPEN, "门店当日休息,不能登记"); + TimiException.required(businessDay.getBusinessDayNo(), "not found businessDay.businessDayNo"); + return businessDay; + } + businessDay = new GaoStoreBusinessDay(); + businessDay.setStoreId(storeId); + businessDay.setDateValue(dateValue); + businessDay.setOpenStatus(GaoStoreBusinessDay.OpenStatus.OPEN); + super.create(businessDay); + rebuildBusinessDayNo(storeId, dateValue); + return mapper.selectByStoreIdAndDateValue(storeId, dateValue); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void rebuildBusinessDayNo(String storeId, Integer beginDateValue) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(beginDateValue, "not found beginDateValue"); + long businessDayNo = TimiJava.defaultIfNull(mapper.selectLastBusinessDayNoBefore(storeId, beginDateValue), 0L); + for (GaoStoreBusinessDay businessDay : mapper.selectOpenListFromDate(storeId, beginDateValue)) { + businessDayNo++; + if (!Long.valueOf(businessDayNo).equals(businessDay.getBusinessDayNo())) { + businessDay.setBusinessDayNo(businessDayNo); + mapper.update(businessDay); + } + eventRecordMapper.updateBusinessDayNoByStoreIdAndRegisteredDateValue(storeId, businessDay.getDateValue(), businessDayNo); + } + } + + @Override + public List listByDateRange(String storeId, Integer beginDateValue, Integer endDateValue) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(beginDateValue, "not found beginDateValue"); + TimiException.required(endDateValue, "not found endDateValue"); + TimiException.requiredTrue(beginDateValue <= endDateValue, "endDateValue lt beginDateValue"); + return mapper.selectByStoreIdAndDateRange(storeId, beginDateValue, endDateValue); + } + + private void prepareBusinessDay(GaoStoreBusinessDay businessDay) { + TimiException.required(businessDay, "not found businessDay"); + TimiException.required(businessDay.getStoreId(), "not found businessDay.storeId"); + TimiException.required(businessDay.getDateValue(), "not found businessDay.dateValue"); + businessDay.setOpenStatus(TimiJava.defaultIfNull(businessDay.getOpenStatus(), GaoStoreBusinessDay.OpenStatus.OPEN)); + if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.CLOSED) { + businessDay.setBusinessDayNo(null); + return; + } + businessDay.setBusinessDayNo(null); + } + + private void checkNoRecord(GaoStoreBusinessDay businessDay) { + Long count = eventRecordMapper.countByStoreIdAndRegisteredDateValue(businessDay.getStoreId(), businessDay.getDateValue()); + TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, "该营业日已有登记记录,不能改为休息或删除"); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreChartServiceImplement.java new file mode 100644 index 0000000..00f02ff --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreChartServiceImplement.java @@ -0,0 +1,64 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoStoreChartService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// 门店报表服务实现 +/// +/// @author Codex +/// @since 2026-08-11 +@Service +@RequiredArgsConstructor +public class GaoStoreChartServiceImplement implements GaoStoreChartService { + + private final GaoCustomerService customerService; + private final GaoEventRecordMapper eventRecordMapper; + + @Override + public List customerDailyTrend(GaoStoreChartReq req) { + checkRangeReq(req); + GaoCustomerTrendStateReq statReq = new GaoCustomerTrendStateReq(); + statReq.setStoreId(req.getStoreId()); + statReq.setBeginCreatedAt(req.getBeginAt()); + statReq.setEndCreatedAt(req.getEndAt()); + return customerService.stateDailyTrend(statReq); + } + + @Override + public List customerGender(String storeId) { + TimiException.required(storeId, "not found storeId"); + return customerService.stateGender(storeId); + } + + @Override + public List eventRecordDailyStat(GaoStoreChartReq req) { + checkRangeReq(req); + return eventRecordMapper.stateDailyByRange(req); + } + + @Override + public List eventRecordCalendar(GaoStoreChartReq req) { + checkRangeReq(req); + return eventRecordMapper.selectCalendarByRange(req); + } + + private void checkRangeReq(GaoStoreChartReq req) { + TimiException.required(req, "not found req"); + TimiException.required(req.getStoreId(), "not found req.storeId"); + TimiException.required(req.getBeginAt(), "not found req.beginAt"); + TimiException.required(req.getEndAt(), "not found req.endAt"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "req.endAt lt req.beginAt"); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java new file mode 100644 index 0000000..6c1dbb1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java @@ -0,0 +1,194 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.api.modules.gao.mapper.GaoStoreMapper; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.gao.util.StoreXSSFBuilder; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +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.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// +/// GAO 门店服务实现 +/// +/// @author Codex +/// @since 2026-08-06 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoStoreServiceImplement extends AbstractEntityService implements GaoStoreService { + + private final ObjectMapper jackson; + + private final LoggerService loggerService; + private final UserLoginService userLoginService; + + private final GaoStoreMapper mapper; + private final StoreXSSFBuilder storeXSSFBuilder; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoStore store) { + TimiException.required(store, "not found store"); + TimiException.required(store.getCode(), "not found store.code"); + TimiException.required(store.getName(), "not found store.name"); + + Logger logger = new Logger(Logger.Module.GAO, "GAO_STORE_CREATE"); + try { + logger.setContent(jackson.writeValueAsString(store)); + checkUnique(store); + store.setStatus(TimiJava.defaultIfNull(store.getStatus(), GaoStore.Status.ACTIVE)); + store.setSort(TimiJava.defaultIfNull(store.getSort(), 0)); + super.create(store); + logger.setLevel(Logger.Level.INFO); + logger.setResult(store.getId()); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("新增 GAO 门店失败", e); + throw new TimiException(TimiCode.ERROR, "新增 GAO 门店失败", e); + } finally { + loggerService.create(logger); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoStore store) { + TimiException.required(store, "not found store"); + TimiException.required(store.getId(), "not found store.id"); + TimiException.required(store.getCode(), "not found store.code"); + TimiException.required(store.getName(), "not found store.name"); + + Logger logger = new Logger(Logger.Module.GAO, "GAO_STORE_UPDATE"); + try { + logger.setContent(jackson.writeValueAsString(store)); + checkUnique(store); + GaoStore dbStore = get(store.getId()); + dbStore.setCode(store.getCode()); + dbStore.setName(store.getName()); + dbStore.setStatus(store.getStatus()); + dbStore.setTelephone(store.getTelephone()); + dbStore.setAddress(store.getAddress()); + dbStore.setRemark(store.getRemark()); + super.update(dbStore); + logger.setLevel(Logger.Level.INFO); + logger.setResult(store.getId()); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("修改 GAO 门店失败", e); + throw new TimiException(TimiCode.ERROR, "修改 GAO 门店失败", e); + } finally { + loggerService.create(logger); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(String id) { + Logger logger = new Logger(Logger.Module.GAO, "GAO_STORE_DELETE"); + try { + logger.setContent(id); + GaoStore store = get(id); + store.setStatus(GaoStore.Status.DELETED); + super.update(store); + super.delete(id); + logger.setLevel(Logger.Level.INFO); + logger.setResult(id); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("删除 GAO 门店失败", e); + throw new TimiException(TimiCode.ERROR, "删除 GAO 门店失败", e); + } finally { + loggerService.create(logger); + } + } + + @Override + public GaoStore getByUserId(String userId) { + TimiException.required(userId, "not found userId"); + return mapper.selectByUserId(userId); + } + + @Override + public String getBelongIdByRequiredLoginUserId() { + return getByUserId(userLoginService.getRequireLoginUserId()).getId(); + } + + @Override + public byte[] export(Page page) throws IOException { + page.setIndex(0); + page.setSize(Long.MAX_VALUE); + List list = mapper.selectByPage(page); + return storeXSSFBuilder.build(list); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void sort(List idList) { + if (TimiJava.isEmpty(idList)) { + return; + } + + List storeList = mapper.selectByIdList(new LinkedHashSet<>(idList)); + Map storeMap = storeList.stream().collect(Collectors.toMap(GaoStore::getId, Function.identity())); + for (int i = 0; i < idList.size(); i++) { + GaoStore store = storeMap.get(idList.get(i)); + if (store == null) { + continue; + } + store.setSort(i); + mapper.update(store); + } + } + + private void checkUnique(GaoStore store) { + GaoStore codeExample = new GaoStore(); + codeExample.setCode(store.getCode()); + GaoStore byCode = mapper.selectByExample(codeExample); + TimiException.requiredTrue(byCode == null || byCode.getId().equals(store.getId()), "该门店编码已存在"); + + GaoStore nameExample = new GaoStore(); + nameExample.setName(store.getName()); + GaoStore byName = mapper.selectByExample(nameExample); + TimiException.requiredTrue(byName == null || byName.getId().equals(store.getId()), "该门店名称已存在"); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java index 1ec583b..66891e4 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java @@ -1,39 +1,25 @@ package com.imyeyu.api.modules.gao.service.implement; import com.fasterxml.jackson.databind.ObjectMapper; -import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoUser; import com.imyeyu.api.modules.gao.mapper.GaoUserMapper; +import com.imyeyu.api.modules.gao.service.GaoStoreService; import com.imyeyu.api.modules.gao.service.GaoUserService; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; -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.TimiSpring; -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 com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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 用户服务实现 /// @@ -44,12 +30,12 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class GaoUserServiceImplement extends AbstractEntityService implements GaoUserService { - private final GaoUserMapper mapper; private final ObjectMapper jackson; - private final LoggerService loggerService; - private final UserMapper userMapper; private final UserService userService; - private final RoleService roleService; + private final LoggerService loggerService; + private final GaoStoreService gaoStoreService; + + private final GaoUserMapper mapper; @Override protected BaseMapper mapper() { @@ -58,17 +44,28 @@ public class GaoUserServiceImplement extends AbstractEntityService pageWithUser(Page page) { - PageResult result = page(page); - loadUser(result.getList()); - return result; - } + Logger logger = new Logger(Logger.Module.GAO, "GAO_USER_TRANSFER_STORE"); + try { + logger.setContent(jackson.writeValueAsString(gaoUser)); - 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, "GAO 用户已存在"); - } - if (entity.getEnabled() == null) { - entity.setEnabled(true); - } - } + GaoStore store = gaoStoreService.get(gaoUser.getStoreId()); + TimiException.required(store, "not found store"); + TimiException.requiredTrue(store.isActivated(), "门店不可用"); - 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"); - user.setPhoneNoVerifyAt(TimiJava.defaultIfNull(user.getPhoneNoVerifyAt(), Time.now())); - userService.create(user); - entity.setUserId(user.getId()); - } + GaoUser dbGaoUser = super.get(gaoUser.getId()); + TimiException.required(dbGaoUser, "not found gao user"); - 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); - } + dbGaoUser.setStoreId(gaoUser.getStoreId()); + super.update(dbGaoUser); - 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 gaoUserList) { - if (gaoUserList == null || gaoUserList.isEmpty()) { - return; - } - List userIdList = gaoUserList.stream().map(GaoUser::getUserId).distinct().toList(); - Map 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 roleList = roleService.listByUserId(gaoUser.getUserId(), ModuleCode.GAO); - gaoUser.setRoleList(roleList); + logger.setLevel(Logger.Level.INFO); + logger.setResult(gaoUser.getId()); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("transfer user store error", e); + throw new TimiException(TimiCode.ERROR, "transfer user store error", e); + } finally { + loggerService.create(logger); } } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/util/CustomerXSSFBuilder.java b/src/main/java/com/imyeyu/api/modules/gao/util/CustomerXSSFBuilder.java new file mode 100644 index 0000000..eed8e8b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/util/CustomerXSSFBuilder.java @@ -0,0 +1,67 @@ +package com.imyeyu.api.modules.gao.util; + +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.util.BaseXSSFBuilder; +import com.imyeyu.utils.Time; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +/// +/// +/// @author 夜雨 +/// @since 2026-08-04 15:59 +@Component +public class CustomerXSSFBuilder extends BaseXSSFBuilder { + + public byte[] build(List list) throws IOException { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("客户列表"); + CellStyle titleStyle = createTitleStyle(workbook); + CellStyle headerStyle = createHeaderStyle(workbook); + + appendRow(sheet, 0, titleStyle, "客户列表导出"); + appendRow(sheet, 1, null, "生成时间", Time.nowString()); + appendRow(sheet, 2, null, "记录总数", String.valueOf(list.size())); + appendRow(sheet, 3, headerStyle, "客户编码", "姓名", "性别", "出生日期(公历)", "出生日期历法", "年龄", "电话", "地址", "病症", "备注", "邀请人", "邀请客户数", "创建时间"); + int rowIndex = 4; + for (GaoCustomer customer : list) { + appendRow(sheet, rowIndex, null, + text(customer.getCode()), + text(customer.getName()), + gender(customer.getGender()), + text(customer.getBirthdate()), + formatBirthdateCalendar(customer), + customer.getAge() == null ? "" : String.valueOf(customer.getAge()), + text(customer.getTelephone()), + text(customer.getAddress()), + text(customer.getDisease()), + text(customer.getRemark()), + customer.getIntroducerCustomer() == null ? "" : text(customer.getIntroducerCustomer().getName()), + String.valueOf(customer.getInvitedCount()), + Time.toDateTime(customer.getCreatedAt()) + ); + rowIndex++; + } + for (int index = 0; index < 13; index++) { + sheet.autoSizeColumn(index); + sheet.setColumnWidth(index, Math.clamp(sheet.getColumnWidth(index), 10 * 256, 36 * 256)); + } + workbook.write(output); + return output.toByteArray(); + } + } + + private String formatBirthdateCalendar(GaoCustomer customer) { + if (customer.getBirthdateCalendar() == null) { + return "公历"; + } + return customer.getBirthdateCalendar() == GaoCustomer.BirthdateCalendar.LUNAR ? "农历" : "公历"; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/util/EventRecordXSSFBuilder.java b/src/main/java/com/imyeyu/api/modules/gao/util/EventRecordXSSFBuilder.java new file mode 100644 index 0000000..ce4db1f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/util/EventRecordXSSFBuilder.java @@ -0,0 +1,53 @@ +package com.imyeyu.api.modules.gao.util; + +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.util.BaseXSSFBuilder; +import com.imyeyu.utils.Time; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +/// +/// +/// @author 夜雨 +/// @since 2026-08-05 11:03 +@Component +public class EventRecordXSSFBuilder extends BaseXSSFBuilder { + + public byte[] build(List list) throws IOException { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("登记事件记录"); + CellStyle titleStyle = createTitleStyle(workbook); + CellStyle headerStyle = createHeaderStyle(workbook); + + appendRow(sheet, 0, titleStyle, "登记事件记录导出"); + appendRow(sheet, 1, null, "生成时间", Time.nowString()); + appendRow(sheet, 2, null, "记录总数", String.valueOf(list.size())); + appendRow(sheet, 3, headerStyle, "客户编码", "客户姓名", "登记事件", "登记时间", "登记人", "备注"); + int rowIndex = 4; + for (GaoEventRecord record : list) { + appendRow(sheet, rowIndex, null, + record.getCustomer() == null ? "" : text(record.getCustomer().getCode()), + record.getCustomer() == null ? "" : text(record.getCustomer().getName()), + record.getEvent() == null ? "" : text(record.getEvent().getName()), + Time.toDateTime(record.getRegisteredAt()), + record.getOperator().getName(), + text(record.getRemark()) + ); + rowIndex++; + } + for (int i = 0; i < 6; i++) { + sheet.autoSizeColumn(i); + sheet.setColumnWidth(i, Math.clamp(sheet.getColumnWidth(i), 12 * 256, 40 * 256)); + } + workbook.write(output); + return output.toByteArray(); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/util/StoreXSSFBuilder.java b/src/main/java/com/imyeyu/api/modules/gao/util/StoreXSSFBuilder.java new file mode 100644 index 0000000..000f7eb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/util/StoreXSSFBuilder.java @@ -0,0 +1,61 @@ +package com.imyeyu.api.modules.gao.util; + +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.api.util.BaseXSSFBuilder; +import com.imyeyu.utils.Time; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +/// +/// 门店 Excel 构建器 +/// +/// @author Codex +/// @since 2026-08-06 +@Component +public class StoreXSSFBuilder extends BaseXSSFBuilder { + + /// 构建门店列表 Excel + /// + /// @param list 门店列表 + /// @return xlsx 文件字节 + /// @throws IOException 写入异常 + public byte[] build(List list) throws IOException { + try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("门店列表"); + CellStyle titleStyle = createTitleStyle(workbook); + CellStyle headerStyle = createHeaderStyle(workbook); + + appendRow(sheet, 0, titleStyle, "门店列表导出"); + appendRow(sheet, 1, null, "生成时间", Time.nowString()); + appendRow(sheet, 2, null, "记录总数", String.valueOf(list.size())); + appendRow(sheet, 3, headerStyle, "门店编码", "门店名称", "状态", "电话", "地址", "备注", "排序", "创建时间"); + int rowIndex = 4; + for (GaoStore store : list) { + appendRow(sheet, rowIndex, null, + text(store.getCode()), + text(store.getName()), + store.getStatus() == null ? "" : store.getStatus().name(), + text(store.getTelephone()), + text(store.getAddress()), + text(store.getRemark()), + store.getSort() == null ? "" : String.valueOf(store.getSort()), + Time.toDateTime(store.getCreatedAt()) + ); + rowIndex++; + } + for (int index = 0; index < 8; index++) { + sheet.autoSizeColumn(index); + sheet.setColumnWidth(index, Math.clamp(sheet.getColumnWidth(index), 10 * 256, 36 * 256)); + } + workbook.write(output); + return output.toByteArray(); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerChartReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerChartReq.java new file mode 100644 index 0000000..8b0c6ad --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerChartReq.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.util.List; + +/// 客户报表查询请求 +/// +/// @author Codex +/// @since 2026-08-11 +@Data +public class GaoCustomerChartReq { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 开始时间 + private Long beginAt; + + /// 结束时间 + private Long endAt; + + /// 登记事件 ID 列表 + private List eventIdList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDailyStateView.java similarity index 89% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDailyStatView.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDailyStateView.java index 3a75c4c..8198c95 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDailyStatView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerDailyStateView.java @@ -8,7 +8,7 @@ import lombok.Data; /// @author Codex /// @since 2026-07-28 @Data -public class GaoCustomerDailyStatView { +public class GaoCustomerDailyStateView { /// 日期值,格式为 yyyyMMdd private Integer dateValue; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegistrationRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerEventRankView.java similarity index 52% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegistrationRankView.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerEventRankView.java index ee9fb11..db3a492 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegistrationRankView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerEventRankView.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.vo; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; import lombok.Data; /// @@ -8,7 +9,7 @@ import lombok.Data; /// @author Codex /// @since 2026-08-03 @Data -public class GaoCustomerRegistrationRankView { +public class GaoCustomerEventRankView { /// 排名 private Integer rank; @@ -16,23 +17,17 @@ public class GaoCustomerRegistrationRankView { /// 客户 ID private String customerId; - /// 客户编码 - private String customerCode; - - /// 客户姓名 - private String customerName; - - /// 客户头像附件 ID - private String customerPhotoAttachmentId; + /// 客户 + private GaoCustomer customer; /// 累计登记天数 - private Long totalRegistrationDays; + private Long totalEventDays; /// 累计登记次数 - private Long totalRegistrationCount; + private Long totalEventCount; /// 最近连续登记天数 - private Long recentContinuousRegistrationDays; + private Long recentContinuousEventDays; /// 最后登记时间 private Long latestRegisteredAt; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerInviteUpdateReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerInviteUpdateReq.java deleted file mode 100644 index fe21f48..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerInviteUpdateReq.java +++ /dev/null @@ -1,18 +0,0 @@ -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; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegisterStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegisterStatView.java deleted file mode 100644 index 741b448..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegisterStatView.java +++ /dev/null @@ -1,30 +0,0 @@ -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; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegistrationDayStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegistrationDayStatView.java deleted file mode 100644 index 902977c..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRegistrationDayStatView.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.imyeyu.api.modules.gao.vo; - -import lombok.Data; - -/// -/// 客户登记日期统计视图 -/// -/// @author Codex -/// @since 2026-08-03 -@Data -public class GaoCustomerRegistrationDayStatView { - - /// 客户 ID - private String customerId; - - /// 客户编码 - private String customerCode; - - /// 客户姓名 - private String customerName; - - /// 客户头像附件 ID - private String customerPhotoAttachmentId; - - /// 登记日期,格式 yyyyMMdd - private Integer dateValue; - - /// 当日登记次数 - private Long dailyCount; - - /// 当日最后登记时间 - private Long latestRegisteredAt; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerSearchReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerSearchReq.java deleted file mode 100644 index 244c1f9..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerSearchReq.java +++ /dev/null @@ -1,18 +0,0 @@ -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; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStatReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStateReq.java similarity index 75% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStatReq.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStateReq.java index b0ca66a..881d521 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStatReq.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStateReq.java @@ -8,7 +8,10 @@ import lombok.Data; /// @author Codex /// @since 2026-07-28 @Data -public class GaoCustomerTrendStatReq { +public class GaoCustomerTrendStateReq { + + /// 门店 ID + private String storeId; /// 开始创建时间 private Long beginCreatedAt; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordCalendarView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordCalendarView.java similarity index 65% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordCalendarView.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordCalendarView.java index 3ee29a9..5a9722d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordCalendarView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordCalendarView.java @@ -2,15 +2,14 @@ package com.imyeyu.api.modules.gao.vo; import lombok.Data; -/// -/// 登记事件记录日历轻量视图 +/// 登记事件日历记录视图 /// /// @author Codex -/// @since 2026-08-01 +/// @since 2026-08-11 @Data -public class GaoRegistrationEventRecordCalendarView { +public class GaoEventRecordCalendarView { - /// 记录 ID + /// 登记记录 ID private String id; /// 客户 ID @@ -23,7 +22,7 @@ public class GaoRegistrationEventRecordCalendarView { private String customerName; /// 登记事件 ID - private String registrationEventId; + private String eventId; /// 登记时间 private Long registeredAt; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordDailyStatView.java similarity index 53% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordDailyStatView.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordDailyStatView.java index 477b902..039a326 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordDailyStatView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordDailyStatView.java @@ -2,20 +2,19 @@ package com.imyeyu.api.modules.gao.vo; import lombok.Data; -/// -/// 登记事件记录每日统计视图 +/// 登记事件每日统计视图 /// /// @author Codex -/// @since 2026-07-28 +/// @since 2026-08-11 @Data -public class GaoRegistrationEventRecordDailyStatView { +public class GaoEventRecordDailyStatView { + + /// 登记事件 ID + private String eventId; /// 日期值,格式为 yyyyMMdd private Integer dateValue; /// 当日登记数 private Long dailyCount; - - /// 截止当日累计登记数 - private Long totalCount; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordRangeReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java similarity index 56% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordRangeReq.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java index d579c34..870c859 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordRangeReq.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java @@ -1,32 +1,35 @@ package com.imyeyu.api.modules.gao.vo; +import com.imyeyu.java.bean.BasePage; import lombok.Data; +import lombok.EqualsAndHashCode; import java.util.List; /// /// 登记事件记录范围查询请求 /// -/// @author Codex -/// @since 2026-07-28 +/// @author 夜雨 +/// @since 2026-07-28 11:01 @Data -public class GaoRegistrationEventRecordRangeReq { +@EqualsAndHashCode(callSuper = true) +public class GaoEventRecordPage extends BasePage { + + /// 门店 ID + private String storeId; /// 客户 ID,为空查询全部客户 private String customerId; - /// 登记事件 ID,为空查询全部登记事件 - private String registrationEventId; - /// 登记事件 ID 列表,为空查询全部登记事件 - private List registrationEventIdList; + private List eventIdList; /// 关键词,匹配客户、登记事件、备注、登记人 private String keyword; /// 开始登记时间 - private Long beginRegisteredAt; + private Long beginAt; /// 结束登记时间 - private Long endRegisteredAt; + private Long endAt; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordRankPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordRankPage.java new file mode 100644 index 0000000..537b853 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordRankPage.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// +/// +/// @author 夜雨 +/// @since 2026-08-05 12:44 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoEventRecordRankPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 客户 ID,为空查询全部客户 + private String customerId; + + /// 登记事件 ID 列表,为空查询全部登记事件 + private List eventIdList; + + /// 开始登记时间 + private Long beginAt; + + /// 结束登记时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointAdjustRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointAdjustRequest.java new file mode 100644 index 0000000..f3f0775 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointAdjustRequest.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 客户积分人工调整请求 +/// +/// @author Codex +/// @since 2026-08-11 +@Data +public class GaoPointAdjustRequest { + + /// 人工调整请求 ID,用于生成幂等键 + private String adjustRequestId; + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 调整积分,正数增加,负数减少 + private Long pointDelta; + + /// 调整原因 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointLedgerPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointLedgerPage.java new file mode 100644 index 0000000..7e30f57 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPointLedgerPage.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 客户积分流水查询请求 +/// +/// @author Codex +/// @since 2026-08-11 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoPointLedgerPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 客户积分账户 ID + private String accountId; + + /// 流水变化类型 + private GaoPointLedger.ChangeType changeType; + + /// 开始发生时间 + private Long beginAt; + + /// 结束发生时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoQuickRegistrationEventRecordReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoQuickRegistrationEventRecordReq.java deleted file mode 100644 index 96d7dc6..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoQuickRegistrationEventRecordReq.java +++ /dev/null @@ -1,27 +0,0 @@ -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 attachmentIdList = new ArrayList<>(); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventPeriodStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventPeriodStatView.java deleted file mode 100644 index c461e9f..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventPeriodStatView.java +++ /dev/null @@ -1,21 +0,0 @@ -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; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordCreateReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordCreateReq.java deleted file mode 100644 index 72d8b81..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordCreateReq.java +++ /dev/null @@ -1,30 +0,0 @@ -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 List customerIdList = new ArrayList<>(); - - /// 登记事件 ID - private String registrationEventId; - - /// 备注 - private String remark; - - /// 登记时间 - private Long registeredAt; - - /// 临时照片附件 ID 列表 - private List attachmentIdList = new ArrayList<>(); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordListItemView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordListItemView.java deleted file mode 100644 index 5bca512..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordListItemView.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.imyeyu.api.modules.gao.vo; - -import lombok.Data; - -/// -/// 登记事件记录列表项视图 -/// -/// @author Codex -/// @since 2026-08-01 -@Data -public class GaoRegistrationEventRecordListItemView { - - /// 记录 ID - private String id; - - /// 客户 ID - private String customerId; - - /// 客户姓名 - private String customerName; - - /// 客户头像附件 ID - private String customerPhotoAttachmentId; - - /// 登记事件 ID - private String registrationEventId; - - /// 登记事件名称 - private String registrationEventName; - - /// 登记时间 - private Long registeredAt; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordPeriodReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordPeriodReq.java deleted file mode 100644 index 5fe8e2a..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordPeriodReq.java +++ /dev/null @@ -1,35 +0,0 @@ -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; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordStatReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordStatReq.java deleted file mode 100644 index 51214bf..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordStatReq.java +++ /dev/null @@ -1,21 +0,0 @@ -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; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordTrendStatReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordTrendStatReq.java deleted file mode 100644 index b83be89..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoRegistrationEventRecordTrendStatReq.java +++ /dev/null @@ -1,24 +0,0 @@ -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; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStoreChartReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStoreChartReq.java new file mode 100644 index 0000000..f3c343a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStoreChartReq.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.util.List; + +/// 门店报表查询请求 +/// +/// @author Codex +/// @since 2026-08-11 +@Data +public class GaoStoreChartReq { + + /// 门店 ID + private String storeId; + + /// 开始时间 + private Long beginAt; + + /// 结束时间 + private Long endAt; + + /// 登记事件 ID 列表 + private List eventIdList; +} diff --git a/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java index 5e849c8..af0a835 100644 --- a/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/system/service/implement/LoggerServiceImplement.java @@ -10,6 +10,7 @@ import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** @@ -18,6 +19,7 @@ import org.springframework.stereotype.Service; * @author 夜雨 * @since 2026-05-13 14:46 */ +@Slf4j @Service @RequiredArgsConstructor public class LoggerServiceImplement extends AbstractEntityService implements LoggerService { @@ -34,18 +36,13 @@ public class LoggerServiceImplement extends AbstractEntityService createSync(logger)); } @Override public void createSync(Logger logger) { - if (TimiJava.isEmpty(logger.getUserId())) { - User loginUser = userLoginService.getLoginUser(); - if (TimiJava.isNotEmpty(loginUser)) { - logger.setUserId(loginUser.getId()); - } - } - logger.setIp(TimiJava.defaultIfEmpty(logger.getIp(), TimiSpring.getRequestIP())); + fillContext(logger); super.create(logger); } @@ -55,4 +52,18 @@ public class LoggerServiceImplement extends AbstractEntityService list(@RequestBody Page page) { return service.page(page); } + @RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ) + @PostMapping("/grantable/list") + public List listGrantable(@RequestParam ModuleCode moduleCode) { + return authorizationScopeService.listGrantablePermission(userLoginService.getRequireLoginUserId(), moduleCode); + } + @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.PERMISSION_READ) @PostMapping("/create") public void create(@RequestBody Permission permission) { @@ -49,7 +57,6 @@ public class PermissionController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.PERMISSION_CREATE) @PostMapping("/create/batch") public void createBatch(@RequestBody BatchCreateReq req) { @@ -57,7 +64,6 @@ public class PermissionController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.PERMISSION_UPDATE) @PostMapping("/update") public void update(@RequestBody Permission permission) { @@ -65,7 +71,6 @@ public class PermissionController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.PERMISSION_DELETE) @PostMapping("/delete") public void delete(@RequestParam("id") String id) { diff --git a/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java b/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java index fc882d9..d0f09f6 100644 --- a/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java +++ b/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java @@ -1,17 +1,19 @@ package com.imyeyu.api.modules.user.controller; import com.imyeyu.api.annotation.RequireCorePermission; -import com.imyeyu.api.annotation.RequireCoreRole; import com.imyeyu.api.bean.CorePermissionCode; -import com.imyeyu.api.bean.CoreRoleCode; import com.imyeyu.api.bean.ModuleCode; -import com.imyeyu.api.bean.RoleMatchMode; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.service.PermissionService; import com.imyeyu.api.modules.user.service.RoleService; -import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeReq; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.api.modules.user.service.UserRoleService; +import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeRequest; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequiredToken; import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.PageResult; import lombok.RequiredArgsConstructor; @@ -22,7 +24,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * 角色 SDK 控制器 @@ -37,30 +42,53 @@ import java.util.List; public class RoleController { private final RoleService service; + private final UserRoleService userRoleService; + private final UserLoginService userLoginService; private final PermissionService permissionService; - @RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY) @RequireCorePermission(CorePermissionCode.ROLE_READ) @PostMapping("/list") public PageResult list(@RequestBody Page page) { + Set manageableRoleIdSet = listManageableRoleId(); PageResult result = service.page(page); if (result.getList() != null) { + result.setList(result.getList().stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList()); result.getList().forEach(this::fillRoleDetail); } return result; } - @RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY) + @RequireCorePermission(CorePermissionCode.ROLE_READ) + @PostMapping("/manageable/list") + public List listManageableRole(@RequestParam ModuleCode moduleCode) { + return service.listManageableRole(moduleCode); + } + + @RequireCorePermission(CorePermissionCode.USER_ROLE_READ) + @PostMapping("/grantable/list") + public List listGrantableRole(@RequestParam ModuleCode moduleCode) { + return service.listGrantableRole(moduleCode); + } + + @RequiredToken + @PostMapping("/current/detail/list") + public List listCurrentRoleDetail(@RequestParam(required = false) ModuleCode moduleCode) { + String loginUserId = userLoginService.getRequireLoginUserId(); + List roleList = moduleCode == null ? userRoleService.listRoleByUserId(loginUserId) : userRoleService.listRoleByUserId(moduleCode, loginUserId); + roleList.forEach(this::fillRoleDetail); + return roleList; + } + @RequireCorePermission(CorePermissionCode.ROLE_READ) @PostMapping("/detail") public Role detail(@RequestParam String id) { Role role = service.get(id); + requireManageableRole(role.getId()); fillRoleDetail(role); return role; } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.ROLE_CREATE) @PostMapping("/create") public void create(@RequestBody Role role) { @@ -68,49 +96,110 @@ public class RoleController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.ROLE_UPDATE) @PostMapping("/update") public void update(@RequestBody Role role) { service.update(role); } + @RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ) + @PostMapping("/permission/grantable/list") + public List listGrantableRolePermission(@RequestParam String roleId) { + requireManageableRole(roleId); + return service.listGrantableRolePermission(roleId); + } + + @RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ) + @PostMapping("/delegation/list") + public List listDelegationPermission(@RequestParam String roleId) { + requireManageableRole(roleId); + return service.listDelegationPermissionByRoleId(roleId); + } + + @RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ) + @PostMapping("/delegation/grantable/list") + public List listGrantableDelegationPermission(@RequestParam String roleId) { + requireManageableRole(roleId); + return service.listGrantableDelegationPermission(roleId); + } + + @AOPLog + @RequireCorePermission(value = {CorePermissionCode.ROLE_PERMISSION_CREATE, CorePermissionCode.ROLE_PERMISSION_DELETE}) + @PostMapping("/delegation/update") + public void updateRoleDelegation(@RequestBody Role role) { + service.updateRoleDelegation(role); + } + + @RequireCorePermission(CorePermissionCode.ROLE_READ) + @PostMapping("/parent/grantable/list") + public List listGrantableParentRole(@RequestParam String roleId) { + requireManageableRole(roleId); + return service.listGrantableParentRole(roleId); + } + + @AOPLog + @RequireCorePermission(CorePermissionCode.ROLE_UPDATE) + @PostMapping("/parent/update") + public void updateRoleParent(@RequestBody Role role) { + service.updateRoleParent(role); + } + @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequireCorePermission(CorePermissionCode.ROLE_DELETE) @PostMapping("/delete") public void delete(@RequestParam String id) { service.delete(id); } - @RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY) @RequireCorePermission(CorePermissionCode.USER_ROLE_READ) @PostMapping("/authorized/list") - public List listUserRole(@RequestParam String userId, @RequestParam(required = false) ModuleCode moduleCode) { + public List listUserRole(@RequestParam(required = false) ModuleCode moduleCode, @RequestParam String userId) { + Set manageableRoleIdSet = listManageableRoleId(); if (moduleCode != null) { - return service.listByUserId(userId, moduleCode); + return userRoleService.listRoleByUserId(moduleCode, userId).stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList(); } - return service.listByUserId(userId); + return userRoleService.listRoleByUserId(userId).stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList(); } @AOPLog - @RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY) @RequireCorePermission(value = {CorePermissionCode.USER_ROLE_CREATE, CorePermissionCode.USER_ROLE_DELETE}) @PostMapping("/authorized/create") - public void authorizeUserRole(@RequestBody UserRoleAuthorizeReq req) { - service.authorizeUserRole(req); + public void authorizeUserRole(@RequestBody UserRoleAuthorizeRequest req) { + userRoleService.authorizeUserRole(req); } - @RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY) @RequireCorePermission(CorePermissionCode.USER_ROLE_READ) @PostMapping("/authorized/permission") public List listAuthorizedPermission(@RequestParam String userId) { - return service.listAllPermissionByUserId(userId); + Set manageableRoleIdSet = listManageableRoleId(); + List roleIdList = userRoleService.listRoleByUserId(userId) + .stream() + .filter(role -> manageableRoleIdSet.contains(role.getId())) + .map(Role::getId) + .toList(); + return roleIdList.isEmpty() ? List.of() : permissionService.listByRoleId(service.listAllRoleIdIncludingChildren(roleIdList)); } private void fillRoleDetail(Role role) { role.setPermissionList(permissionService.listByRoleId(role.getId())); role.setAllPermissionList(service.listPermissionByRoleId(role.getId())); role.setChildRoleList(service.listChildByParentRoleId(role.getId())); + if (role.getParentRoleId() != null) { + role.setParentRole(service.get(role.getParentRoleId())); + } + role.setDelegationPermissionList(service.listDelegationPermissionByRoleId(role.getId())); + } + + private Set listManageableRoleId() { + return Arrays.stream(ModuleCode.values()) + .flatMap(moduleCode -> service.listManageableRole(moduleCode).stream()) + .map(Role::getId) + .collect(Collectors.toSet()); + } + + private void requireManageableRole(String roleId) { + if (!listManageableRoleId().contains(roleId)) { + throw new TimiException(TimiCode.PERMISSION_ERROR); + } } } diff --git a/src/main/java/com/imyeyu/api/modules/user/controller/UserController.java b/src/main/java/com/imyeyu/api/modules/user/controller/UserController.java index 5ff2101..a38226e 100644 --- a/src/main/java/com/imyeyu/api/modules/user/controller/UserController.java +++ b/src/main/java/com/imyeyu/api/modules/user/controller/UserController.java @@ -2,12 +2,9 @@ package com.imyeyu.api.modules.user.controller; import com.fasterxml.jackson.annotation.JsonView; import com.imyeyu.api.annotation.RequireCorePermission; -import com.imyeyu.api.annotation.RequireCoreRole; import com.imyeyu.api.bean.CorePermissionCode; -import com.imyeyu.api.bean.CoreRoleCode; 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.user.entity.User; import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserService; @@ -17,6 +14,7 @@ import com.imyeyu.api.modules.user.vo.LoginResponse; import com.imyeyu.api.modules.user.vo.PhoneVerifyCodeSendRequest; import com.imyeyu.api.modules.user.vo.PhoneVerifyRequest; import com.imyeyu.api.modules.user.vo.UpdatePasswordRequest; +import com.imyeyu.java.TimiJava; import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.CaptchaValid; @@ -56,7 +54,6 @@ public class UserController { private final UserLoginService userLoginService; private final AttachmentService attachmentService; - @RequireCoreRole(CoreRoleCode.ADMIN) @RequestRateLimit @RequireCorePermission(CorePermissionCode.USER_READ) @PostMapping("/list") @@ -65,7 +62,6 @@ public class UserController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequestRateLimit @RequireCorePermission(CorePermissionCode.USER_READ) @RequestMapping("/detail") @@ -74,7 +70,6 @@ public class UserController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequestRateLimit @RequireCorePermission(CorePermissionCode.USER_CREATE) @PostMapping("/create") @@ -83,7 +78,6 @@ public class UserController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequestRateLimit @RequireCorePermission(CorePermissionCode.USER_UPDATE) @PostMapping("/update") @@ -92,7 +86,6 @@ public class UserController { } @AOPLog - @RequireCoreRole(CoreRoleCode.ADMIN) @RequestRateLimit @RequestMapping("/delete") public void delete(@RequestParam String id) { @@ -125,12 +118,14 @@ public class UserController { @AOPLog @JsonView(ResponseView.Public.class) @RequestRateLimit + @CaptchaValid @PostMapping("/login") public LoginResponse login(@RequestBody @Valid CaptchaData req) { return userLoginService.login(req.getData()); } @AOPLog + @JsonView(ResponseView.Public.class) @RequiredToken @RequestRateLimit @RequestMapping("/login/token") @@ -141,37 +136,24 @@ public class UserController { /** * 更新当前用户资料 * - * @param req 用户资料 + * @param user 用户资料 * @return 最新登录用户 */ @AOPLog @RequiredToken @RequestRateLimit - @RequireCorePermission(CorePermissionCode.USER_UPDATE) @PostMapping("/current/update") - public LoginResponse updateCurrent(@RequestBody User req) { + public LoginResponse updateCurrent(@RequestBody User user) { User loginUser = userLoginService.getRequireLoginUser(); - User user = userService.get(loginUser.getId()); - if (req.getName() != null) { - user.setName(req.getName()); - } - if (req.getNick() != null) { - user.setNick(req.getNick()); - } - if (req.getGender() != null) { - user.setGender(req.getGender()); - } - if (req.getDescription() != null) { - user.setDescription(req.getDescription()); - } - if (req.getAvatarType() != null) { - user.setAvatarType(req.getAvatarType()); - } - if (req.getWrapperType() != null) { - user.setWrapperType(req.getWrapperType()); - } - userService.update(user); - updateCurrentAvatar(loginUser.getId(), req.getTempFileIdList()); + User dbUser = userService.get(loginUser.getId()); + dbUser.setName(TimiJava.defaultIfEmpty(user.getName(), dbUser.getName())); + dbUser.setNick(TimiJava.defaultIfEmpty(user.getNick(), dbUser.getNick())); + dbUser.setGender(TimiJava.defaultIfNull(user.getGender(), dbUser.getGender())); + dbUser.setDescription(user.getDescription()); + dbUser.setAvatarType(TimiJava.defaultIfNull(user.getAvatarType(), dbUser.getAvatarType())); + dbUser.setWrapperType(TimiJava.defaultIfNull(user.getAvatarType(), dbUser.getWrapperType())); + userService.update(dbUser); + updateCurrentAvatar(loginUser.getId(), user.getTempFileIdList()); return userLoginService.login(TimiSpring.getToken(), loginUser.getId()); } @@ -245,9 +227,6 @@ public class UserController { if (tempFileIdList == null || tempFileIdList.isEmpty()) { return; } - BizUpdateReq req = new BizUpdateReq(); - req.setBizType(Attachment.BizType.USER); - req.setBizId(userId); List items = attachmentService.listByBizId(Attachment.BizType.USER, userId).stream() .filter(item -> !User.AttachType.AVATAR.name().equals(item.getAttachType())) .map(item -> { @@ -264,7 +243,6 @@ public class UserController { }).toList(); items = new ArrayList<>(items); items.addAll(avatarItems); - req.setItems(items); - attachmentService.updateByBiz(req); + attachmentService.updateByBizId(Attachment.BizType.USER, userId, items); } } diff --git a/src/main/java/com/imyeyu/api/modules/user/entity/Permission.java b/src/main/java/com/imyeyu/api/modules/user/entity/Permission.java index f7935f0..5079827 100644 --- a/src/main/java/com/imyeyu/api/modules/user/entity/Permission.java +++ b/src/main/java/com/imyeyu/api/modules/user/entity/Permission.java @@ -2,6 +2,7 @@ package com.imyeyu.api.modules.user.entity; import com.imyeyu.api.annotation.MultilingualField; import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; @@ -29,6 +30,21 @@ public class Permission extends UUIDEntity { /** 说明 */ private String description; + /// true 为系统内置权限 + private Boolean builtin; + + /// true 为受保护权限 + private Boolean protectedPermission; + + /// 归属类型 + private AuthOwnerType ownerType; + + /// 归属 ID + private String ownerId; + + /// 创建人用户 ID + private String createdBy; + @Transient @MultilingualField("nameLangId") private String name; diff --git a/src/main/java/com/imyeyu/api/modules/user/entity/Role.java b/src/main/java/com/imyeyu/api/modules/user/entity/Role.java index abc40a1..bdfa982 100644 --- a/src/main/java/com/imyeyu/api/modules/user/entity/Role.java +++ b/src/main/java/com/imyeyu/api/modules/user/entity/Role.java @@ -2,6 +2,7 @@ package com.imyeyu.api.modules.user.entity; import com.imyeyu.api.annotation.MultilingualField; import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; @@ -32,6 +33,24 @@ public class Role extends UUIDEntity { /** 说明 */ private String description; + /// true 为系统内置角色 + private Boolean builtin; + + /// true 为受保护角色 + private Boolean protectedRole; + + /// 管理父角色 ID + private String parentRoleId; + + /// 归属类型 + private AuthOwnerType ownerType; + + /// 归属 ID + private String ownerId; + + /// 创建人用户 ID + private String createdBy; + @Transient @MultilingualField("nameLangId") private String name; @@ -48,6 +67,14 @@ public class Role extends UUIDEntity { @Transient protected List childRoleList; + /// 管理父角色 + @Transient + protected Role parentRole; + + /// 角色允许继续转授的权限列表 + @Transient + protected List delegationPermissionList; + /** 权限 ID 列表 */ @Transient protected Set permissionIdList; @@ -55,4 +82,8 @@ public class Role extends UUIDEntity { /** 子角色 ID 列表 */ @Transient protected Set childRoleIdList; + + /// 转授权限 ID 列表 + @Transient + protected Set delegationPermissionIdList; } diff --git a/src/main/java/com/imyeyu/api/modules/user/entity/RolePermissionDelegation.java b/src/main/java/com/imyeyu/api/modules/user/entity/RolePermissionDelegation.java new file mode 100644 index 0000000..141b5fa --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/entity/RolePermissionDelegation.java @@ -0,0 +1,45 @@ +package com.imyeyu.api.modules.user.entity; + +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.DeleteColumn; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.Deletable; +import com.imyeyu.spring.entity.IDEntity; +import lombok.Data; + +/// 角色转授权限 +/// +/// @author Codex +/// @since 2026-08-08 +@Data +public class RolePermissionDelegation implements IDEntity, Creatable, Deletable { + + /// 主键 + @Id + @AutoUUID + protected String id; + + /// 角色 ID + protected String roleId; + + /// 权限 ID + protected String permissionId; + + /// 创建人用户 ID + protected String createdBy; + + /// 创建时间 + protected Long createdAt; + + /// 删除时间 + @DeleteColumn + protected Long deletedAt; + + @Transient + protected Role role; + + @Transient + protected Permission permission; +} diff --git a/src/main/java/com/imyeyu/api/modules/user/entity/RoleRelation.java b/src/main/java/com/imyeyu/api/modules/user/entity/RolePermissionInherit.java similarity index 53% rename from src/main/java/com/imyeyu/api/modules/user/entity/RoleRelation.java rename to src/main/java/com/imyeyu/api/modules/user/entity/RolePermissionInherit.java index 2073fa9..a61be41 100644 --- a/src/main/java/com/imyeyu/api/modules/user/entity/RoleRelation.java +++ b/src/main/java/com/imyeyu/api/modules/user/entity/RolePermissionInherit.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.user.entity; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; import com.imyeyu.spring.annotation.table.AutoUUID; import com.imyeyu.spring.annotation.table.DeleteColumn; import com.imyeyu.spring.annotation.table.Id; @@ -8,30 +9,37 @@ import com.imyeyu.spring.entity.Deletable; import com.imyeyu.spring.entity.IDEntity; import lombok.Data; -/** - * 角色关联 - * - * @author 夜雨 - * @since 2026-05-13 14:40 - */ +/// 角色权限继承 +/// +/// @author 夜雨 +/// @since 2026-05-13 14:40 @Data -public class RoleRelation implements IDEntity, Creatable, Deletable { +public class RolePermissionInherit implements IDEntity, Creatable, Deletable { - /** 主键 */ + /// 主键 @Id @AutoUUID private String id; - /** 上级角色 ID */ + /// 继承方角色 ID private String parentRoleId; - /** 子级角色 ID */ + /// 被继承角色 ID private String childRoleId; - /** 创建时间 */ + /// 归属类型 + private AuthOwnerType ownerType; + + /// 归属 ID + private String ownerId; + + /// 创建人用户 ID + private String createdBy; + + /// 创建时间 private Long createdAt; - /** 删除时间 */ + /// 删除时间 @DeleteColumn private Long deletedAt; } diff --git a/src/main/java/com/imyeyu/api/modules/user/entity/User.java b/src/main/java/com/imyeyu/api/modules/user/entity/User.java index 2b7fe33..21095c9 100644 --- a/src/main/java/com/imyeyu/api/modules/user/entity/User.java +++ b/src/main/java/com/imyeyu/api/modules/user/entity/User.java @@ -8,6 +8,7 @@ import com.imyeyu.api.modules.user.bean.Gender; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.util.ResponseView; +import com.imyeyu.utils.Regex; import com.imyeyu.utils.Time; import lombok.Data; import lombok.EqualsAndHashCode; @@ -104,7 +105,7 @@ public class User extends UUIDEntity { protected String lastLoginIP; /** 上次登录时间 */ - @JsonView(ResponseView.Admin.class) + @JsonView(ResponseView.Public.class) protected Long lastLoginAt; /** 解除禁言时间 */ @@ -123,6 +124,10 @@ public class User extends UUIDEntity { @JsonView(ResponseView.Public.class) protected List permissionList; + @Transient + @JsonView(ResponseView.Public.class) + protected List roleEntityList; + @Transient @JsonView(ResponseView.Public.class) protected List attachmentList; @@ -135,6 +140,10 @@ public class User extends UUIDEntity { @JsonView(ResponseView.Public.class) protected List tempFileIdList; + public boolean isValidPhone() { + return Regex.isMatch(Regex.MOBILE_PHONE, phoneNo); + } + /** @return true 为禁言中 */ public boolean isMuting() { return unmuteAt != null && Time.now() < unmuteAt; diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/PermissionMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/PermissionMapper.java index 769f38f..692f9c0 100644 --- a/src/main/java/com/imyeyu/api/modules/user/mapper/PermissionMapper.java +++ b/src/main/java/com/imyeyu/api/modules/user/mapper/PermissionMapper.java @@ -2,6 +2,8 @@ package com.imyeyu.api.modules.user.mapper; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.api.bean.ModuleCode; +import org.apache.ibatis.annotations.Param; import java.util.List; @@ -13,5 +15,7 @@ import java.util.List; */ public interface PermissionMapper extends BaseMapper { - List selectByIdList(List list); + List selectByIdList(@Param("idList") List idList); + + List selectByModuleCode(@Param("moduleCode") ModuleCode moduleCode); } diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/RoleMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/RoleMapper.java index 459e5f4..505f517 100644 --- a/src/main/java/com/imyeyu/api/modules/user/mapper/RoleMapper.java +++ b/src/main/java/com/imyeyu/api/modules/user/mapper/RoleMapper.java @@ -5,6 +5,7 @@ import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.spring.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; +import java.util.Collection; import java.util.List; /** @@ -15,9 +16,13 @@ import java.util.List; */ public interface RoleMapper extends BaseMapper { - List selectByIdList(List idList); + Role selectById(String id); - List selectByModuleCodeAndCodeList(@Param("moduleCode") ModuleCode moduleCode, @Param("codeList") List codeList); + List selectByIdList(@Param("idList") Collection idList); List selectByModuleCode(@Param("moduleCode") ModuleCode moduleCode); + + List selectByParentRoleId(@Param("parentRoleId") String parentRoleId); + + List selectAll(); } diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionDelegationMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionDelegationMapper.java new file mode 100644 index 0000000..5dd8f19 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionDelegationMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.user.mapper; + +import com.imyeyu.api.modules.user.entity.Permission; +import com.imyeyu.api.modules.user.entity.RolePermissionDelegation; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// 角色转授权限 Mapper +/// +/// @author Codex +/// @since 2026-08-08 +public interface RolePermissionDelegationMapper extends BaseMapper { + + default List selectByRoleId(String roleId) { + return selectByRoleIdList(List.of(roleId)); + } + + List selectByRoleIdList(@Param("roleIdList") List roleIdList); + + List selectPermissionByRoleIdList(@Param("roleIdList") List roleIdList); + + void createBatch(@Param("rolePermissionDelegationList") List rolePermissionDelegationList); + + void deleteByPermissionIdList(@Param("roleId") String roleId, @Param("permissionIdList") List permissionIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionInheritMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionInheritMapper.java new file mode 100644 index 0000000..f51f429 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionInheritMapper.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.user.mapper; + +import com.imyeyu.api.modules.user.bean.AuthOwnerType; +import com.imyeyu.api.modules.user.entity.RolePermissionInherit; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// 角色权限继承 Mapper +/// +/// @author 夜雨 +/// @since 2026-05-13 14:46 +public interface RolePermissionInheritMapper extends BaseMapper { + + List selectByParentRoleId(@Param("parentRoleId") String parentRoleId); + + List selectChildRoleIdListByParentRoleId(@Param("parentRoleId") String parentRoleId); + + List selectByParentRoleIdList(@Param("parentRoleIdList") List parentRoleIdList); + + List selectChildRoleIdListByParentRoleIdList(@Param("parentRoleIdList") List parentRoleIdList); + + void createBatch(@Param("rolePermissionInheritList") List rolePermissionInheritList); + + void deleteByChildRoleIdList( + @Param("parentRoleId") String parentRoleId, + @Param("childRoleIdList") List childRoleIdList, + @Param("ownerType") AuthOwnerType ownerType, + @Param("ownerId") String ownerId + ); +} diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionMapper.java index f9e28d0..94b0536 100644 --- a/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionMapper.java +++ b/src/main/java/com/imyeyu/api/modules/user/mapper/RolePermissionMapper.java @@ -3,6 +3,7 @@ package com.imyeyu.api.modules.user.mapper; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.RolePermission; import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; import java.util.List; @@ -18,11 +19,11 @@ public interface RolePermissionMapper extends BaseMapper return selectByRoleIdList(List.of(roleId)); } - List selectByRoleIdList(List roleIdList); + List selectByRoleIdList(@Param("roleIdList") List roleIdList); - List selectPermissionByRoleIdList(List roleIdList); + List selectPermissionByRoleIdList(@Param("roleIdList") List roleIdList); - void createBatch(List rolePermissionList); + void createBatch(@Param("rolePermissionList") List rolePermissionList); - void deleteByPermissionIdList(String roleId, List permissionIdList); + void deleteByPermissionIdList(@Param("roleId") String roleId, @Param("permissionIdList") List permissionIdList); } diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/RoleRelationMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/RoleRelationMapper.java deleted file mode 100644 index 546f55b..0000000 --- a/src/main/java/com/imyeyu/api/modules/user/mapper/RoleRelationMapper.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.imyeyu.api.modules.user.mapper; - -import com.imyeyu.api.modules.user.entity.RoleRelation; -import com.imyeyu.spring.mapper.BaseMapper; - -import java.util.List; - -/** - * 角色关联 Mapper - * - * @author 夜雨 - * @since 2026-05-13 14:46 - */ -public interface RoleRelationMapper extends BaseMapper { - - List selectByParentRoleId(String parentRoleId); - - List selectChildRoleIdListByParentRoleId(String parentRoleId); - - List selectChildRoleIdListByParentRoleIdList(List parentRoleIdList); - - void createBatch(List roleRelationList); - - void deleteByChildRoleIdList(String parentRoleId, List childRoleIdList); -} diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/UserMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/UserMapper.java index feefe9f..eb62db4 100644 --- a/src/main/java/com/imyeyu/api/modules/user/mapper/UserMapper.java +++ b/src/main/java/com/imyeyu/api/modules/user/mapper/UserMapper.java @@ -4,6 +4,7 @@ import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.spring.mapper.BaseMapper; import org.apache.ibatis.annotations.Select; +import java.util.Collection; import java.util.List; /** @@ -32,5 +33,5 @@ public interface UserMapper extends BaseMapper { * @param idList 用户 ID 列表 * @return 用户列表 */ - List listByIdList(List idList); + List listByIdList(Collection idList); } diff --git a/src/main/java/com/imyeyu/api/modules/user/mapper/UserRoleMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/UserRoleMapper.java index 729e0fc..704c6ff 100644 --- a/src/main/java/com/imyeyu/api/modules/user/mapper/UserRoleMapper.java +++ b/src/main/java/com/imyeyu/api/modules/user/mapper/UserRoleMapper.java @@ -2,7 +2,9 @@ package com.imyeyu.api.modules.user.mapper; import com.imyeyu.api.modules.user.entity.UserRole; import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; +import java.util.Collection; import java.util.List; /** @@ -15,5 +17,7 @@ public interface UserRoleMapper extends BaseMapper { void createBatch(List userRoleList); - void deleteByRoleIdList(String userId, List roleIdList); + List selectByUserIdList(@Param("userIdList") Collection userIdList); + + void deleteByRoleIdList(@Param("userId") String userId, @Param("roleIdList") List roleIdList); } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/AuthorizationBoundaryService.java b/src/main/java/com/imyeyu/api/modules/user/service/AuthorizationBoundaryService.java new file mode 100644 index 0000000..a02fb39 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/AuthorizationBoundaryService.java @@ -0,0 +1,79 @@ +package com.imyeyu.api.modules.user.service; + +import com.imyeyu.api.modules.user.entity.Role; + +import java.util.Collection; + +/// 授权边界服务 +/// +/// @author Codex +/// @since 2026-08-07 +public interface AuthorizationBoundaryService { + + /// 校验是否可以授予用户角色 + /// + /// @param operatorUserId 操作者用户 ID + /// @param targetUserId 目标用户 ID + /// @param roleIdList 角色 ID 列表 + void checkCanGrantRoles(String operatorUserId, String targetUserId, Collection roleIdList); + + /// 校验是否可以移除用户角色 + /// + /// @param operatorUserId 操作者用户 ID + /// @param targetUserId 目标用户 ID + /// @param roleIdList 角色 ID 列表 + void checkCanRevokeRoles(String operatorUserId, String targetUserId, Collection roleIdList); + + /// 校验是否可以创建角色 + /// + /// @param operatorUserId 操作者用户 ID + /// @param role 角色 + void checkCanCreateRole(String operatorUserId, Role role); + + /// 校验是否可以修改角色基础信息 + /// + /// @param operatorUserId 操作者用户 ID + /// @param role 角色 + void checkCanUpdateRole(String operatorUserId, Role role); + + /// 校验是否可以删除角色 + /// + /// @param operatorUserId 操作者用户 ID + /// @param roleId 角色 ID + void checkCanDeleteRole(String operatorUserId, String roleId); + + /// 校验是否可以授予角色权限 + /// + /// @param operatorUserId 操作者用户 ID + /// @param roleId 角色 ID + /// @param permissionIdList 权限 ID 列表 + void checkCanGrantPermissions(String operatorUserId, String roleId, Collection permissionIdList); + + /// 校验是否可以移除角色权限 + /// + /// @param operatorUserId 操作者用户 ID + /// @param roleId 角色 ID + /// @param permissionIdList 权限 ID 列表 + void checkCanRevokePermissions(String operatorUserId, String roleId, Collection permissionIdList); + + /// 校验是否可以修改角色转授权限 + /// + /// @param operatorUserId 操作者用户 ID + /// @param roleId 角色 ID + /// @param permissionIdList 转授权限 ID 列表 + void checkCanUpdateRoleDelegation(String operatorUserId, String roleId, Collection permissionIdList); + + /// 校验是否可以更新角色管理父级 + /// + /// @param operatorUserId 操作者用户 ID + /// @param roleId 角色 ID + /// @param parentRoleId 管理父角色 ID + void checkCanUpdateRoleParent(String operatorUserId, String roleId, String parentRoleId); + + /// 校验是否可以更新角色继承关系 + /// + /// @param operatorUserId 操作者用户 ID + /// @param parentRoleId 父角色 ID + /// @param childRoleIdList 子角色 ID 列表 + void checkCanUpdateRolePermissionInherits(String operatorUserId, String parentRoleId, Collection childRoleIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/AuthorizationScopeService.java b/src/main/java/com/imyeyu/api/modules/user/service/AuthorizationScopeService.java new file mode 100644 index 0000000..614b3f5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/AuthorizationScopeService.java @@ -0,0 +1,42 @@ +package com.imyeyu.api.modules.user.service; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.modules.user.entity.Permission; +import com.imyeyu.api.modules.user.entity.Role; + +import java.util.List; + +/// 授权范围服务 +/// +/// @author Codex +/// @since 2026-08-07 +public interface AuthorizationScopeService { + + /// 查询可授权角色 + /// + /// @param operatorUserId 操作者用户 ID + /// @param moduleCode 模块代码 + /// @return 角色列表 + List listGrantableRole(String operatorUserId, ModuleCode moduleCode); + + /// 查询可授权权限 + /// + /// @param operatorUserId 操作者用户 ID + /// @param moduleCode 模块代码 + /// @return 权限列表 + List listGrantablePermission(String operatorUserId, ModuleCode moduleCode); + + /// 查询可管理角色 + /// + /// @param operatorUserId 操作者用户 ID + /// @param moduleCode 模块代码 + /// @return 角色列表 + List listManageableRole(String operatorUserId, ModuleCode moduleCode); + + /// 判断是否为系统管理员 + /// + /// @param operatorUserId 操作者用户 ID + /// @return true 为系统管理员 + boolean isSystemAdmin(String operatorUserId); + +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/ModuleResourceScopeResolver.java b/src/main/java/com/imyeyu/api/modules/user/service/ModuleResourceScopeResolver.java new file mode 100644 index 0000000..907be80 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/ModuleResourceScopeResolver.java @@ -0,0 +1,39 @@ +package com.imyeyu.api.modules.user.service; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; +import com.imyeyu.api.modules.user.entity.Role; + +/// 模块资源范围解析器 +/// +/// @author Codex +/// @since 2026-08-07 +public interface ModuleResourceScopeResolver { + + /// 模块代码 + /// + /// @return 模块代码 + ModuleCode moduleCode(); + + /// 判断是否可以管理目标用户 + /// + /// @param operatorUserId 操作者用户 ID + /// @param targetUserId 目标用户 ID + /// @return true 为可以管理 + boolean canManageUser(String operatorUserId, String targetUserId); + + /// 判断是否可以管理角色 + /// + /// @param operatorUserId 操作者用户 ID + /// @param role 角色 + /// @return true 为可以管理 + boolean canManageRole(String operatorUserId, Role role); + + /// 判断是否可以使用指定归属范围 + /// + /// @param operatorUserId 操作者用户 ID + /// @param ownerType 归属类型 + /// @param ownerId 归属 ID + /// @return true 为可以使用 + boolean canUseOwnerScope(String operatorUserId, AuthOwnerType ownerType, String ownerId); +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/PermissionService.java b/src/main/java/com/imyeyu/api/modules/user/service/PermissionService.java index 3dac03e..e35d792 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/PermissionService.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/PermissionService.java @@ -2,6 +2,7 @@ package com.imyeyu.api.modules.user.service; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.vo.permission.BatchCreateReq; +import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.spring.service.BaseService; import java.util.List; @@ -30,4 +31,10 @@ public interface PermissionService extends BaseService { /// @param roleIdList 角色 ID 列表 /// @return 权限列表 List listByRoleId(List roleIdList); + + /// 查询模块权限 + /// + /// @param moduleCode 模块代码 + /// @return 权限列表 + List listByModuleCode(ModuleCode moduleCode); } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/RoleHierarchyService.java b/src/main/java/com/imyeyu/api/modules/user/service/RoleHierarchyService.java new file mode 100644 index 0000000..b076674 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/RoleHierarchyService.java @@ -0,0 +1,39 @@ +package com.imyeyu.api.modules.user.service; + +import java.util.Collection; +import java.util.List; + +/// 角色继承服务 +/// +/// @author Codex +/// @since 2026-08-07 +public interface RoleHierarchyService { + + /// 递归查询角色及其系统、模块作用域内的所有子角色 ID + /// + /// 此方法不依赖用户资源范围,不会展开门店、用户等需要操作者上下文的作用域关系 + /// + /// @param roleIdList 角色 ID 列表 + /// @return 角色 ID 列表 + List listAllRoleIdIncludingChildren(Collection roleIdList); + + /// 递归查询用户作用域内的角色及其所有子角色 ID + /// + /// @param userId 用户 ID + /// @param roleIdList 角色 ID 列表 + /// @return 角色 ID 列表 + List listAllRoleIdIncludingChildren(String userId, Collection roleIdList); + + /// 校验角色继承关系不存在环 + /// + /// @param parentRoleId 父角色 ID + /// @param childRoleIdList 子角色 ID 列表 + void checkNoCycle(String parentRoleId, Collection childRoleIdList); + + /// 判断目标角色是否为子孙角色 + /// + /// @param parentRoleId 父角色 ID + /// @param childRoleId 子角色 ID + /// @return true 为子孙角色 + boolean isDescendant(String parentRoleId, String childRoleId); +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/RolePermissionInheritService.java b/src/main/java/com/imyeyu/api/modules/user/service/RolePermissionInheritService.java new file mode 100644 index 0000000..a68614a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/RolePermissionInheritService.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.user.service; + +import com.imyeyu.api.modules.user.entity.RolePermissionInherit; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// 角色权限继承服务 +/// +/// @author 夜雨 +/// @since 2026-05-13 14:46 +public interface RolePermissionInheritService extends BaseService { + + /// 查询直属权限继承 + /// + /// @param parentRoleId 继承方角色 ID + /// @return 权限继承列表 + List listByParentRoleId(String parentRoleId); + + /// 查询直属被继承角色 ID 列表 + /// + /// @param parentRoleId 继承方角色 ID + /// @return 被继承角色 ID 列表 + List listChildRoleIdByParentRoleId(String parentRoleId); + + /// 递归展开全部被继承角色 ID + /// + /// @param parentRoleIdList 继承方角色 ID 列表 + /// @return 全部被继承角色 ID 列表 + List listAllChildRoleIdByParentRoleIdList(List parentRoleIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/RoleRelationService.java b/src/main/java/com/imyeyu/api/modules/user/service/RoleRelationService.java deleted file mode 100644 index 199adba..0000000 --- a/src/main/java/com/imyeyu/api/modules/user/service/RoleRelationService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.imyeyu.api.modules.user.service; - -import com.imyeyu.api.modules.user.entity.RoleRelation; -import com.imyeyu.spring.service.BaseService; - -import java.util.List; - -/// 角色继承关系服务 -/// -/// @author 夜雨 -/// @since 2026-05-13 14:46 -public interface RoleRelationService extends BaseService { - - /// 查询直属子角色关联 - /// - /// @param parentRoleId 父角色 ID - /// @return 角色关联列表 - List listByParentRoleId(String parentRoleId); - - /// 查询直属子角色 ID 列表 - /// - /// @param parentRoleId 父角色 ID - /// @return 子角色 ID 列表 - List listChildRoleIdByParentRoleId(String parentRoleId); - - /// 递归展开全部子角色 ID - /// - /// @param parentRoleIdList 父角色 ID 列表 - /// @return 全部子角色 ID 列表 - List listAllChildRoleIdByParentRoleIdList(List parentRoleIdList); -} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java b/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java index eda63dd..f3c5332 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java @@ -3,9 +3,9 @@ package com.imyeyu.api.modules.user.service; import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Role; -import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeReq; import com.imyeyu.spring.service.BaseService; +import java.util.Collection; import java.util.List; /// 角色服务 @@ -14,60 +14,25 @@ import java.util.List; /// @since 2026-05-13 14:46 public interface RoleService extends BaseService { - /// 给用户授权角色 - /// - /// @param req 用户角色授权请求 - void authorizeUserRole(UserRoleAuthorizeReq req); - - /// 查询用户直属角色 - /// - /// @param userId 用户 ID - /// @return 角色列表 - List listByUserId(String userId); - - /// 查询用户指定模块直属角色 - /// - /// @param userId 用户 ID - /// @param moduleCode 模块代码 - /// @return 角色列表 - List listByUserId(String userId, ModuleCode moduleCode); - /// 查询模块角色 /// /// @param moduleCode 模块代码 /// @return 角色列表 List listByModuleCode(ModuleCode moduleCode); - /// 查询用户直属角色编码 - /// - /// @param userId 用户 ID - /// @return 角色编码列表,格式为 MODULE:ROLE - default List listCodeByUserId(String userId) { - return listByUserId(userId).stream().map(role -> "%s:%s".formatted(role.getModuleCode().name(), role.getCode())).toList(); - } + List listByIdList(Collection idList); - /// 查询用户全部角色编码 + /// 查询当前用户可管理的模块角色 /// - /// @param userId 用户 ID - /// @return 包含子角色的角色编码列表,格式为 MODULE:ROLE - List listAllCodeByUserId(String userId); + /// @param moduleCode 模块代码 + /// @return 角色列表 + List listManageableRole(ModuleCode moduleCode); - /// 查询用户全部权限 + /// 查询当前用户可授权给账号的模块角色 /// - /// @param userId 用户 ID - /// @return 权限列表 - List listAllPermissionByUserId(String userId); - - /// 查询用户全部权限编码 - /// - /// @param userId 用户 ID - /// @return 权限编码列表,格式为 MODULE:PERMISSION - default List listAllPermissionCodeByUserId(String userId) { - return listAllPermissionByUserId(userId).stream() - .map(permission -> "%s:%s".formatted(permission.getModuleCode().name(), permission.getCode())) - .distinct() - .toList(); - } + /// @param moduleCode 模块代码 + /// @return 角色列表 + List listGrantableRole(ModuleCode moduleCode); /// 查询直属子角色 /// @@ -80,4 +45,44 @@ public interface RoleService extends BaseService { /// @param roleId 角色 ID /// @return 权限列表 List listPermissionByRoleId(String roleId); + + /// 查询可配置给目标角色的直接权限 + /// + /// @param roleId 角色 ID + /// @return 权限列表 + List listGrantableRolePermission(String roleId); + + /// 查询角色转授权限 + /// + /// @param roleId 角色 ID + /// @return 权限列表 + List listDelegationPermissionByRoleId(String roleId); + + /// 查询可配置给目标角色的转授权限 + /// + /// @param roleId 角色 ID + /// @return 权限列表 + List listGrantableDelegationPermission(String roleId); + + /// 修改角色转授权限 + /// + /// @param role 角色 + void updateRoleDelegation(Role role); + + /// 查询可配置给目标角色的管理父级角色 + /// + /// @param roleId 角色 ID + /// @return 角色列表 + List listGrantableParentRole(String roleId); + + /// 修改角色管理父级 + /// + /// @param role 角色 + void updateRoleParent(Role role); + + /// 递归获取角色及其所有子角色的 ID 列表 + /// + /// @param idList 角色 ID 列表 + /// @return 包含所有子角色的 ID 列表 + List listAllRoleIdIncludingChildren(Collection idList); } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/UserLoginService.java b/src/main/java/com/imyeyu/api/modules/user/service/UserLoginService.java index cd79a0f..8c3b499 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/UserLoginService.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/UserLoginService.java @@ -3,6 +3,7 @@ package com.imyeyu.api.modules.user.service; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.vo.LoginRequest; import com.imyeyu.api.modules.user.vo.LoginResponse; +import com.imyeyu.spring.TimiSpring; /** * 用户登录服务 @@ -27,7 +28,11 @@ public interface UserLoginService { * * @return 用户 */ - User getLoginUser(); + default User getLoginUser() { + return getLoginUser(TimiSpring.getToken()); + } + + User getLoginUser(String token); /** * 根据令牌获取必须有效登录的用户,否则抛出错误 diff --git a/src/main/java/com/imyeyu/api/modules/user/service/UserRoleService.java b/src/main/java/com/imyeyu/api/modules/user/service/UserRoleService.java index e2e3ee3..11423f7 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/UserRoleService.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/UserRoleService.java @@ -1,9 +1,15 @@ package com.imyeyu.api.modules.user.service; +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.modules.user.entity.Permission; +import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.UserRole; +import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeRequest; import com.imyeyu.spring.service.BaseService; +import java.util.Collection; import java.util.List; +import java.util.Map; /// 用户角色绑定服务 /// @@ -16,11 +22,62 @@ public interface UserRoleService extends BaseService { /// @param list 用户角色绑定列表 void createBatch(List list); - /// 查询用户角色绑定 + /// 给用户授权角色 + /// + /// @param req 用户角色授权请求 + void authorizeUserRole(UserRoleAuthorizeRequest req); + + List listByUserId(String userId); + + /// 查询多个用户角色绑定 + /// + /// @param userIdList 用户 ID 列表 + /// @return 用户角色绑定列表 + List listByUserIdList(Collection userIdList); + + /// 查询用户直属角色 /// /// @param userId 用户 ID - /// @return 用户角色绑定列表 - List listByUserId(String userId); + /// @return 角色列表 + List listRoleByUserId(String userId); + + /// 查询用户指定模块直属角色 + /// + /// @param moduleCode 模块代码 + /// @param userId 用户 ID + /// @return 角色列表 + List listRoleByUserId(ModuleCode moduleCode, String userId); + + /// 查询用户直属角色编码 + /// + /// @param userId 用户 ID + /// @return 角色编码列表,格式为 MODULE:ROLE + default List listRoleCodeByUserId(String userId) { + return listRoleByUserId(userId).stream().map(role -> "%s:%s".formatted(role.getModuleCode().name(), role.getCode())).toList(); + } + + /// 查询用户全部角色编码 + /// + /// @param userId 用户 ID + /// @return 包含子角色的角色编码列表,格式为 MODULE:ROLE + List listAllRoleCodeByUserId(String userId); + + /// 查询用户全部权限 + /// + /// @param userId 用户 ID + /// @return 权限列表 + List listAllPermissionByUserId(String userId); + + /// 查询用户全部权限编码 + /// + /// @param userId 用户 ID + /// @return 权限编码列表,格式为 MODULE:PERMISSION + default List listAllPermissionCodeByUserId(String userId) { + return listAllPermissionByUserId(userId).stream() + .map(permission -> "%s:%s".formatted(permission.getModuleCode().name(), permission.getCode())) + .distinct() + .toList(); + } /// 查询用户直属角色 ID /// @@ -31,6 +88,12 @@ public interface UserRoleService extends BaseService { return bindingList.stream().map(UserRole::getRoleId).toList(); } + /// 按用户 ID 分组查询直属角色 + /// + /// @param idList 用户 ID 列表 + /// @return 用户角色映射 + Map> mapRoleListByUserIdList(Collection idList); + /// 删除用户指定角色绑定 /// /// @param userId 用户 ID diff --git a/src/main/java/com/imyeyu/api/modules/user/service/UserService.java b/src/main/java/com/imyeyu/api/modules/user/service/UserService.java index 465ab0e..c0b9fe2 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/UserService.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/UserService.java @@ -3,6 +3,9 @@ package com.imyeyu.api.modules.user.service; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.spring.service.BaseService; +import java.util.Collection; +import java.util.Map; + /** * 用户管理服务 * @@ -20,4 +23,10 @@ public interface UserService extends BaseService { User getByPhoneNo(String phoneNo); User getByVerifiedPhoneNo(String phoneNo); + + void verifyEmail(String id); + + void verifyPhoneNo(String id); + + Map mapByIdList(Collection idList); } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java new file mode 100644 index 0000000..30baa9c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java @@ -0,0 +1,376 @@ +package com.imyeyu.api.modules.user.service.implement; + +import com.imyeyu.api.bean.CorePermissionCode; +import com.imyeyu.api.bean.CoreRoleCode; +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; +import com.imyeyu.api.modules.user.entity.Permission; +import com.imyeyu.api.modules.user.entity.Role; +import com.imyeyu.api.modules.user.entity.UserRole; +import com.imyeyu.api.modules.user.mapper.PermissionMapper; +import com.imyeyu.api.modules.user.mapper.RoleMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionDelegationMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionMapper; +import com.imyeyu.api.modules.user.mapper.UserRoleMapper; +import com.imyeyu.api.modules.user.service.AuthorizationBoundaryService; +import com.imyeyu.api.modules.user.service.AuthorizationScopeService; +import com.imyeyu.api.modules.user.service.ModuleResourceScopeResolver; +import com.imyeyu.api.modules.user.service.RoleHierarchyService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// 授权边界服务实现 +/// +/// @author Codex +/// @since 2026-08-07 +@Service +@RequiredArgsConstructor +public class AuthorizationBoundaryServiceImplement implements AuthorizationBoundaryService { + + private final RoleMapper roleMapper; + private final UserRoleMapper userRoleMapper; + private final PermissionMapper permissionMapper; + private final RolePermissionMapper rolePermissionMapper; + private final RolePermissionDelegationMapper rolePermissionDelegationMapper; + private final AuthorizationScopeService authorizationScopeService; + private final RoleHierarchyService roleHierarchyService; + private final List moduleResourceScopeResolverList; + + @Override + public void checkCanGrantRoles(String operatorUserId, String targetUserId, Collection roleIdList) { + if (TimiJava.isEmpty(roleIdList)) { + return; + } + checkCorePermission(operatorUserId, CorePermissionCode.USER_ROLE_CREATE); + if (authorizationScopeService.isSystemAdmin(operatorUserId)) { + return; + } + List roleList = roleMapper.selectByIdList(roleIdList); + checkRoleIdListComplete(roleIdList, roleList); + for (Role role : roleList) { + checkCanManageUserInModule(operatorUserId, targetUserId, role.getModuleCode()); + checkGrantableRole(operatorUserId, role); + } + } + + @Override + public void checkCanRevokeRoles(String operatorUserId, String targetUserId, Collection roleIdList) { + if (TimiJava.isEmpty(roleIdList)) { + return; + } + checkCorePermission(operatorUserId, CorePermissionCode.USER_ROLE_DELETE); + if (authorizationScopeService.isSystemAdmin(operatorUserId)) { + return; + } + List roleList = roleMapper.selectByIdList(roleIdList); + checkRoleIdListComplete(roleIdList, roleList); + for (Role role : roleList) { + checkCanManageUserInModule(operatorUserId, targetUserId, role.getModuleCode()); + checkManageableRole(operatorUserId, role); + } + } + + @Override + public void checkCanCreateRole(String operatorUserId, Role role) { + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_CREATE); + if (authorizationScopeService.isSystemAdmin(operatorUserId)) { + return; + } + TimiException.required(role.getModuleCode(), "not found role.moduleCode"); + TimiException.required(role.getOwnerType(), "not found role.ownerType"); + TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能创建内置角色"); + TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能创建受保护角色"); + checkOwnerScope(operatorUserId, role.getModuleCode(), role.getOwnerType(), role.getOwnerId()); + } + + @Override + public void checkCanUpdateRole(String operatorUserId, Role role) { + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_UPDATE); + Role dbRole = roleMapper.selectById(role.getId()); + TimiException.required(dbRole, "not found role"); + if (authorizationScopeService.isSystemAdmin(operatorUserId)) { + return; + } + checkManageableRole(operatorUserId, dbRole); + TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能设置内置角色"); + TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能设置受保护角色"); + if (role.getModuleCode() != null) { + TimiException.requiredTrue(role.getModuleCode() == dbRole.getModuleCode(), "不能修改角色模块"); + } + if (role.getOwnerType() != null) { + checkOwnerScope(operatorUserId, dbRole.getModuleCode(), role.getOwnerType(), role.getOwnerId()); + } + } + + @Override + public void checkCanDeleteRole(String operatorUserId, String roleId) { + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_DELETE); + Role role = roleMapper.selectById(roleId); + TimiException.required(role, "not found role"); + if (authorizationScopeService.isSystemAdmin(operatorUserId)) { + return; + } + checkManageableRole(operatorUserId, role); + TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能删除内置角色"); + TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能删除受保护角色"); + } + + @Override + public void checkCanGrantPermissions(String operatorUserId, String roleId, Collection permissionIdList) { + if (TimiJava.isEmpty(permissionIdList)) { + return; + } + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_CREATE); + Role role = roleMapper.selectById(roleId); + TimiException.required(role, "not found role"); + boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId); + if (!systemAdmin) { + checkManageableRole(operatorUserId, role); + } + List permissionList = permissionMapper.selectByIdList(permissionIdList.stream().toList()); + checkPermissionIdListComplete(permissionIdList, permissionList); + Set grantablePermissionIdSet = listRoleDirectGrantablePermissionId(operatorUserId, role); + for (Permission permission : permissionList) { + TimiException.requiredTrue(grantablePermissionIdSet.contains(permission.getId()), "权限不在上级下放边界内:%s".formatted(permission.getCode())); + } + } + + @Override + public void checkCanRevokePermissions(String operatorUserId, String roleId, Collection permissionIdList) { + if (TimiJava.isEmpty(permissionIdList)) { + return; + } + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_DELETE); + Role role = roleMapper.selectById(roleId); + TimiException.required(role, "not found role"); + if (authorizationScopeService.isSystemAdmin(operatorUserId)) { + return; + } + checkManageableRole(operatorUserId, role); + } + + @Override + public void checkCanUpdateRoleDelegation(String operatorUserId, String roleId, Collection permissionIdList) { + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_CREATE); + Role role = roleMapper.selectById(roleId); + TimiException.required(role, "not found role"); + boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId); + if (!systemAdmin) { + checkManageableRole(operatorUserId, role); + } + if (TimiJava.isEmpty(permissionIdList)) { + return; + } + List permissionList = permissionMapper.selectByIdList(permissionIdList.stream().toList()); + checkPermissionIdListComplete(permissionIdList, permissionList); + boolean hasParentRole = TimiJava.isNotEmpty(role.getParentRoleId()); + Set parentDelegationPermissionIdSet = listParentDelegationPermissionId(roleId); + if (!isCoreDelegationRoot(role)) { + TimiException.requiredTrue(hasParentRole, "目标角色缺少管理父级"); + } + Set operatorGrantablePermissionIdSet = listGrantablePermissionId(operatorUserId, permissionList); + for (Permission permission : permissionList) { + TimiException.requiredTrue(role.getModuleCode() == permission.getModuleCode() || isCoreDelegationRoot(role) || isCoreAuthorizationPermission(permission), "不能配置其他模块转授权限"); + if (hasParentRole) { + TimiException.requiredTrue(parentDelegationPermissionIdSet.contains(permission.getId()), "转授权限超过上级边界:%s".formatted(permission.getCode())); + } + if (!systemAdmin) { + TimiException.requiredTrue(operatorGrantablePermissionIdSet.contains(permission.getId()), "权限不在可转授范围内:%s".formatted(permission.getCode())); + } + } + } + + @Override + public void checkCanUpdateRoleParent(String operatorUserId, String roleId, String parentRoleId) { + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_UPDATE); + Role targetRole = roleMapper.selectById(roleId); + TimiException.required(targetRole, "not found role"); + boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId); + if (!systemAdmin) { + checkManageableRole(operatorUserId, targetRole); + } + if (TimiJava.isEmpty(parentRoleId)) { + TimiException.requiredTrue(isCoreDelegationRoot(targetRole), "非根角色必须设置管理父级"); + return; + } + Role parentRole = roleMapper.selectById(parentRoleId); + TimiException.required(parentRole, "not found parent role"); + TimiException.requiredTrue(!roleId.equals(parentRoleId), "管理父级不能指向自身"); + TimiException.requiredTrue(!isManagementDescendant(roleId, parentRoleId), "管理父级不能指向自己的下级"); + if (!systemAdmin && !isOperatorRole(operatorUserId, parentRole)) { + checkManageableRole(operatorUserId, parentRole); + } + } + + @Override + public void checkCanUpdateRolePermissionInherits(String operatorUserId, String parentRoleId, Collection childRoleIdList) { + if (TimiJava.isEmpty(childRoleIdList)) { + return; + } + checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_INHERIT_CREATE); + Role parentRole = roleMapper.selectById(parentRoleId); + TimiException.required(parentRole, "not found parent role"); + roleHierarchyService.checkNoCycle(parentRoleId, childRoleIdList); + if (authorizationScopeService.isSystemAdmin(operatorUserId)) { + return; + } + if (!isOperatorRole(operatorUserId, parentRole)) { + checkManageableRole(operatorUserId, parentRole); + } + List childRoleList = roleMapper.selectByIdList(childRoleIdList); + checkRoleIdListComplete(childRoleIdList, childRoleList); + for (Role childRole : childRoleList) { + TimiException.requiredTrue(parentRole.getModuleCode() == childRole.getModuleCode(), "不能继承其他模块角色"); + checkManageableRole(operatorUserId, childRole); + } + } + + private void checkGrantableRole(String operatorUserId, Role role) { + Set grantableRoleIdSet = authorizationScopeService.listGrantableRole(operatorUserId, role.getModuleCode()) + .stream() + .map(Role::getId) + .collect(Collectors.toSet()); + TimiException.requiredTrue(grantableRoleIdSet.contains(role.getId()), "角色不在可授权范围内:%s".formatted(role.getCode())); + } + + private void checkManageableRole(String operatorUserId, Role role) { + Set manageableRoleIdSet = authorizationScopeService.listManageableRole(operatorUserId, role.getModuleCode()) + .stream() + .map(Role::getId) + .collect(Collectors.toSet()); + TimiException.requiredTrue(manageableRoleIdSet.contains(role.getId()), "角色不在可管理范围内:%s".formatted(role.getCode())); + } + + private Set listRoleAllPermissionId(String operatorUserId, String roleId) { + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(operatorUserId, List.of(roleId)); + return rolePermissionMapper.selectPermissionByRoleIdList(allRoleIdList) + .stream() + .map(Permission::getId) + .collect(Collectors.toSet()); + } + + private Set listGrantablePermissionId(String operatorUserId, List permissionList) { + if (TimiJava.isEmpty(permissionList)) { + return Set.of(); + } + return permissionList.stream() + .map(Permission::getModuleCode) + .distinct() + .flatMap(moduleCode -> authorizationScopeService.listGrantablePermission(operatorUserId, moduleCode).stream()) + .map(Permission::getId) + .collect(Collectors.toSet()); + } + + private Set listRoleDirectGrantablePermissionId(String operatorUserId, Role role) { + boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId); + if (systemAdmin && isCoreDelegationRoot(role) && TimiJava.isEmpty(role.getParentRoleId())) { + return permissionMapper.selectAll().stream().map(Permission::getId).collect(Collectors.toSet()); + } + TimiException.requiredTrue(TimiJava.isNotEmpty(role.getParentRoleId()), "目标角色缺少管理父级"); + Set parentDelegationPermissionIdSet = listParentDelegationPermissionId(role.getId()); + if (systemAdmin) { + return parentDelegationPermissionIdSet; + } + List parentDelegationPermissionList = TimiJava.isEmpty(parentDelegationPermissionIdSet) + ? List.of() + : permissionMapper.selectByIdList(parentDelegationPermissionIdSet.stream().toList()); + Set operatorGrantablePermissionIdSet = listGrantablePermissionId(operatorUserId, parentDelegationPermissionList); + return parentDelegationPermissionIdSet.stream() + .filter(operatorGrantablePermissionIdSet::contains) + .collect(Collectors.toSet()); + } + + private Set listParentDelegationPermissionId(String roleId) { + Role role = roleMapper.selectById(roleId); + if (role == null || TimiJava.isEmpty(role.getParentRoleId())) { + return Set.of(); + } + return rolePermissionDelegationMapper.selectPermissionByRoleIdList(List.of(role.getParentRoleId())) + .stream() + .map(Permission::getId) + .collect(Collectors.toSet()); + } + + private boolean isOperatorRole(String operatorUserId, Role role) { + List roleIdList = userRoleMapper.selectByUserIdList(List.of(operatorUserId)).stream().map(UserRole::getRoleId).toList(); + if (TimiJava.isEmpty(roleIdList)) { + return false; + } + return roleHierarchyService.listAllRoleIdIncludingChildren(operatorUserId, roleIdList).contains(role.getId()); + } + + private boolean isCoreDelegationRoot(Role role) { + return role != null + && ModuleCode.CORE == role.getModuleCode() + && (CoreRoleCode.SYSTEM.name().equals(role.getCode()) || CoreRoleCode.ADMIN.name().equals(role.getCode())); + } + + private boolean isManagementDescendant(String parentRoleId, String childRoleId) { + List childRoleList = roleMapper.selectByParentRoleId(parentRoleId); + for (Role childRole : childRoleList) { + if (childRoleId.equals(childRole.getId()) || isManagementDescendant(childRole.getId(), childRoleId)) { + return true; + } + } + return false; + } + + private boolean isCoreAuthorizationPermission(Permission permission) { + return permission != null && ModuleCode.CORE == permission.getModuleCode(); + } + + private void checkCanManageUserInModule(String operatorUserId, String targetUserId, ModuleCode moduleCode) { + ModuleResourceScopeResolver resolver = getResolver(moduleCode); + TimiException.requiredTrue(resolver == null || resolver.canManageUser(operatorUserId, targetUserId), "目标用户不在可管理范围内"); + } + + private void checkOwnerScope(String operatorUserId, ModuleCode moduleCode, AuthOwnerType ownerType, String ownerId) { + ModuleResourceScopeResolver resolver = getResolver(moduleCode); + TimiException.requiredTrue(resolver == null || resolver.canUseOwnerScope(operatorUserId, ownerType, ownerId), "归属范围不在可管理范围内"); + } + + private void checkCorePermission(String operatorUserId, CorePermissionCode permissionCode) { + TimiException.requiredTrue(hasPermission(operatorUserId, ModuleCode.CORE, permissionCode.getValue()), "权限不足:%s".formatted(permissionCode.getValue())); + } + + private boolean hasPermission(String userId, ModuleCode moduleCode, String permissionCode) { + List roleIdList = userRoleMapper.selectByUserIdList(List.of(userId)).stream().map(UserRole::getRoleId).toList(); + if (TimiJava.isEmpty(roleIdList)) { + return false; + } + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(userId, roleIdList); + return rolePermissionMapper.selectPermissionByRoleIdList(allRoleIdList) + .stream() + .anyMatch(permission -> moduleCode == permission.getModuleCode() && permissionCode.equals(permission.getCode())); + } + + private ModuleResourceScopeResolver getResolver(ModuleCode moduleCode) { + return moduleResourceScopeResolverList.stream() + .filter(resolver -> moduleCode == resolver.moduleCode()) + .findFirst() + .orElse(null); + } + + private void checkRoleIdListComplete(Collection idList, List roleList) { + Set foundIdSet = roleList.stream().map(Role::getId).collect(Collectors.toSet()); + Set idSet = new HashSet<>(idList); + TimiException.requiredTrue(foundIdSet.containsAll(idSet), "存在无效角色 ID"); + } + + private void checkPermissionIdListComplete(Collection idList, List permissionList) { + Set foundIdSet = permissionList.stream().map(Permission::getId).collect(Collectors.toSet()); + Set idSet = new HashSet<>(idList); + TimiException.requiredTrue(foundIdSet.containsAll(idSet), "存在无效权限 ID"); + } + +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationScopeServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationScopeServiceImplement.java new file mode 100644 index 0000000..069b51c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationScopeServiceImplement.java @@ -0,0 +1,178 @@ +package com.imyeyu.api.modules.user.service.implement; + +import com.imyeyu.api.bean.CoreRoleCode; +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.modules.user.entity.Permission; +import com.imyeyu.api.modules.user.entity.Role; +import com.imyeyu.api.modules.user.entity.UserRole; +import com.imyeyu.api.modules.user.mapper.PermissionMapper; +import com.imyeyu.api.modules.user.mapper.RoleMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionDelegationMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionMapper; +import com.imyeyu.api.modules.user.mapper.UserRoleMapper; +import com.imyeyu.api.modules.user.service.AuthorizationScopeService; +import com.imyeyu.api.modules.user.service.ModuleResourceScopeResolver; +import com.imyeyu.api.modules.user.service.RoleHierarchyService; +import com.imyeyu.java.TimiJava; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// 授权范围服务实现 +/// +/// @author Codex +/// @since 2026-08-07 +@Service +@RequiredArgsConstructor +public class AuthorizationScopeServiceImplement implements AuthorizationScopeService { + + private final RoleMapper roleMapper; + private final UserRoleMapper userRoleMapper; + private final PermissionMapper permissionMapper; + private final RolePermissionMapper rolePermissionMapper; + private final RolePermissionDelegationMapper rolePermissionDelegationMapper; + private final RoleHierarchyService roleHierarchyService; + private final List moduleResourceScopeResolverList; + + @Override + public List listGrantableRole(String operatorUserId, ModuleCode moduleCode) { + List roleList = listManageableRole(operatorUserId, moduleCode); + if (isSystemAdmin(operatorUserId)) { + return roleList; + } + Set operatorDelegationPermissionIdSet = listOperatorDelegationPermissionId(operatorUserId); + return roleList.stream() + .filter(role -> operatorDelegationPermissionIdSet.containsAll(listRoleAllPermissionId(operatorUserId, role.getId()))) + .toList(); + } + + @Override + public List listGrantablePermission(String operatorUserId, ModuleCode moduleCode) { + List permissionList = permissionMapper.selectByModuleCode(moduleCode); + if (isSystemAdmin(operatorUserId)) { + return permissionList; + } + Set delegationPermissionIdSet = listOperatorDelegationPermissionId(operatorUserId, moduleCode); + return permissionList.stream() + .filter(permission -> !Boolean.TRUE.equals(permission.getProtectedPermission())) + .filter(permission -> delegationPermissionIdSet.contains(permission.getId())) + .toList(); + } + + @Override + public List listManageableRole(String operatorUserId, ModuleCode moduleCode) { + List roleList = roleMapper.selectByModuleCode(moduleCode); + if (isSystemAdmin(operatorUserId)) { + return roleList; + } + Set manageableRoleIdSet = listManagementDescendantRoleId(operatorUserId); + ModuleResourceScopeResolver resolver = getResolver(moduleCode); + return roleList.stream() + .filter(role -> manageableRoleIdSet.contains(role.getId())) + .filter(role -> !Boolean.TRUE.equals(role.getProtectedRole())) + .filter(role -> resolver == null || resolver.canManageRole(operatorUserId, role)) + .toList(); + } + + @Override + public boolean isSystemAdmin(String operatorUserId) { + return listAllOperatorRole(operatorUserId).stream() + .anyMatch(role -> ModuleCode.CORE == role.getModuleCode() + && CoreRoleCode.SYSTEM.name().equals(role.getCode())); + } + + private Set listManagementDescendantRoleId(String operatorUserId) { + List operatorRoleIdList = listOperatorRoleId(operatorUserId); + if (TimiJava.isEmpty(operatorRoleIdList)) { + return Set.of(); + } + Map> childRoleMap = roleMapper.selectAll() + .stream() + .filter(role -> TimiJava.isNotEmpty(role.getParentRoleId())) + .collect(Collectors.groupingBy(Role::getParentRoleId)); + Set result = new HashSet<>(); + ArrayDeque queue = new ArrayDeque<>(operatorRoleIdList); + while (!queue.isEmpty()) { + String roleId = queue.removeFirst(); + for (Role childRole : childRoleMap.getOrDefault(roleId, List.of())) { + if (result.add(childRole.getId())) { + queue.addLast(childRole.getId()); + } + } + } + return result; + } + + private List listAllOperatorRole(String operatorUserId) { + List allRoleIdList = listAllOperatorRoleId(operatorUserId); + if (TimiJava.isEmpty(allRoleIdList)) { + return List.of(); + } + Map roleMap = roleMapper.selectByIdList(allRoleIdList).stream().collect(Collectors.toMap(Role::getId, Function.identity())); + return allRoleIdList.stream().map(roleMap::get).filter(java.util.Objects::nonNull).toList(); + } + + private List listAllOperatorRoleId(String operatorUserId) { + List roleIdList = listOperatorRoleId(operatorUserId); + if (TimiJava.isEmpty(roleIdList)) { + return List.of(); + } + return roleHierarchyService.listAllRoleIdIncludingChildren(operatorUserId, roleIdList); + } + + private List listOperatorRoleId(String operatorUserId) { + if (TimiJava.isEmpty(operatorUserId)) { + return List.of(); + } + return userRoleMapper.selectByUserIdList(List.of(operatorUserId)).stream().map(UserRole::getRoleId).toList(); + } + + private Set listOperatorDelegationPermissionId(String operatorUserId, ModuleCode moduleCode) { + List roleIdList = listOperatorRoleId(operatorUserId); + if (TimiJava.isEmpty(roleIdList)) { + return Set.of(); + } + return rolePermissionDelegationMapper.selectPermissionByRoleIdList(roleIdList) + .stream() + .filter(permission -> moduleCode == permission.getModuleCode()) + .map(Permission::getId) + .collect(Collectors.toCollection(HashSet::new)); + } + + private Set listOperatorDelegationPermissionId(String operatorUserId) { + List roleIdList = listOperatorRoleId(operatorUserId); + if (TimiJava.isEmpty(roleIdList)) { + return Set.of(); + } + return rolePermissionDelegationMapper.selectPermissionByRoleIdList(roleIdList) + .stream() + .map(Permission::getId) + .collect(Collectors.toCollection(HashSet::new)); + } + + private Set listRoleAllPermissionId(String operatorUserId, String roleId) { + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(operatorUserId, List.of(roleId)); + if (TimiJava.isEmpty(allRoleIdList)) { + return Set.of(); + } + return rolePermissionMapper.selectPermissionByRoleIdList(allRoleIdList) + .stream() + .map(Permission::getId) + .collect(Collectors.toSet()); + } + + private ModuleResourceScopeResolver getResolver(ModuleCode moduleCode) { + return moduleResourceScopeResolverList.stream() + .filter(resolver -> moduleCode == resolver.moduleCode()) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionCheckerImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionCheckerImplement.java index fd6427c..32e0f39 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionCheckerImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionCheckerImplement.java @@ -4,6 +4,7 @@ import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.modules.user.service.PermissionChecker; import com.imyeyu.api.modules.user.service.RoleService; import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.api.modules.user.service.UserRoleService; import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiException; import lombok.RequiredArgsConstructor; @@ -25,7 +26,7 @@ import java.util.Set; @RequiredArgsConstructor public class PermissionCheckerImplement implements PermissionChecker { - private final RoleService roleService; + private final UserRoleService userRoleService; private final UserLoginService userLoginService; public Set codeSet() { @@ -34,7 +35,7 @@ public class PermissionCheckerImplement implements PermissionChecker { @Override public Set codeSet(String userId) { - return new HashSet<>(roleService.listAllPermissionCodeByUserId(userId)); + return new HashSet<>(userRoleService.listAllPermissionCodeByUserId(userId)); } @Override diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionServiceImplement.java index a80a34f..4651df5 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/PermissionServiceImplement.java @@ -1,8 +1,10 @@ package com.imyeyu.api.modules.user.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.modules.common.mapper.MultilingualMapper; import com.imyeyu.api.modules.common.service.MultilingualService; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.mapper.PermissionMapper; import com.imyeyu.api.modules.user.mapper.RolePermissionMapper; @@ -45,6 +47,7 @@ public class PermissionServiceImplement extends AbstractEntityService listByModuleCode(ModuleCode moduleCode) { + return mapper.selectByModuleCode(moduleCode); + } + + private void fillPermissionDefault(Permission permission) { + permission.setBuiltin(TimiJava.defaultIfNull(permission.getBuiltin(), false)); + permission.setProtectedPermission(TimiJava.defaultIfNull(permission.getProtectedPermission(), false)); + permission.setOwnerType(TimiJava.defaultIfNull(permission.getOwnerType(), AuthOwnerType.MODULE)); + permission.setOwnerId(TimiJava.defaultIfEmpty(permission.getOwnerId(), permission.getModuleCode() == null ? null : permission.getModuleCode().name())); + } } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleCheckerImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleCheckerImplement.java index 57a81f5..9fad2a1 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleCheckerImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleCheckerImplement.java @@ -2,8 +2,8 @@ package com.imyeyu.api.modules.user.service.implement; import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.modules.user.service.RoleChecker; -import com.imyeyu.api.modules.user.service.RoleService; import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.api.modules.user.service.UserRoleService; import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiException; import lombok.RequiredArgsConstructor; @@ -24,12 +24,12 @@ import java.util.Set; @RequiredArgsConstructor public class RoleCheckerImplement implements RoleChecker { - private final RoleService roleService; + private final UserRoleService userRoleService; private final UserLoginService userLoginService; @Override public Set codeSet() { - return new HashSet<>(roleService.listAllCodeByUserId(userLoginService.getRequireLoginUserId())); + return new HashSet<>(userRoleService.listAllRoleCodeByUserId(userLoginService.getRequireLoginUserId())); } @Override diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleHierarchyServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleHierarchyServiceImplement.java new file mode 100644 index 0000000..22004df --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleHierarchyServiceImplement.java @@ -0,0 +1,109 @@ +package com.imyeyu.api.modules.user.service.implement; + +import com.imyeyu.api.modules.user.bean.AuthOwnerType; +import com.imyeyu.api.modules.user.entity.Role; +import com.imyeyu.api.modules.user.entity.RolePermissionInherit; +import com.imyeyu.api.modules.user.mapper.RoleMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionInheritMapper; +import com.imyeyu.api.modules.user.service.ModuleResourceScopeResolver; +import com.imyeyu.api.modules.user.service.RoleHierarchyService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import org.springframework.beans.factory.ObjectProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// 角色继承服务实现 +/// +/// @author Codex +/// @since 2026-08-07 +@Service +@RequiredArgsConstructor +public class RoleHierarchyServiceImplement implements RoleHierarchyService { + + private final RolePermissionInheritMapper rolePermissionInheritMapper; + private final RoleMapper roleMapper; + private final ObjectProvider moduleResourceScopeResolverProvider; + + @Override + public List listAllRoleIdIncludingChildren(Collection roleIdList) { + return listAllRoleIdIncludingChildren(null, roleIdList); + } + + @Override + public List listAllRoleIdIncludingChildren(String userId, Collection roleIdList) { + if (TimiJava.isEmpty(roleIdList)) { + return List.of(); + } + Set allRoleIdSet = new HashSet<>(roleIdList); + List currentLevelRoleIdList = new ArrayList<>(roleIdList); + while (TimiJava.isNotEmpty(currentLevelRoleIdList)) { + List relationList = rolePermissionInheritMapper.selectByParentRoleIdList(currentLevelRoleIdList); + List parentAndChildRoleIdList = relationList.stream() + .flatMap(relation -> java.util.stream.Stream.of(relation.getParentRoleId(), relation.getChildRoleId())) + .distinct() + .toList(); + Map roleMap = roleMapper.selectByIdList(parentAndChildRoleIdList).stream().collect(Collectors.toMap(Role::getId, Function.identity())); + List childRoleIdList = relationList.stream() + .filter(relation -> isVisible(userId, relation, roleMap.get(relation.getParentRoleId()))) + .map(RolePermissionInherit::getChildRoleId) + .toList(); + if (TimiJava.isEmpty(childRoleIdList)) { + break; + } + List newChildRoleIdList = childRoleIdList.stream().filter(id -> !allRoleIdSet.contains(id)).toList(); + if (TimiJava.isEmpty(newChildRoleIdList)) { + break; + } + allRoleIdSet.addAll(newChildRoleIdList); + currentLevelRoleIdList = newChildRoleIdList; + } + return new ArrayList<>(allRoleIdSet); + } + + @Override + public void checkNoCycle(String parentRoleId, Collection childRoleIdList) { + if (TimiJava.isEmpty(childRoleIdList)) { + return; + } + for (String childRoleId : childRoleIdList) { + if (parentRoleId.equals(childRoleId) || isDescendant(childRoleId, parentRoleId)) { + throw new TimiException(TimiCode.PERMISSION_ERROR, "角色继承关系不能形成循环"); + } + } + } + + @Override + public boolean isDescendant(String parentRoleId, String childRoleId) { + return listAllRoleIdIncludingChildren(List.of(parentRoleId)).contains(childRoleId); + } + + private boolean isVisible(String userId, RolePermissionInherit relation, Role parentRole) { + if (relation.getOwnerType() == null) { + return true; + } + if (relation.getOwnerType() == AuthOwnerType.MODULE && parentRole != null && Objects.equals(relation.getOwnerId(), parentRole.getModuleCode().name())) { + return true; + } + if (TimiJava.isEmpty(userId) || parentRole == null) { + return false; + } + ModuleResourceScopeResolver resolver = moduleResourceScopeResolverProvider.orderedStream() + .filter(item -> parentRole.getModuleCode() == item.moduleCode()) + .findFirst() + .orElse(null); + return resolver != null && resolver.canUseOwnerScope(userId, relation.getOwnerType(), relation.getOwnerId()); + } +} + diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleRelationServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/RolePermissionInheritServiceImplement.java similarity index 68% rename from src/main/java/com/imyeyu/api/modules/user/service/implement/RoleRelationServiceImplement.java rename to src/main/java/com/imyeyu/api/modules/user/service/implement/RolePermissionInheritServiceImplement.java index a12f818..5446125 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleRelationServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/RolePermissionInheritServiceImplement.java @@ -1,8 +1,8 @@ package com.imyeyu.api.modules.user.service.implement; -import com.imyeyu.api.modules.user.entity.RoleRelation; -import com.imyeyu.api.modules.user.mapper.RoleRelationMapper; -import com.imyeyu.api.modules.user.service.RoleRelationService; +import com.imyeyu.api.modules.user.entity.RolePermissionInherit; +import com.imyeyu.api.modules.user.mapper.RolePermissionInheritMapper; +import com.imyeyu.api.modules.user.service.RolePermissionInheritService; import com.imyeyu.java.TimiJava; import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; @@ -14,34 +14,34 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -/// 角色关联服务实现 +/// 角色权限继承服务实现 /// /// @author 夜雨 /// @since 2026-05-13 14:46 @Service @RequiredArgsConstructor -public class RoleRelationServiceImplement extends AbstractEntityService implements RoleRelationService { +public class RolePermissionInheritServiceImplement extends AbstractEntityService implements RolePermissionInheritService { - private final RoleRelationMapper mapper; + private final RolePermissionInheritMapper mapper; @Override - protected BaseMapper mapper() { + protected BaseMapper mapper() { return mapper; } @Override - public List listByParentRoleId(String parentRoleId) { + public List listByParentRoleId(String parentRoleId) { if (TimiJava.isEmpty(parentRoleId)) { return List.of(); } - RoleRelation example = new RoleRelation(); + RolePermissionInherit example = new RolePermissionInherit(); example.setParentRoleId(parentRoleId); return mapper.selectAllByExample(example); } @Override public List listChildRoleIdByParentRoleId(String parentRoleId) { - return listByParentRoleId(parentRoleId).stream().map(RoleRelation::getChildRoleId).distinct().toList(); + return listByParentRoleId(parentRoleId).stream().map(RolePermissionInherit::getChildRoleId).distinct().toList(); } @Override diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java index eb9aa83..99a5850 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java @@ -2,23 +2,29 @@ package com.imyeyu.api.modules.user.service.implement; import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; -import com.imyeyu.api.modules.common.entity.Multilingual; -import com.imyeyu.api.modules.common.mapper.MultilingualMapper; import com.imyeyu.api.modules.common.service.MultilingualService; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.RolePermission; -import com.imyeyu.api.modules.user.entity.RoleRelation; +import com.imyeyu.api.modules.user.entity.RolePermissionDelegation; +import com.imyeyu.api.modules.user.entity.RolePermissionInherit; import com.imyeyu.api.modules.user.entity.UserRole; import com.imyeyu.api.modules.user.mapper.RoleMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionDelegationMapper; import com.imyeyu.api.modules.user.mapper.RolePermissionMapper; -import com.imyeyu.api.modules.user.mapper.RoleRelationMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionInheritMapper; +import com.imyeyu.api.modules.user.mapper.UserRoleMapper; +import com.imyeyu.api.modules.user.service.AuthorizationBoundaryService; +import com.imyeyu.api.modules.user.service.AuthorizationScopeService; +import com.imyeyu.api.modules.user.service.ModuleResourceScopeResolver; import com.imyeyu.api.modules.user.service.PermissionService; +import com.imyeyu.api.modules.user.service.RoleHierarchyService; import com.imyeyu.api.modules.user.service.RoleService; -import com.imyeyu.api.modules.user.service.UserRoleService; -import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeReq; +import com.imyeyu.api.modules.user.service.UserLoginService; 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; @@ -28,10 +34,16 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; /// 角色服务实现 @@ -42,14 +54,20 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class RoleServiceImplement extends AbstractEntityService implements RoleService { - private final UserRoleService userRoleService; private final PermissionService permissionService; - private final MultilingualMapper multilingualMapper; private final MultilingualService multilingualService; + private final LoggerService loggerService; + private final UserLoginService userLoginService; + private final RoleHierarchyService roleHierarchyService; + private final AuthorizationBoundaryService authorizationBoundaryService; + private final AuthorizationScopeService authorizationScopeService; + private final List moduleResourceScopeResolverList; private final RoleMapper mapper; - private final RoleRelationMapper roleRelationMapper; + private final RolePermissionInheritMapper rolePermissionInheritMapper; private final RolePermissionMapper rolePermissionMapper; + private final RolePermissionDelegationMapper rolePermissionDelegationMapper; + private final UserRoleMapper userRoleMapper; @Override protected BaseMapper mapper() { @@ -59,8 +77,20 @@ public class RoleServiceImplement extends AbstractEntityService im @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void create(Role role) { + fillRoleDefault(role); + if (!Boolean.TRUE.equals(role.getBuiltin())) { + String loginUserId = userLoginService.getRequireLoginUserId(); + role.setCreatedBy(TimiJava.defaultIfEmpty(role.getCreatedBy(), loginUserId)); + authorizationBoundaryService.checkCanCreateRole(loginUserId, role); + } role.setNameLangId(multilingualService.create(role.getName())); super.create(role); + if (!Boolean.TRUE.equals(role.getBuiltin())) { + String loginUserId = userLoginService.getRequireLoginUserId(); + authorizationBoundaryService.checkCanUpdateRoleParent(loginUserId, role.getId(), role.getParentRoleId()); + authorizationBoundaryService.checkCanGrantPermissions(loginUserId, role.getId(), role.getPermissionIdList()); + authorizationBoundaryService.checkCanUpdateRolePermissionInherits(loginUserId, role.getId(), role.getChildRoleIdList()); + } if (TimiJava.isNotEmpty(role.getPermissionIdList())) { List rolePermissionList = new ArrayList<>(); @@ -77,34 +107,47 @@ public class RoleServiceImplement extends AbstractEntityService im } if (TimiJava.isNotEmpty(role.getChildRoleIdList())) { - List roleRelationList = new ArrayList<>(); + List rolePermissionInheritList = new ArrayList<>(); long now = Time.now(); + Map childRoleMap = mapper.selectByIdList(role.getChildRoleIdList()).stream().collect(Collectors.toMap(Role::getId, Function.identity())); for (String childRoleId : role.getChildRoleIdList()) { - RoleRelation roleRelation = new RoleRelation(); - roleRelation.setId(UUID.randomUUID().toString()); - roleRelation.setParentRoleId(role.getId()); - roleRelation.setChildRoleId(childRoleId); - roleRelation.setCreatedAt(now); - roleRelationList.add(roleRelation); + RolePermissionInherit rolePermissionInherit = new RolePermissionInherit(); + rolePermissionInherit.setId(UUID.randomUUID().toString()); + rolePermissionInherit.setParentRoleId(role.getId()); + rolePermissionInherit.setChildRoleId(childRoleId); + fillRelationScope(rolePermissionInherit, childRoleMap.get(childRoleId), role.getCreatedBy()); + rolePermissionInherit.setCreatedAt(now); + rolePermissionInheritList.add(rolePermissionInherit); } - roleRelationMapper.createBatch(roleRelationList); + rolePermissionInheritMapper.createBatch(rolePermissionInheritList); } } @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void update(Role role) { - role.setNameLangId(multilingualService.createIfDifferent(role.getNameLangId(), role.getName())); - mapper.updateSelective(role); - { + String loginUserId = userLoginService.getRequireLoginUserId(); + Role dbRole = mapper.selectById(role.getId()); + if (dbRole != null && Boolean.TRUE.equals(dbRole.getBuiltin())) { + // 内建角色允许修改名称和说明,但角色代码必须保持不变。 + role.setCode(null); + } + if (hasRoleBaseField(role)) { + authorizationBoundaryService.checkCanUpdateRole(loginUserId, role); + role.setNameLangId(multilingualService.createIfDifferent(role.getNameLangId(), role.getName())); + mapper.updateSelective(role); + } + if (role.getPermissionIdList() != null) { Set dbIdSet = rolePermissionMapper.selectByRoleId(role.getId()).stream().map(RolePermission::getPermissionId).collect(Collectors.toSet()); - Set newIdSet = TimiJava.defaultIfEmpty(new HashSet<>(role.getPermissionIdList()), new HashSet<>()); + Set newIdSet = TimiJava.isEmpty(role.getPermissionIdList()) ? new HashSet<>() : new HashSet<>(role.getPermissionIdList()); Set deleteIdList = new HashSet<>(dbIdSet); deleteIdList.removeAll(newIdSet); Set addIdList = new HashSet<>(newIdSet); addIdList.removeAll(dbIdSet); + authorizationBoundaryService.checkCanRevokePermissions(loginUserId, role.getId(), deleteIdList); + authorizationBoundaryService.checkCanGrantPermissions(loginUserId, role.getId(), addIdList); if (TimiJava.isNotEmpty(deleteIdList)) { rolePermissionMapper.deleteByPermissionIdList(role.getId(), new ArrayList<>(deleteIdList)); @@ -122,96 +165,56 @@ public class RoleServiceImplement extends AbstractEntityService im } rolePermissionMapper.createBatch(rolePermissionList); } + logRoleAuthorizationChange("USER_ROLE_PERMISSION_UPDATE", loginUserId, role.getId(), addIdList, deleteIdList); } - { - Set dbIdSet = roleRelationMapper.selectByParentRoleId(role.getId()).stream().map(RoleRelation::getChildRoleId).collect(Collectors.toSet()); - Set newIdSet = TimiJava.defaultIfEmpty(new HashSet<>(role.getChildRoleIdList()), new HashSet<>()); + if (role.getChildRoleIdList() != null) { + Role parentRole = mapper.selectById(role.getId()); + List dbRelationList = rolePermissionInheritMapper.selectByParentRoleId(role.getId()); + List touchableRelationList = dbRelationList.stream() + .filter(relation -> canTouchRelation(loginUserId, parentRole, relation)) + .toList(); + Set newIdSet = TimiJava.isEmpty(role.getChildRoleIdList()) ? new HashSet<>() : new HashSet<>(role.getChildRoleIdList()); + Map childRoleMap = mapper.selectByIdList(newIdSet).stream().collect(Collectors.toMap(Role::getId, Function.identity())); + Set dbKeySet = dbRelationList.stream().map(this::relationKey).collect(Collectors.toSet()); + Set newKeySet = newIdSet.stream().map(childRoleId -> relationKey(childRoleId, childRoleMap.get(childRoleId))).collect(Collectors.toSet()); - Set deleteIdList = new HashSet<>(dbIdSet); - deleteIdList.removeAll(newIdSet); + Set deleteKeyList = new HashSet<>(dbKeySet); + deleteKeyList.removeAll(newKeySet); - Set addIdList = new HashSet<>(newIdSet); - addIdList.removeAll(dbIdSet); + Set addKeyList = new HashSet<>(newKeySet); + addKeyList.removeAll(dbKeySet); + Set addIdList = addKeyList.stream().map(key -> key.split("\\|", 2)[0]).collect(Collectors.toSet()); + authorizationBoundaryService.checkCanUpdateRolePermissionInherits(loginUserId, role.getId(), addIdList); - if (TimiJava.isNotEmpty(deleteIdList)) { - roleRelationMapper.deleteByChildRoleIdList(role.getId(), new ArrayList<>(deleteIdList)); + if (TimiJava.isNotEmpty(deleteKeyList)) { + for (RolePermissionInherit relation : touchableRelationList) { + if (deleteKeyList.contains(relationKey(relation))) { + rolePermissionInheritMapper.deleteByChildRoleIdList(role.getId(), List.of(relation.getChildRoleId()), relation.getOwnerType(), relation.getOwnerId()); + } + } } if (TimiJava.isNotEmpty(addIdList)) { - List roleRelationList = new ArrayList<>(); + List rolePermissionInheritList = new ArrayList<>(); long now = Time.now(); for (String childRoleId : addIdList) { - RoleRelation roleRelation = new RoleRelation(); - roleRelation.setId(UUID.randomUUID().toString()); - roleRelation.setParentRoleId(role.getId()); - roleRelation.setChildRoleId(childRoleId); - roleRelation.setCreatedAt(now); - roleRelationList.add(roleRelation); + RolePermissionInherit rolePermissionInherit = new RolePermissionInherit(); + rolePermissionInherit.setId(UUID.randomUUID().toString()); + rolePermissionInherit.setParentRoleId(role.getId()); + rolePermissionInherit.setChildRoleId(childRoleId); + fillRelationScope(rolePermissionInherit, childRoleMap.get(childRoleId), loginUserId); + rolePermissionInherit.setCreatedAt(now); + rolePermissionInheritList.add(rolePermissionInherit); } - roleRelationMapper.createBatch(roleRelationList); + rolePermissionInheritMapper.createBatch(rolePermissionInheritList); } } } - @Transactional(TimiServerDBConfig.ROLLBACKER) @Override - public void authorizeUserRole(UserRoleAuthorizeReq req) { - TimiException.required(req, "not found req"); - TimiException.required(req.getUserId(), "not found req.userId"); - if (req.getModuleCode() == null) { - throw new TimiException(TimiCode.ARG_MISS, "moduleCode"); - } - List bindingList = userRoleService.listByUserId(req.getUserId()); - List bindingRoleIdList = bindingList.stream().map(UserRole::getRoleId).toList(); - Set dbIdSet = mapper.selectByIdList(bindingRoleIdList).stream() - .filter(role -> req.getModuleCode() == role.getModuleCode()) - .map(Role::getId) - .collect(Collectors.toSet()); - Set newIdSet = new HashSet<>(); - if (TimiJava.isNotEmpty(req.getRoleIdList())) { - newIdSet.addAll(mapper.selectByIdList(req.getRoleIdList()).stream() - .filter(role -> req.getModuleCode() == role.getModuleCode()) - .map(Role::getId) - .toList()); - } - if (TimiJava.isNotEmpty(req.getRoleCodeList())) { - newIdSet.addAll(mapper.selectByModuleCodeAndCodeList(req.getModuleCode(), req.getRoleCodeList()).stream().map(Role::getId).toList()); - } - Set deleteIdList = new HashSet<>(dbIdSet); - deleteIdList.removeAll(newIdSet); - - Set addIdList = new HashSet<>(newIdSet); - addIdList.removeAll(dbIdSet); - - if (TimiJava.isNotEmpty(deleteIdList)) { - userRoleService.deleteByRoleIdList(req.getUserId(), new ArrayList<>(deleteIdList)); - } - if (TimiJava.isNotEmpty(addIdList)) { - List roleBindingList = new ArrayList<>(); - long now = Time.now(); - for (String roleId : addIdList) { - UserRole roleBinding = new UserRole(); - roleBinding.setId(UUID.randomUUID().toString()); - roleBinding.setUserId(req.getUserId()); - roleBinding.setRoleId(roleId); - roleBinding.setCreatedAt(now); - roleBindingList.add(roleBinding); - } - userRoleService.createBatch(roleBindingList); - } - } - - @Override - public List listByUserId(String userId) { - return mapper.selectByIdList(userRoleService.listRoleIdByUserId(userId)); - } - - @Override - public List listByUserId(String userId, ModuleCode moduleCode) { - TimiException.required(moduleCode, "not found moduleCode"); - return listByUserId(userId).stream() - .filter(role -> moduleCode == role.getModuleCode()) - .toList(); + public void delete(String id) { + authorizationBoundaryService.checkCanDeleteRole(userLoginService.getRequireLoginUserId(), id); + super.delete(id); } @Override @@ -221,25 +224,29 @@ public class RoleServiceImplement extends AbstractEntityService im } @Override - public List listAllCodeByUserId(String userId) { - List roleIdList = listByUserId(userId).stream().map(Role::getId).toList(); - List allRoleIdList = getAllRoleIdsIncludingChildren(roleIdList); - if (TimiJava.isEmpty(allRoleIdList)) { - return List.of(); - } - return mapper.selectByIdList(allRoleIdList).stream().map(role -> "%s:%s".formatted(role.getModuleCode().name(), role.getCode())).distinct().toList(); + public List listByIdList(Collection idList) { + return mapper.selectByIdList(new HashSet<>(idList)); } @Override - public List listAllPermissionByUserId(String userId) { - List roleIdList = listByUserId(userId).stream().map(Role::getId).toList(); - List allRoleIdList = getAllRoleIdsIncludingChildren(roleIdList); - return permissionService.listByRoleId(allRoleIdList); + public List listManageableRole(ModuleCode moduleCode) { + String loginUserId = userLoginService.getRequireLoginUserId(); + return authorizationScopeService.listManageableRole(loginUserId, moduleCode); + } + + @Override + public List listGrantableRole(ModuleCode moduleCode) { + String loginUserId = userLoginService.getRequireLoginUserId(); + return authorizationScopeService.listGrantableRole(loginUserId, moduleCode); } @Override public List listChildByParentRoleId(String parentRoleId) { - List childRoleIdList = roleRelationMapper.selectChildRoleIdListByParentRoleId(parentRoleId); + List childRoleIdList = rolePermissionInheritMapper.selectByParentRoleId(parentRoleId) + .stream() + .map(RolePermissionInherit::getChildRoleId) + .distinct() + .toList(); if (TimiJava.isEmpty(childRoleIdList)) { return new ArrayList<>(); } @@ -248,47 +255,282 @@ public class RoleServiceImplement extends AbstractEntityService im @Override public List listPermissionByRoleId(String roleId) { - List allRoleIdList = getAllRoleIdsIncludingChildren(List.of(roleId)); + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(userLoginService.getRequireLoginUserId(), List.of(roleId)); return permissionService.listByRoleId(allRoleIdList); } - /// 递归获取角色及其所有子角色的 ID 列表 - /// - /// @param roleIdList 角色 ID 列表 - /// @return 包含所有子角色的 ID 列表 - private List getAllRoleIdsIncludingChildren(List roleIdList) { - if (TimiJava.isEmpty(roleIdList)) { - return new ArrayList<>(); - } - Set allRoleIdSet = new HashSet<>(roleIdList); - List currentLevelRoleIdList = new ArrayList<>(roleIdList); - - while (TimiJava.isNotEmpty(currentLevelRoleIdList)) { - List childRoleIdList = roleRelationMapper.selectChildRoleIdListByParentRoleIdList(currentLevelRoleIdList); - if (TimiJava.isEmpty(childRoleIdList)) { - break; - } - List newChildRoleIdList = childRoleIdList.stream().filter(id -> !allRoleIdSet.contains(id)).toList(); - if (TimiJava.isEmpty(newChildRoleIdList)) { - break; - } - allRoleIdSet.addAll(newChildRoleIdList); - currentLevelRoleIdList = newChildRoleIdList; - } - return new ArrayList<>(allRoleIdSet); + @Override + public List listGrantableRolePermission(String roleId) { + String loginUserId = userLoginService.getRequireLoginUserId(); + Role role = mapper.selectById(roleId); + TimiException.required(role, "not found role"); + checkManageableRole(loginUserId, role); + Set candidateIdSet = listRoleDirectGrantablePermissionId(loginUserId, role); + return listPermissionById(candidateIdSet).stream() + .collect(Collectors.toMap(Permission::getId, Function.identity(), (oldValue, newValue) -> oldValue, LinkedHashMap::new)) + .values() + .stream() + .toList(); } - private String saveNameMultilingual(String langId, String name) { - if (TimiJava.isEmpty(name)) { - return langId; + @Override + public List listDelegationPermissionByRoleId(String roleId) { + return rolePermissionDelegationMapper.selectPermissionByRoleIdList(List.of(roleId)); + } + + @Override + public List listGrantableDelegationPermission(String roleId) { + String loginUserId = userLoginService.getRequireLoginUserId(); + Role role = mapper.selectById(roleId); + TimiException.required(role, "not found role"); + boolean hasParentRole = TimiJava.isNotEmpty(role.getParentRoleId()); + boolean systemAdmin = authorizationScopeService.isSystemAdmin(loginUserId); + Set candidateIdSet = isCoreDelegationRoot(role) && systemAdmin && !hasParentRole + ? Arrays.stream(ModuleCode.values()) + .flatMap(moduleCode -> permissionService.listByModuleCode(moduleCode).stream()) + .map(Permission::getId) + .collect(Collectors.toSet()) + : listDelegationParentPermissionId(roleId); + List candidateList = listPermissionById(candidateIdSet); + Set operatorGrantableIdSet = listOperatorGrantablePermissionId(loginUserId, candidateList); + return candidateList.stream() + .filter(permission -> role.getModuleCode() == permission.getModuleCode() || isCoreDelegationRoot(role) || isCoreAuthorizationPermission(permission)) + .filter(permission -> operatorGrantableIdSet.contains(permission.getId()) || systemAdmin) + .toList(); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void updateRoleDelegation(Role role) { + String loginUserId = userLoginService.getRequireLoginUserId(); + TimiException.required(role.getId(), "not found role.id"); + Set newIdSet = TimiJava.isEmpty(role.getDelegationPermissionIdList()) ? new HashSet<>() : new HashSet<>(role.getDelegationPermissionIdList()); + authorizationBoundaryService.checkCanUpdateRoleDelegation(loginUserId, role.getId(), newIdSet); + Set dbIdSet = rolePermissionDelegationMapper.selectByRoleId(role.getId()).stream().map(RolePermissionDelegation::getPermissionId).collect(Collectors.toSet()); + + Set deleteIdList = new HashSet<>(dbIdSet); + deleteIdList.removeAll(newIdSet); + + Set addIdList = new HashSet<>(newIdSet); + addIdList.removeAll(dbIdSet); + if (TimiJava.isNotEmpty(deleteIdList)) { + rolePermissionDelegationMapper.deleteByPermissionIdList(role.getId(), new ArrayList<>(deleteIdList)); } - if (TimiJava.isEmpty(langId)) { - return multilingualService.create(name); + if (TimiJava.isNotEmpty(addIdList)) { + List delegationList = new ArrayList<>(); + long now = Time.now(); + for (String permissionId : addIdList) { + RolePermissionDelegation delegation = new RolePermissionDelegation(); + delegation.setId(UUID.randomUUID().toString()); + delegation.setRoleId(role.getId()); + delegation.setPermissionId(permissionId); + delegation.setCreatedBy(loginUserId); + delegation.setCreatedAt(now); + delegationList.add(delegation); + } + rolePermissionDelegationMapper.createBatch(delegationList); } - Multilingual multilingual = new Multilingual(); - multilingual.setId(langId); - multilingual.setZhCN(name); - multilingualMapper.updateSelective(multilingual); - return langId; + logRoleAuthorizationChange("USER_ROLE_DELEGATION_PERMISSION_UPDATE", loginUserId, role.getId(), addIdList, deleteIdList); + } + + @Override + public List listGrantableParentRole(String roleId) { + String loginUserId = userLoginService.getRequireLoginUserId(); + Role targetRole = mapper.selectById(roleId); + TimiException.required(targetRole, "not found role"); + boolean systemAdmin = authorizationScopeService.isSystemAdmin(loginUserId); + Set manageableRoleIdSet = Arrays.stream(ModuleCode.values()) + .flatMap(moduleCode -> authorizationScopeService.listManageableRole(loginUserId, moduleCode).stream()) + .map(Role::getId) + .collect(Collectors.toSet()); + Set operatorRoleIdSet = listOperatorRoleId(loginUserId); + return mapper.selectAll() + .stream() + .filter(role -> !Objects.equals(role.getId(), targetRole.getId())) + .filter(role -> !isManagementDescendant(targetRole.getId(), role.getId())) + .filter(role -> systemAdmin || operatorRoleIdSet.contains(role.getId()) || manageableRoleIdSet.contains(role.getId())) + .collect(Collectors.toMap(Role::getId, Function.identity(), (oldValue, newValue) -> oldValue, LinkedHashMap::new)) + .values() + .stream() + .toList(); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void updateRoleParent(Role role) { + String loginUserId = userLoginService.getRequireLoginUserId(); + TimiException.required(role.getId(), "not found role.id"); + Role dbRole = mapper.selectById(role.getId()); + TimiException.required(dbRole, "not found role"); + authorizationBoundaryService.checkCanUpdateRoleParent(loginUserId, role.getId(), role.getParentRoleId()); + mapper.updateSelective(role); + Logger logger = new Logger(Logger.Module.USER, Logger.Level.INFO, "USER_ROLE_PARENT_UPDATE"); + logger.setUserId(loginUserId); + logger.setContent("roleId=%s;oldParentRoleId=%s;newParentRoleId=%s".formatted(role.getId(), dbRole.getParentRoleId(), role.getParentRoleId())); + logger.setResult(role.getId()); + loggerService.createSync(logger); + } + + public List listAllRoleIdIncludingChildren(Collection idList) { + return roleHierarchyService.listAllRoleIdIncludingChildren(idList); + } + + private void fillRoleDefault(Role role) { + role.setBuiltin(TimiJava.defaultIfNull(role.getBuiltin(), false)); + role.setProtectedRole(TimiJava.defaultIfNull(role.getProtectedRole(), false)); + role.setOwnerType(TimiJava.defaultIfNull(role.getOwnerType(), AuthOwnerType.MODULE)); + role.setOwnerId(TimiJava.defaultIfEmpty(role.getOwnerId(), role.getModuleCode() == null ? null : role.getModuleCode().name())); + } + + private void fillRelationScope(RolePermissionInherit relation, Role childRole, String createdBy) { + if (childRole == null) { + return; + } + relation.setOwnerType(childRole.getOwnerType()); + relation.setOwnerId(childRole.getOwnerId()); + relation.setCreatedBy(createdBy); + } + + private boolean canTouchRelation(String userId, Role parentRole, RolePermissionInherit relation) { + if (authorizationScopeService.isSystemAdmin(userId)) { + return true; + } + if (relation.getOwnerType() == AuthOwnerType.MODULE && parentRole != null && Objects.equals(relation.getOwnerId(), parentRole.getModuleCode().name())) { + return false; + } + ModuleResourceScopeResolver resolver = moduleResourceScopeResolverList.stream() + .filter(item -> parentRole != null && parentRole.getModuleCode() == item.moduleCode()) + .findFirst() + .orElse(null); + return resolver != null && resolver.canUseOwnerScope(userId, relation.getOwnerType(), relation.getOwnerId()); + } + + private boolean isCoreDelegationRoot(Role role) { + return role != null + && ModuleCode.CORE == role.getModuleCode() + && (com.imyeyu.api.bean.CoreRoleCode.SYSTEM.name().equals(role.getCode()) || com.imyeyu.api.bean.CoreRoleCode.ADMIN.name().equals(role.getCode())); + } + + private boolean isCoreAuthorizationPermission(Permission permission) { + return permission != null && ModuleCode.CORE == permission.getModuleCode(); + } + + private Set listOperatorGrantablePermissionId(String loginUserId, List permissionList) { + if (TimiJava.isEmpty(permissionList)) { + return Set.of(); + } + return permissionList.stream() + .map(Permission::getModuleCode) + .distinct() + .flatMap(moduleCode -> authorizationScopeService.listGrantablePermission(loginUserId, moduleCode).stream()) + .map(Permission::getId) + .collect(Collectors.toSet()); + } + + private Set listRoleDirectGrantablePermissionId(String loginUserId, Role role) { + boolean systemAdmin = authorizationScopeService.isSystemAdmin(loginUserId); + if (systemAdmin && isCoreDelegationRoot(role) && TimiJava.isEmpty(role.getParentRoleId())) { + return Arrays.stream(ModuleCode.values()) + .flatMap(moduleCode -> permissionService.listByModuleCode(moduleCode).stream()) + .map(Permission::getId) + .collect(Collectors.toSet()); + } + TimiException.requiredTrue(TimiJava.isNotEmpty(role.getParentRoleId()), "目标角色缺少管理父级"); + Set parentDelegationPermissionIdSet = listDelegationParentPermissionId(role.getId()); + if (systemAdmin) { + return parentDelegationPermissionIdSet; + } + List parentDelegationPermissionList = listPermissionById(parentDelegationPermissionIdSet); + Set operatorGrantablePermissionIdSet = listOperatorGrantablePermissionId(loginUserId, parentDelegationPermissionList); + return parentDelegationPermissionIdSet.stream() + .filter(operatorGrantablePermissionIdSet::contains) + .collect(Collectors.toSet()); + } + + private List listPermissionById(Set permissionIdSet) { + if (TimiJava.isEmpty(permissionIdSet)) { + return List.of(); + } + return Arrays.stream(ModuleCode.values()) + .flatMap(moduleCode -> permissionService.listByModuleCode(moduleCode).stream()) + .filter(permission -> permissionIdSet.contains(permission.getId())) + .toList(); + } + + private Set listDelegationParentPermissionId(String roleId) { + Role role = mapper.selectById(roleId); + if (role == null || TimiJava.isEmpty(role.getParentRoleId())) { + return Set.of(); + } + return rolePermissionDelegationMapper.selectPermissionByRoleIdList(List.of(role.getParentRoleId())) + .stream() + .map(Permission::getId) + .collect(Collectors.toSet()); + } + + private Set listOperatorRoleId(String loginUserId) { + List roleIdList = userRoleMapper.selectByUserIdList(List.of(loginUserId)).stream().map(UserRole::getRoleId).toList(); + if (TimiJava.isEmpty(roleIdList)) { + return Set.of(); + } + return new HashSet<>(roleHierarchyService.listAllRoleIdIncludingChildren(loginUserId, roleIdList)); + } + + private boolean hasRoleBaseField(Role role) { + return role.getModuleCode() != null + || role.getCode() != null + || role.getNameLangId() != null + || role.getDescription() != null + || role.getBuiltin() != null + || role.getProtectedRole() != null + || role.getParentRoleId() != null + || role.getOwnerType() != null + || role.getOwnerId() != null + || role.getCreatedBy() != null + || role.getName() != null; + } + + private void checkManageableRole(String loginUserId, Role role) { + Set manageableRoleIdSet = authorizationScopeService.listManageableRole(loginUserId, role.getModuleCode()) + .stream() + .map(Role::getId) + .collect(Collectors.toSet()); + TimiException.requiredTrue(manageableRoleIdSet.contains(role.getId()), "角色不在可管理范围内:%s".formatted(role.getCode())); + } + + private boolean isManagementDescendant(String parentRoleId, String childRoleId) { + List childRoleList = mapper.selectByParentRoleId(parentRoleId); + for (Role childRole : childRoleList) { + if (childRoleId.equals(childRole.getId()) || isManagementDescendant(childRole.getId(), childRoleId)) { + return true; + } + } + return false; + } + + private void logRoleAuthorizationChange(String operation, String loginUserId, String roleId, Collection addIdList, Collection deleteIdList) { + if (TimiJava.isEmpty(addIdList) && TimiJava.isEmpty(deleteIdList)) { + return; + } + Logger logger = new Logger(Logger.Module.USER, Logger.Level.INFO, operation); + logger.setUserId(loginUserId); + logger.setContent("roleId=%s;addCount=%d;deleteCount=%d;addIdList=%s;deleteIdList=%s".formatted( + roleId, + TimiJava.isEmpty(addIdList) ? 0 : addIdList.size(), + TimiJava.isEmpty(deleteIdList) ? 0 : deleteIdList.size(), + TimiJava.isEmpty(addIdList) ? "" : String.join(",", addIdList), + TimiJava.isEmpty(deleteIdList) ? "" : String.join(",", deleteIdList) + )); + logger.setResult(roleId); + loggerService.createSync(logger); + } + + private String relationKey(RolePermissionInherit relation) { + return "%s|%s|%s".formatted(relation.getChildRoleId(), relation.getOwnerType(), relation.getOwnerId()); + } + + private String relationKey(String childRoleId, Role childRole) { + return "%s|%s|%s".formatted(childRoleId, childRole == null ? null : childRole.getOwnerType(), childRole == null ? null : childRole.getOwnerId()); } } + diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java index 8977985..5972d9a 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java @@ -9,6 +9,7 @@ import com.imyeyu.api.modules.user.bean.BootstrapData; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.service.RoleService; import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.api.modules.user.service.UserRoleService; import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.api.modules.user.vo.LoginRequest; import com.imyeyu.api.modules.user.vo.LoginResponse; @@ -45,6 +46,7 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava { private final UserService userService; private final RoleService roleService; private final SettingService settingService; + private final UserRoleService userRoleService; private final AttachmentService attachmentService; private final Redis redisUserToken; @@ -86,15 +88,18 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava { result.setUser(userService.get(userId)); User user = result.getUser(); - user.setPermissionList(roleService.listAllPermissionCodeByUserId(user.getId())); - user.setRoleList(roleService.listAllCodeByUserId(user.getId())); + user.setLastLoginIP(TimiSpring.getRequestIP()); + user.setLastLoginAt(Time.now()); + userService.update(user); + + user.setPermissionList(userRoleService.listAllPermissionCodeByUserId(user.getId())); + user.setRoleList(userRoleService.listAllRoleCodeByUserId(user.getId())); user.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.USER, user.getId())); return result; } @Override - public User getLoginUser() { - String token = TimiSpring.getToken(); + public User getLoginUser(String token) { if (TimiJava.isEmpty(token)) { return null; } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserRoleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserRoleServiceImplement.java index 1ed6dc2..8c8a56d 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserRoleServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserRoleServiceImplement.java @@ -1,14 +1,38 @@ package com.imyeyu.api.modules.user.service.implement; +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.user.bean.BootstrapData; +import com.imyeyu.api.modules.user.entity.Permission; +import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.UserRole; import com.imyeyu.api.modules.user.mapper.UserRoleMapper; +import com.imyeyu.api.modules.user.service.AuthorizationBoundaryService; +import com.imyeyu.api.modules.user.service.PermissionService; +import com.imyeyu.api.modules.user.service.RoleHierarchyService; +import com.imyeyu.api.modules.user.service.RoleService; +import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserRoleService; +import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeRequest; +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 com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; /// 用户角色服务实现 /// @@ -18,6 +42,12 @@ import java.util.List; @RequiredArgsConstructor public class UserRoleServiceImplement extends AbstractEntityService implements UserRoleService { + private final RoleService roleService; + private final PermissionService permissionService; + private final UserLoginService userLoginService; + private final AuthorizationBoundaryService authorizationBoundaryService; + private final RoleHierarchyService roleHierarchyService; + private final UserRoleMapper mapper; @Override @@ -30,6 +60,59 @@ public class UserRoleServiceImplement extends AbstractEntityService bindingList = listByUserId(req.getUserId()); + List bindingRoleIdList = bindingList.stream().map(UserRole::getRoleId).toList(); + Set dbIdSet = roleService.listByIdList(bindingRoleIdList) + .stream() + .filter(role -> req.getModuleCode() == role.getModuleCode()) + .map(Role::getId) + .collect(Collectors.toSet()); + Set newIdSet = new HashSet<>(); + if (TimiJava.isNotEmpty(req.getRoleIdList())) { + newIdSet.addAll(roleService.listByIdList(req.getRoleIdList()).stream().filter(role -> req.getModuleCode() == role.getModuleCode()).map(Role::getId).toList()); + } + Set deleteIdList = new HashSet<>(dbIdSet); + deleteIdList.removeAll(newIdSet); + + Set addIdList = new HashSet<>(newIdSet); + addIdList.removeAll(dbIdSet); + + String loginUserId = userLoginService.getRequireLoginUserId(); + authorizationBoundaryService.checkCanRevokeRoles(loginUserId, req.getUserId(), deleteIdList); + authorizationBoundaryService.checkCanGrantRoles(loginUserId, req.getUserId(), addIdList); + + if (TimiJava.isNotEmpty(deleteIdList)) { + for (String roleId : deleteIdList) { + UserRole userRole = new UserRole(); + userRole.setUserId(req.getUserId()); + userRole.setRoleId(roleId); + BootstrapData.checkProtectedUserRole(userRole); + } + deleteByRoleIdList(req.getUserId(), new ArrayList<>(deleteIdList)); + } + if (TimiJava.isNotEmpty(addIdList)) { + List roleBindingList = new ArrayList<>(); + long now = Time.now(); + for (String roleId : addIdList) { + UserRole roleBinding = new UserRole(); + roleBinding.setId(UUID.randomUUID().toString()); + roleBinding.setUserId(req.getUserId()); + roleBinding.setRoleId(roleId); + roleBinding.setCreatedAt(now); + roleBindingList.add(roleBinding); + } + createBatch(roleBindingList); + } + } + @Override public List listByUserId(String userId) { UserRole example = new UserRole(); @@ -37,6 +120,57 @@ public class UserRoleServiceImplement extends AbstractEntityService listByUserIdList(Collection userIdList) { + return mapper.selectByUserIdList(userIdList); + } + + @Override + public List listRoleByUserId(String userId) { + List userRoleList = listByUserId(userId); + return roleService.listByIdList(userRoleList.stream().map(UserRole::getRoleId).toList()); + } + + @Override + public List listRoleByUserId(ModuleCode moduleCode, String userId) { + TimiException.required(moduleCode, "not found moduleCode"); + return listRoleByUserId(userId).stream().filter(role -> moduleCode == role.getModuleCode()).toList(); + } + + @Override + public List listAllRoleCodeByUserId(String userId) { + List roleIdList = listRoleByUserId(userId).stream().map(Role::getId).toList(); + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(userId, roleIdList); + if (TimiJava.isEmpty(allRoleIdList)) { + return List.of(); + } + return roleService.listByIdList(allRoleIdList).stream().map(role -> "%s:%s".formatted(role.getModuleCode().name(), role.getCode())).distinct().toList(); + } + + @Override + public List listAllPermissionByUserId(String userId) { + List roleIdList = listRoleByUserId(userId).stream().map(Role::getId).toList(); + List allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(userId, roleIdList); + return permissionService.listByRoleId(allRoleIdList); + } + + @Override + public Map> mapRoleListByUserIdList(Collection idList) { + if (TimiJava.isEmpty(idList)) { + return Map.of(); + } + List userRoleList = listByUserIdList(idList); + if (TimiJava.isEmpty(userRoleList)) { + return Map.of(); + } + Map roleMap = roleService.listByIdList(userRoleList.stream().map(UserRole::getRoleId).distinct().toList()) + .stream() + .collect(Collectors.toMap(Role::getId, Function.identity())); + return userRoleList.stream() + .filter(userRole -> roleMap.containsKey(userRole.getRoleId())) + .collect(Collectors.groupingBy(UserRole::getUserId, Collectors.mapping(userRole -> roleMap.get(userRole.getRoleId()), Collectors.toList()))); + } + @Override public void deleteByRoleIdList(String userId, List roleIdList) { mapper.deleteByRoleIdList(userId, roleIdList); diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java index 33a79c3..0f0ba8f 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java @@ -11,11 +11,18 @@ import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Digest; import com.imyeyu.utils.Regex; +import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + /** * 用户管理服务实现 * @@ -27,8 +34,6 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor(onConstructor_ = {@Lazy}) public class UserServiceImplement extends AbstractEntityService implements UserService, TimiJava { - private static final String REGEX_PHONE_NO = "^1\\d{10}$"; - private final UserMapper mapper; @Override @@ -51,7 +56,7 @@ public class UserServiceImplement extends AbstractEntityService im } { if (TimiJava.isNotEmpty(user.getPhoneNo())) { - validatePhoneNo(user.getPhoneNo()); + TimiException.requiredTrue(user.isValidPhone(), "invalid phone"); User byPhoneNo = getByVerifiedPhoneNo(user.getPhoneNo()); TimiException.requiredNull(byPhoneNo, "existed phone number"); } @@ -65,7 +70,6 @@ public class UserServiceImplement extends AbstractEntityService im dbUser.setPassword(Digest.argon2id(user.getPassword())); dbUser.setEmail(user.getEmail()); dbUser.setPhoneNo(user.getPhoneNo()); - dbUser.setPhoneNoVerifyAt(user.getPhoneNoVerifyAt()); dbUser.setExp(TimiJava.defaultIfNull(user.getExp(), 0)); super.create(dbUser); user.setId(dbUser.getId()); @@ -88,7 +92,7 @@ public class UserServiceImplement extends AbstractEntityService im TimiException.requiredTrue(byEmail == null || byEmail.getId().equals(dbUser.getId()), "existed email"); } if (TimiJava.isNotEmpty(phoneNo)) { - validatePhoneNo(phoneNo); + TimiException.requiredTrue(user.isValidPhone(), "invalid phone"); if (!phoneNo.equals(dbUser.getPhoneNo())) { User byPhoneNo = getByVerifiedPhoneNo(phoneNo); TimiException.requiredTrue(byPhoneNo == null || byPhoneNo.getId().equals(dbUser.getId()), "existed phone number"); @@ -138,7 +142,22 @@ public class UserServiceImplement extends AbstractEntityService im return mapper.selectByVerifiedPhoneNo(phoneNo); } - private void validatePhoneNo(String phoneNo) { - TimiException.requiredTrue(Regex.isMatch(REGEX_PHONE_NO, phoneNo), "invalid phone number"); + @Override + public void verifyEmail(String id) { + User user = get(id); + user.setEmailVerifyAt(Time.now()); + update(user); + } + + @Override + public void verifyPhoneNo(String id) { + User user = get(id); + user.setPhoneNoVerifyAt(Time.now()); + update(user); + } + + @Override + public Map mapByIdList(Collection idList) { + return mapper.listByIdList(new HashSet<>(idList)).stream().collect(Collectors.toMap(User::getId, Function.identity())); } } diff --git a/src/main/java/com/imyeyu/api/modules/user/vo/role/UserRoleAuthorizeReq.java b/src/main/java/com/imyeyu/api/modules/user/vo/role/UserRoleAuthorizeRequest.java similarity index 79% rename from src/main/java/com/imyeyu/api/modules/user/vo/role/UserRoleAuthorizeReq.java rename to src/main/java/com/imyeyu/api/modules/user/vo/role/UserRoleAuthorizeRequest.java index 1187ddf..3c70a32 100644 --- a/src/main/java/com/imyeyu/api/modules/user/vo/role/UserRoleAuthorizeReq.java +++ b/src/main/java/com/imyeyu/api/modules/user/vo/role/UserRoleAuthorizeRequest.java @@ -10,13 +10,11 @@ import java.util.List; /// @author 夜雨 /// @since 2026-07-17 14:16 @Data -public class UserRoleAuthorizeReq { +public class UserRoleAuthorizeRequest { private String userId; private ModuleCode moduleCode; private List roleIdList; - - private List roleCodeList; } diff --git a/src/main/java/com/imyeyu/api/util/BaseXSSFBuilder.java b/src/main/java/com/imyeyu/api/util/BaseXSSFBuilder.java new file mode 100644 index 0000000..74d9afb --- /dev/null +++ b/src/main/java/com/imyeyu/api/util/BaseXSSFBuilder.java @@ -0,0 +1,65 @@ +package com.imyeyu.api.util; + +import com.imyeyu.api.modules.user.bean.Gender; +import com.imyeyu.java.TimiJava; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; + +import java.io.IOException; +import java.util.List; + +/// +/// +/// @author 夜雨 +/// @since 2026-08-04 15:57 +public abstract class BaseXSSFBuilder { + + public abstract byte[] build(List list) throws IOException; + + protected CellStyle createTitleStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 14); + style.setFont(font); + return style; + } + + protected CellStyle createHeaderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + return style; + } + + protected void appendRow(Sheet sheet, int rowIndex, CellStyle style, String... values) { + Row row = sheet.createRow(rowIndex); + for (int index = 0; index < values.length; index++) { + row.createCell(index).setCellValue(values[index]); + if (style != null) { + row.getCell(index).setCellStyle(style); + } + } + } + + protected String text(Object obj) { + if (obj == null) { + return ""; + } + return TimiJava.defaultIfEmpty(obj.toString(), ""); + } + + protected String gender(Gender gender) { + if (gender == null) { + return ""; + } + return switch (gender) { + case MALE -> "男"; + case FEMALE -> "女"; + }; + } +} diff --git a/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java b/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java index 5850f68..4e1c281 100644 --- a/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java +++ b/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java @@ -1,5 +1,7 @@ package com.imyeyu.api.util; +import com.imyeyu.api.bean.CorePermissionCode; +import com.imyeyu.api.bean.CoreRoleCode; import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.common.bean.ImageType; @@ -14,16 +16,18 @@ import com.imyeyu.api.modules.user.bean.BuiltinRoleCode; import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.RolePermission; -import com.imyeyu.api.modules.user.entity.RoleRelation; +import com.imyeyu.api.modules.user.entity.RolePermissionDelegation; +import com.imyeyu.api.modules.user.entity.RolePermissionInherit; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.UserRole; import com.imyeyu.api.modules.user.mapper.PermissionMapper; import com.imyeyu.api.modules.user.mapper.RoleMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionDelegationMapper; +import com.imyeyu.api.modules.user.mapper.RolePermissionInheritMapper; import com.imyeyu.api.modules.user.mapper.RolePermissionMapper; -import com.imyeyu.api.modules.user.mapper.RoleRelationMapper; import com.imyeyu.api.modules.user.mapper.UserRoleMapper; import com.imyeyu.api.modules.user.service.PermissionService; -import com.imyeyu.api.modules.user.service.RoleRelationService; +import com.imyeyu.api.modules.user.service.RolePermissionInheritService; import com.imyeyu.api.modules.user.service.RoleService; import com.imyeyu.api.modules.user.service.UserRoleService; import com.imyeyu.api.modules.user.service.UserService; @@ -53,13 +57,14 @@ public class InitBuiltinUser implements ApplicationRunner { private final LoggerService loggerService; private final UserRoleService userRoleService; private final PermissionService permissionService; - private final RoleRelationService roleRelationService; + private final RolePermissionInheritService rolePermissionInheritService; private final RoleMapper roleMapper; private final UserRoleMapper userRoleMapper; private final PermissionMapper permissionMapper; private final RolePermissionMapper rolePermissionMapper; - private final RoleRelationMapper roleRelationMapper; + private final RolePermissionDelegationMapper rolePermissionDelegationMapper; + private final RolePermissionInheritMapper rolePermissionInheritMapper; /** * 初始化内建数据 @@ -73,8 +78,10 @@ public class InitBuiltinUser implements ApplicationRunner { // 先同步所有内建权限和角色,再补齐继承与默认授权关系 Map permissionMap = initBuiltinPermissionMap(); Map roleMap = initBuiltinRoleMap(); - bindBuiltinRoleRelation(roleMap); + bindBuiltinRolePermissionInherit(roleMap); + bindBuiltinRoleParent(roleMap); bindBuiltinPermission(roleMap, permissionMap); + bindBuiltinPermissionDelegation(roleMap, permissionMap); Role systemRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(BootstrapData.SYS_ROLE_MODULE_CODE, BootstrapData.SYS_ROLE_CODE)); // 初始化系统用户 @@ -94,9 +101,7 @@ public class InitBuiltinUser implements ApplicationRunner { String logContent = "初始化 %s 自举用户成功,请立即修改密码:%s".formatted(BootstrapData.SYS_USER_NAME, password); - Logger logger = new Logger(); - logger.setModule(Logger.Module.SYSTEM); - logger.setOperation("INITIALIZE_SYSTEM_USER"); + Logger logger = Logger.info(Logger.Module.SYSTEM, "INITIALIZE_SYSTEM_USER"); logger.setUserId(user.getId()); logger.setContent(logContent); loggerService.create(logger); @@ -131,8 +136,12 @@ public class InitBuiltinUser implements ApplicationRunner { permission.setModuleCode(item.getModuleCode()); permission.setCode(item.getValue()); permission.setName(item.getDefaultName()); + fillBuiltinPermission(permission, item); permissionService.create(permission); permission = permissionMapper.selectByExample(example); + } else { + fillBuiltinPermission(permission, item); + permissionMapper.updateSelective(permission); } permissionMap.put(BuiltinAuthRegistry.toPermissionKey(item.getModuleCode(), item.getValue()), permission); } @@ -155,8 +164,12 @@ public class InitBuiltinUser implements ApplicationRunner { role.setModuleCode(item.getModuleCode()); role.setCode(code); role.setName(item.getDefaultName()); + fillBuiltinRole(role, item); roleService.create(role); role = roleMapper.selectByExample(example); + } else { + fillBuiltinRole(role, item); + roleMapper.updateSelective(role); } roleMap.put(BuiltinAuthRegistry.toRoleKey(item.getModuleCode(), code), role); } @@ -166,7 +179,7 @@ public class InitBuiltinUser implements ApplicationRunner { /// 按角色定义补齐父子角色关系 /// /// @param roleMap 已同步的角色映射 - private void bindBuiltinRoleRelation(Map roleMap) { + private void bindBuiltinRolePermissionInherit(Map roleMap) { for (BuiltinRoleCode item : BuiltinAuthRegistry.ROLE_CODE_LIST) { if (TimiJava.isEmpty(item.getParentCode())) { continue; @@ -176,15 +189,26 @@ public class InitBuiltinUser implements ApplicationRunner { if (parentRole == null || childRole == null) { continue; } - RoleRelation relation = new RoleRelation(); + RolePermissionInherit relation = new RolePermissionInherit(); relation.setParentRoleId(parentRole.getId()); relation.setChildRoleId(childRole.getId()); - if (roleRelationMapper.selectByExample(relation) == null) { - roleRelationService.create(relation); + relation.setOwnerType(item.getOwnerType()); + relation.setOwnerId(item.getOwnerId()); + if (rolePermissionInheritMapper.selectByExample(relation) == null) { + rolePermissionInheritService.create(relation); } } } + /// 补齐内置角色管理父级 + /// + /// @param roleMap 已同步的角色映射 + private void bindBuiltinRoleParent(Map roleMap) { + setRoleParent(roleMap, ModuleCode.CORE, CoreRoleCode.ADMIN.name(), ModuleCode.CORE, CoreRoleCode.SYSTEM.name()); + setRoleParent(roleMap, ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name(), ModuleCode.CORE, CoreRoleCode.ADMIN.name()); + setRoleParent(roleMap, ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name(), ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name()); + } + /// 按权限定义补齐 SYSTEM 和 ADMIN 的默认权限 /// /// @param roleMap 已同步的角色映射 @@ -192,6 +216,7 @@ public class InitBuiltinUser implements ApplicationRunner { private void bindBuiltinPermission(Map roleMap, Map permissionMap) { Role systemRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(BootstrapData.SYS_ROLE_MODULE_CODE, BootstrapData.SYS_ROLE_CODE)); Role adminRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(BootstrapData.ADMIN_ROLE_MODULE_CODE, BootstrapData.ADMIN_ROLE_CODE)); + Role gaoGlobalManagerRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())); Role gaoStoreManagerRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name())); for (BuiltinPermissionCode item : BuiltinAuthRegistry.PERMISSION_CODE_LIST) { Permission permission = permissionMap.get(BuiltinAuthRegistry.toPermissionKey(item.getModuleCode(), item.getValue())); @@ -204,12 +229,50 @@ public class InitBuiltinUser implements ApplicationRunner { if (item.isGrantToAdmin() && adminRole != null) { createRolePermissionIfAbsent(adminRole.getId(), permission.getId()); } + if (item instanceof CorePermissionCode corePermission && isGrantToModuleRoleManager(corePermission) && gaoGlobalManagerRole != null) { + createRolePermissionIfAbsent(gaoGlobalManagerRole.getId(), permission.getId()); + } + if (item instanceof CorePermissionCode corePermission && isGrantToModuleRoleManager(corePermission) && gaoStoreManagerRole != null) { + createRolePermissionIfAbsent(gaoStoreManagerRole.getId(), permission.getId()); + } + if (item instanceof GaoPermissionCode gaoPermission && gaoPermission.isGrantToGlobalManager() && gaoGlobalManagerRole != null) { + createRolePermissionIfAbsent(gaoGlobalManagerRole.getId(), permission.getId()); + } if (item instanceof GaoPermissionCode gaoPermission && gaoPermission.isGrantToStoreManager() && gaoStoreManagerRole != null) { createRolePermissionIfAbsent(gaoStoreManagerRole.getId(), permission.getId()); } } } + /// 按内置策略补齐角色转授权限 + /// + /// @param roleMap 已同步的角色映射 + /// @param permissionMap 已同步的权限映射 + private void bindBuiltinPermissionDelegation(Map roleMap, Map permissionMap) { + Role systemRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(BootstrapData.SYS_ROLE_MODULE_CODE, BootstrapData.SYS_ROLE_CODE)); + Role adminRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(BootstrapData.ADMIN_ROLE_MODULE_CODE, BootstrapData.ADMIN_ROLE_CODE)); + Role gaoGlobalManagerRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())); + Role gaoStoreManagerRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name())); + for (BuiltinPermissionCode item : BuiltinAuthRegistry.PERMISSION_CODE_LIST) { + Permission permission = permissionMap.get(BuiltinAuthRegistry.toPermissionKey(item.getModuleCode(), item.getValue())); + if (permission == null) { + continue; + } + if (systemRole != null) { + createRolePermissionDelegationIfAbsent(systemRole.getId(), permission.getId()); + } + if (adminRole != null) { + createRolePermissionDelegationIfAbsent(adminRole.getId(), permission.getId()); + } + if (item instanceof GaoPermissionCode gaoPermission && gaoPermission.isGrantToStoreManager() && gaoGlobalManagerRole != null) { + createRolePermissionDelegationIfAbsent(gaoGlobalManagerRole.getId(), permission.getId()); + } + if (item instanceof GaoPermissionCode gaoPermission && gaoPermission.isGrantToStoreManager() && gaoStoreManagerRole != null) { + createRolePermissionDelegationIfAbsent(gaoStoreManagerRole.getId(), permission.getId()); + } + } + } + /// 角色权限关系不存在时才创建 /// /// @param roleId 角色 ID @@ -226,4 +289,70 @@ public class InitBuiltinUser implements ApplicationRunner { rolePermission.setPermissionId(permissionId); rolePermissionMapper.insert(rolePermission); } + + /// 角色转授权限关系不存在时才创建 + /// + /// @param roleId 角色 ID + /// @param permissionId 权限 ID + private void createRolePermissionDelegationIfAbsent(String roleId, String permissionId) { + RolePermissionDelegation example = new RolePermissionDelegation(); + example.setRoleId(roleId); + example.setPermissionId(permissionId); + if (rolePermissionDelegationMapper.selectByExample(example) != null) { + return; + } + RolePermissionDelegation rolePermissionDelegation = new RolePermissionDelegation(); + rolePermissionDelegation.setRoleId(roleId); + rolePermissionDelegation.setPermissionId(permissionId); + rolePermissionDelegationMapper.insert(rolePermissionDelegation); + } + + /// 填充内置权限元数据 + /// + /// @param permission 权限 + /// @param item 内置权限定义 + private void fillBuiltinPermission(Permission permission, BuiltinPermissionCode item) { + permission.setBuiltin(true); + permission.setProtectedPermission(item.isProtected()); + permission.setOwnerType(item.getOwnerType()); + permission.setOwnerId(item.getOwnerId()); + } + + /// 填充内置角色元数据 + /// + /// @param role 角色 + /// @param item 内置角色定义 + private void fillBuiltinRole(Role role, BuiltinRoleCode item) { + role.setBuiltin(true); + role.setProtectedRole(item.isProtected()); + role.setOwnerType(item.getOwnerType()); + role.setOwnerId(item.getOwnerId()); + } + + /// 设置角色管理父级 + /// + /// @param roleMap 已同步的角色映射 + /// @param moduleCode 角色模块 + /// @param roleCode 角色代码 + /// @param parentModuleCode 父角色模块 + /// @param parentRoleCode 父角色代码 + private void setRoleParent(Map roleMap, ModuleCode moduleCode, String roleCode, ModuleCode parentModuleCode, String parentRoleCode) { + Role role = roleMap.get(BuiltinAuthRegistry.toRoleKey(moduleCode, roleCode)); + Role parentRole = roleMap.get(BuiltinAuthRegistry.toRoleKey(parentModuleCode, parentRoleCode)); + if (role == null || parentRole == null || parentRole.getId().equals(role.getParentRoleId())) { + return; + } + role.setParentRoleId(parentRole.getId()); + roleMapper.updateSelective(role); + } + + /// 判断权限是否默认授予模块角色管理员 + /// + /// @param permissionCode 核心权限 + /// @return true 为授予 + private boolean isGrantToModuleRoleManager(CorePermissionCode permissionCode) { + return permissionCode.name().startsWith("ROLE_") + || permissionCode.name().startsWith("USER_ROLE_") + || CorePermissionCode.PERMISSION_READ == permissionCode; + } } diff --git a/src/main/resources/db/migration/timiserver/V23__rename_gao_event_tables.sql b/src/main/resources/db/migration/timiserver/V23__rename_gao_event_tables.sql new file mode 100644 index 0000000..2e4ec83 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V23__rename_gao_event_tables.sql @@ -0,0 +1,54 @@ +RENAME TABLE `gao_registration_event` TO `gao_event`; + +ALTER TABLE `gao_event` + DROP KEY `idx_gao_registration_event_status`, + ADD KEY `idx_gao_event_status` (`status`, `deleted_at`, `sort`, `created_at`); + +RENAME TABLE `gao_registration_event_role_relation` TO `gao_event_role_relation`; + +ALTER TABLE `gao_event_role_relation` + DROP KEY `idx_gao_registration_event_role_relation_registration_event_id`, + CHANGE COLUMN `registration_event_id` `event_id` VARCHAR(36) NOT NULL COMMENT '登记事件 ID', + ADD KEY `idx_gao_event_role_relation_event_id` (`event_id`); + +RENAME TABLE `gao_registration_event_record` TO `gao_event_record`; + +ALTER TABLE `gao_event_record` + DROP KEY `idx_gao_registration_event_record_customer_id`, + DROP KEY `idx_gao_registration_event_record_registration_event_id`, + DROP KEY `idx_gao_registration_event_record_registered_at`, + CHANGE COLUMN `registration_event_id` `event_id` VARCHAR(36) NOT NULL COMMENT '登记事件 ID', + ADD KEY `idx_gao_event_record_customer_id` (`customer_id`), + ADD KEY `idx_gao_event_record_event_id` (`event_id`), + ADD KEY `idx_gao_event_record_registered_at` (`registered_at`); + +UPDATE `attachment` +SET `biz_type` = 'GAO_EVENT_RECORD' +WHERE `biz_type` = 'GAO_REGISTRATION_EVENT_RECORD'; + +UPDATE `permission` +SET `code` = CASE `code` + WHEN 'REGISTRATION_EVENT:CREATE' THEN 'EVENT:CREATE' + WHEN 'REGISTRATION_EVENT:READ' THEN 'EVENT:READ' + WHEN 'REGISTRATION_EVENT:UPDATE' THEN 'EVENT:UPDATE' + WHEN 'REGISTRATION_EVENT:DELETE' THEN 'EVENT:DELETE' + WHEN 'REGISTRATION_EVENT_RECORD:CREATE' THEN 'EVENT_RECORD:CREATE' + WHEN 'REGISTRATION_EVENT_RECORD:READ' THEN 'EVENT_RECORD:READ' + WHEN 'REGISTRATION_EVENT_RECORD:EXPORT' THEN 'EVENT_RECORD:EXPORT' + WHEN 'REGISTRATION_EVENT_RECORD:UPDATE' THEN 'EVENT_RECORD:UPDATE' + WHEN 'REGISTRATION_EVENT_RECORD:DELETE' THEN 'EVENT_RECORD:DELETE' + ELSE `code` +END +WHERE + `module_code` = 'GAO' + AND `code` IN ( + 'REGISTRATION_EVENT:CREATE', + 'REGISTRATION_EVENT:READ', + 'REGISTRATION_EVENT:UPDATE', + 'REGISTRATION_EVENT:DELETE', + 'REGISTRATION_EVENT_RECORD:CREATE', + 'REGISTRATION_EVENT_RECORD:READ', + 'REGISTRATION_EVENT_RECORD:EXPORT', + 'REGISTRATION_EVENT_RECORD:UPDATE', + 'REGISTRATION_EVENT_RECORD:DELETE' + ); diff --git a/src/main/resources/db/migration/timiserver/V24__add_gao_store_scope.sql b/src/main/resources/db/migration/timiserver/V24__add_gao_store_scope.sql new file mode 100644 index 0000000..3474b47 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V24__add_gao_store_scope.sql @@ -0,0 +1,192 @@ +-- 新增门店主表。active_code / active_name 沿用项目现有软删除唯一索引方案: +-- 未删除数据参与唯一约束,已删除数据生成 NULL,不阻塞后续重建同编码或同名门店。 +CREATE TABLE IF NOT EXISTS `gao_store` ( + `id` VARCHAR(36) NOT NULL, + `code` VARCHAR(64) NOT NULL COMMENT '门店编码', + `active_code` VARCHAR(64) GENERATED ALWAYS AS ( + CASE WHEN `deleted_at` IS NULL THEN NULLIF(TRIM(`code`), '') ELSE NULL END + ) STORED COMMENT '未删除门店编码' INVISIBLE, + `name` VARCHAR(64) NOT NULL COMMENT '门店名称', + `active_name` VARCHAR(64) GENERATED ALWAYS AS ( + CASE WHEN `deleted_at` IS NULL THEN NULLIF(TRIM(`name`), '') ELSE NULL END + ) STORED COMMENT '未删除门店名称' INVISIBLE, + `status` VARCHAR(32) NOT NULL COMMENT '门店状态', + `telephone` VARCHAR(32) NULL COMMENT '电话', + `address` VARCHAR(255) NULL COMMENT '地址', + `remark` TEXT NULL COMMENT '备注', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_store_active_code` (`active_code`), + UNIQUE KEY `uk_gao_store_active_name` (`active_name`), + KEY `idx_gao_store_status_sort` (`status`, `deleted_at`, `sort`, `created_at`) +) COMMENT='GAO 门店'; + +-- 旧数据来自单门店模型,迁移时全部归入固定默认门店。 +-- 使用固定 ID 而不是 UUID(),避免迁移中断后排查或手工重跑时产生多个默认门店。 +SET @gao_default_store_id = '00000000-0000-0000-0000-000000000001'; +SET @gao_now = ROUND(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000); + +-- Flyway 正常不会重复执行版本脚本;这里保留 ON DUPLICATE KEY,方便本地或预发排查时手工重放片段。 +INSERT INTO `gao_store` ( + `id`, + `code`, + `name`, + `status`, + `telephone`, + `address`, + `remark`, + `sort`, + `created_at` +) VALUES ( + @gao_default_store_id, + 'DEFAULT', + '默认门店', + 'ACTIVE', + NULL, + NULL, + '系统迁移自动创建', + 0, + @gao_now +) ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `status` = VALUES(`status`), + `updated_at` = @gao_now; + +-- 门店营业日历是连续登记统计的事实维度。 +-- business_day_no 只给营业日使用,休息日 business_day_no 为 NULL。 +-- 后续统计连续登记时按 business_day_no 连续,不按自然日连续。 +CREATE TABLE IF NOT EXISTS `gao_store_business_day` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `date_value` INT NOT NULL COMMENT '日期值,格式为 yyyyMMdd', + `active_date_value` INT GENERATED ALWAYS AS ( + CASE WHEN `deleted_at` IS NULL THEN `date_value` ELSE NULL END + ) STORED COMMENT '未删除日期值' INVISIBLE, + `open_status` VARCHAR(32) NOT NULL COMMENT '营业状态', + `business_day_no` BIGINT NULL COMMENT '营业日递增序号,仅营业日有值', + `active_business_day_no` BIGINT GENERATED ALWAYS AS ( + CASE WHEN `deleted_at` IS NULL AND `open_status` = 'OPEN' THEN `business_day_no` ELSE NULL END + ) STORED COMMENT '未删除营业日序号' INVISIBLE, + `reason` VARCHAR(255) NULL COMMENT '原因', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_store_business_day_active_date` (`store_id`, `active_date_value`), + UNIQUE KEY `uk_gao_store_business_day_active_no` (`store_id`, `active_business_day_no`), + KEY `idx_gao_store_business_day_open_no` (`store_id`, `open_status`, `business_day_no`) +) COMMENT='GAO 门店营业日'; + +-- 客户归属门店。先允许 NULL,加字段后回填,再改 NOT NULL,避免已有数据直接违反约束。 +ALTER TABLE `gao_customer` + ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `id`; + +UPDATE `gao_customer` +SET `store_id` = @gao_default_store_id +WHERE `store_id` IS NULL; + +ALTER TABLE `gao_customer` + MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + -- 客户编码、姓名从全局唯一改为门店内唯一。 + DROP KEY `uk_gao_customer_active_code`, + DROP KEY `uk_gao_customer_active_name`, + ADD UNIQUE KEY `uk_gao_customer_store_active_code` (`store_id`, `active_code`), + ADD UNIQUE KEY `uk_gao_customer_store_active_name` (`store_id`, `active_name`), + ADD KEY `idx_gao_customer_store_created_at` (`store_id`, `deleted_at`, `created_at`); + +-- 登记事件归属门店。历史事件统一归入默认门店。 +ALTER TABLE `gao_event` + ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `id`; + +UPDATE `gao_event` +SET `store_id` = @gao_default_store_id +WHERE `store_id` IS NULL; + +ALTER TABLE `gao_event` + MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + ADD KEY `idx_gao_event_store_status` (`store_id`, `status`, `deleted_at`, `sort`, `created_at`); + +-- 登记记录是事实表,需要保存门店和营业日快照。 +-- store_id:优先跟随历史登记事件所属门店,找不到事件时兜底默认门店。 +-- registered_date_value:由 registered_at 计算 yyyyMMdd,避免查询时反复做时间函数。 +-- business_day_no:按门店营业日历回填,用于连续登记统计。 +ALTER TABLE `gao_event_record` + ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `customer_id`, + ADD COLUMN `registered_date_value` INT NULL COMMENT '登记日期值,格式为 yyyyMMdd' AFTER `registered_at`, + ADD COLUMN `business_day_no` BIGINT NULL COMMENT '营业日递增序号' AFTER `registered_date_value`; + +UPDATE `gao_event_record` r +LEFT JOIN `gao_event` e ON r.`event_id` = e.`id` +SET + r.`store_id` = COALESCE(e.`store_id`, @gao_default_store_id), + r.`registered_date_value` = CAST(DATE_FORMAT(FROM_UNIXTIME(r.`registered_at` / 1000), '%Y%m%d') AS UNSIGNED) +WHERE + r.`store_id` IS NULL + OR r.`registered_date_value` IS NULL; + +-- 以历史登记记录中实际发生过登记的日期生成营业日。 +-- 这是最平滑的迁移口径:有登记的日期一定视为营业日;没有登记的历史日期不主动生成休息日或营业日。 +-- 如果门店历史休息日需要展示,可在迁移后通过营业日接口补录 CLOSED 数据,不影响已有登记记录。 +INSERT INTO `gao_store_business_day` ( + `id`, + `store_id`, + `date_value`, + `open_status`, + `business_day_no`, + `reason`, + `created_at` +) +SELECT + UUID(), + d.`store_id`, + d.`registered_date_value`, + 'OPEN', + ROW_NUMBER() OVER (PARTITION BY d.`store_id` ORDER BY d.`registered_date_value`), + '系统迁移按历史登记日期生成', + @gao_now +FROM ( + SELECT DISTINCT + `store_id`, + `registered_date_value` + FROM `gao_event_record` + WHERE + `store_id` IS NOT NULL + AND `registered_date_value` IS NOT NULL +) d; + +-- 把营业日序号快照回填到历史登记记录。 +-- 后续即使营业日历被调整,历史记录仍保留迁移时的事实快照;如需重算,应做显式重算脚本。 +UPDATE `gao_event_record` r +LEFT JOIN `gao_store_business_day` d ON r.`store_id` = d.`store_id` AND r.`registered_date_value` = d.`date_value` AND d.`deleted_at` IS NULL +SET r.`business_day_no` = d.`business_day_no` +WHERE r.`business_day_no` IS NULL; + +ALTER TABLE `gao_event_record` + MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + MODIFY COLUMN `registered_date_value` INT NOT NULL COMMENT '登记日期值,格式为 yyyyMMdd', + MODIFY COLUMN `business_day_no` BIGINT NOT NULL COMMENT '营业日递增序号', + ADD KEY `idx_gao_event_record_store_registered_at` (`store_id`, `deleted_at`, `registered_at`), + ADD KEY `idx_gao_event_record_store_date` (`store_id`, `registered_date_value`, `deleted_at`), + ADD KEY `idx_gao_event_record_store_customer_time` (`store_id`, `customer_id`, `deleted_at`, `registered_at`), + ADD KEY `idx_gao_event_record_store_event_time` (`store_id`, `event_id`, `deleted_at`, `registered_at`), + ADD KEY `idx_gao_event_record_store_customer_day` (`store_id`, `customer_id`, `business_day_no`); + +-- GAO 用户归属门店。用户可在不同门店分别存在一条 GAO 用户记录。 +-- 注意:当前用户角色关系仍是用户维度,不是门店维度;不同门店不同角色需要后续扩展 user_role 作用域。 +ALTER TABLE `gao_user` + ADD COLUMN `store_id` VARCHAR(36) NULL COMMENT '门店 ID' AFTER `id`, + ADD COLUMN `active_user_id` VARCHAR(36) GENERATED ALWAYS AS ( + CASE WHEN `deleted_at` IS NULL THEN `user_id` ELSE NULL END + ) STORED COMMENT '未删除用户 ID' INVISIBLE; + +UPDATE `gao_user` +SET `store_id` = @gao_default_store_id +WHERE `store_id` IS NULL; + +ALTER TABLE `gao_user` + MODIFY COLUMN `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + ADD UNIQUE KEY `uk_gao_user_store_active_user` (`store_id`, `active_user_id`), + ADD KEY `idx_gao_user_store_enabled` (`store_id`, `enabled`, `deleted_at`, `created_at`); diff --git a/src/main/resources/db/migration/timiserver/V25__restrict_gao_user_to_single_store.sql b/src/main/resources/db/migration/timiserver/V25__restrict_gao_user_to_single_store.sql new file mode 100644 index 0000000..df810d9 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V25__restrict_gao_user_to_single_store.sql @@ -0,0 +1,8 @@ +-- V24 已经上线为“用户可在多个门店分别存在 GAO 用户记录”。 +-- 现在业务改为一个用户只能归属一个门店,所以不能再改 V24,必须通过 V25 切换唯一约束。 + +-- 如果线上已经存在同一 user_id 绑定多个未删除门店的数据,下面新增唯一索引会失败。 +-- 这是正确行为:这种数据无法自动判断保留哪个门店,必须人工先清理重复 GAO 用户记录。 +ALTER TABLE `gao_user` + DROP KEY `uk_gao_user_store_active_user`, + ADD UNIQUE KEY `uk_gao_user_active_user` (`active_user_id`); diff --git a/src/main/resources/db/migration/timiserver/V26__add_authorization_boundary_fields.sql b/src/main/resources/db/migration/timiserver/V26__add_authorization_boundary_fields.sql new file mode 100644 index 0000000..6173d6e --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V26__add_authorization_boundary_fields.sql @@ -0,0 +1,39 @@ +ALTER TABLE `role` + ADD COLUMN `builtin` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为系统内置角色' AFTER `description`, + ADD COLUMN `protected_role` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为受保护角色' AFTER `builtin`, + ADD COLUMN `parent_role_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '管理父角色 ID' AFTER `protected_role`, + ADD COLUMN `owner_type` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'MODULE' COMMENT '归属类型' AFTER `parent_role_id`, + ADD COLUMN `owner_id` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '归属 ID' AFTER `owner_type`, + ADD COLUMN `created_by` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建人用户 ID' AFTER `owner_id`, + ADD INDEX `idx_role_parent_role_id` (`parent_role_id`), + ADD INDEX `idx_role_owner` (`owner_type`, `owner_id`); + +ALTER TABLE `permission` + ADD COLUMN `builtin` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为系统内置权限' AFTER `description`, + ADD COLUMN `protected_permission` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为受保护权限' AFTER `builtin`, + ADD COLUMN `owner_type` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'MODULE' COMMENT '归属类型' AFTER `protected_permission`, + ADD COLUMN `owner_id` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '归属 ID' AFTER `owner_type`, + ADD COLUMN `created_by` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建人用户 ID' AFTER `owner_id`, + ADD INDEX `idx_permission_owner` (`owner_type`, `owner_id`); + +RENAME TABLE `role_relation` TO `role_permission_inherit`; + +ALTER TABLE `role_permission_inherit` + DROP INDEX `idx_role_relation`, + DROP INDEX `idx_child_role_id`, + ADD INDEX `idx_role_permission_inherit` (`parent_role_id`, `child_role_id`), + ADD INDEX `idx_role_permission_inherit_child_role_id` (`child_role_id`); + +ALTER TABLE `role_permission_inherit` + ADD COLUMN `owner_type` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'MODULE' COMMENT '归属类型' AFTER `child_role_id`, + ADD COLUMN `owner_id` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '归属 ID' AFTER `owner_type`, + ADD COLUMN `created_by` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建人用户 ID' AFTER `owner_id`, + ADD INDEX `idx_role_permission_inherit_owner` (`owner_type`, `owner_id`); + +UPDATE `role_permission_inherit` inherit +INNER JOIN `role` child_role ON inherit.`child_role_id` = child_role.`id` +SET inherit.`owner_id` = child_role.`module_code` +WHERE + inherit.`owner_type` = 'MODULE' + AND inherit.`owner_id` IS NULL + AND inherit.`deleted_at` IS NULL; diff --git a/src/main/resources/db/migration/timiserver/V27__add_role_permission_delegation.sql b/src/main/resources/db/migration/timiserver/V27__add_role_permission_delegation.sql new file mode 100644 index 0000000..871fb22 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V27__add_role_permission_delegation.sql @@ -0,0 +1,11 @@ +CREATE TABLE `role_permission_delegation` ( + `id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `role_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色 ID', + `permission_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '允许转授的权限 ID', + `created_by` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建人用户 ID', + `created_at` BIGINT(20) COMMENT '创建时间', + `deleted_at` BIGINT(20) COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role_permission_delegation` (`role_id` ASC, `permission_id` ASC) USING BTREE, + INDEX `idx_permission_id` (`permission_id` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色转授权限' ROW_FORMAT = Dynamic; diff --git a/src/main/resources/db/migration/timiserver/V28__deduplicate_permission_inherit_and_delegation.sql b/src/main/resources/db/migration/timiserver/V28__deduplicate_permission_inherit_and_delegation.sql new file mode 100644 index 0000000..a5d7a96 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V28__deduplicate_permission_inherit_and_delegation.sql @@ -0,0 +1,40 @@ +UPDATE `role_permission_delegation` target +JOIN ( + SELECT + id, + ROW_NUMBER() OVER ( + PARTITION BY role_id, permission_id + ORDER BY created_at DESC, id DESC + ) AS rn + FROM `role_permission_delegation` + WHERE deleted_at IS NULL +) duplicate ON duplicate.id = target.id +SET target.deleted_at = UNIX_TIMESTAMP() * 1000 +WHERE duplicate.rn > 1; + +ALTER TABLE `role_permission_delegation` + ADD COLUMN `active_key` TINYINT(1) GENERATED ALWAYS AS (IF(`deleted_at` IS NULL, 1, NULL)) STORED COMMENT '未删除唯一键'; + +CREATE UNIQUE INDEX `uk_role_permission_delegation` + ON `role_permission_delegation` (`role_id`, `permission_id`, `active_key`); + +UPDATE `role_permission_inherit` target +JOIN ( + SELECT + id, + ROW_NUMBER() OVER ( + PARTITION BY parent_role_id, child_role_id, owner_type, IFNULL(owner_id, '') + ORDER BY created_at DESC, id DESC + ) AS rn + FROM `role_permission_inherit` + WHERE deleted_at IS NULL +) duplicate ON duplicate.id = target.id +SET target.deleted_at = UNIX_TIMESTAMP() * 1000 +WHERE duplicate.rn > 1; + +ALTER TABLE `role_permission_inherit` + ADD COLUMN `active_key` TINYINT(1) GENERATED ALWAYS AS (IF(`deleted_at` IS NULL, 1, NULL)) STORED COMMENT '未删除唯一键', + ADD COLUMN `owner_key` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (IFNULL(`owner_id`, '')) STORED COMMENT '归属唯一键'; + +CREATE UNIQUE INDEX `uk_role_permission_inherit` + ON `role_permission_inherit` (`parent_role_id`, `child_role_id`, `owner_type`, `owner_key`, `active_key`); diff --git a/src/main/resources/db/migration/timiserver/V29__create_gao_customer_point_tables.sql b/src/main/resources/db/migration/timiserver/V29__create_gao_customer_point_tables.sql new file mode 100644 index 0000000..0e2f43e --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V29__create_gao_customer_point_tables.sql @@ -0,0 +1,72 @@ +CREATE TABLE IF NOT EXISTS `gao_point_account` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `active_customer_id` VARCHAR(36) GENERATED ALWAYS AS ( + CASE WHEN `deleted_at` IS NULL THEN `customer_id` END + ) STORED COMMENT '未删除客户 ID' INVISIBLE, + `balance` BIGINT NOT NULL DEFAULT 0 COMMENT '当前可用积分余额', + `total_earned` BIGINT NOT NULL DEFAULT 0 COMMENT '累计获得积分', + `total_revoked` BIGINT NOT NULL DEFAULT 0 COMMENT '累计撤销积分', + `total_adjusted` BIGINT NOT NULL DEFAULT 0 COMMENT '累计人工调整积分', + `version` BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_point_account_customer` (`store_id`, `active_customer_id`), + KEY `idx_gao_point_account_customer_id` (`customer_id`), + KEY `idx_gao_point_account_store_balance` (`store_id`, `deleted_at`, `balance`) +) COMMENT='GAO 客户积分账户'; + +CREATE TABLE IF NOT EXISTS `gao_point_rule` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `event_id` VARCHAR(36) NOT NULL COMMENT '登记事件 ID', + `name` VARCHAR(64) NOT NULL COMMENT '规则名称', + `status` VARCHAR(32) NOT NULL COMMENT '规则状态', + `trigger_type` VARCHAR(32) NOT NULL COMMENT '触发类型', + `point_mode` VARCHAR(32) NOT NULL COMMENT '积分值模式', + `point_value` BIGINT NULL COMMENT '固定积分值', + `condition_json` JSON NULL COMMENT '条件配置', + `begin_at` BIGINT NULL COMMENT '生效开始时间戳,毫秒', + `end_at` BIGINT NULL COMMENT '生效结束时间戳,毫秒', + `priority` INT NOT NULL DEFAULT 0 COMMENT '优先级,数值小优先', + `exclusive` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'true 为命中后停止继续匹配后续规则', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_point_rule_match` (`store_id`, `event_id`, `status`, `deleted_at`, `priority`), + KEY `idx_gao_point_rule_type` (`trigger_type`, `point_mode`) +) COMMENT='GAO 客户积分规则'; + +CREATE TABLE IF NOT EXISTS `gao_point_ledger` ( + `id` VARCHAR(36) NOT NULL, + `account_id` VARCHAR(36) NOT NULL COMMENT '客户积分账户 ID', + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `change_type` VARCHAR(32) NOT NULL COMMENT '变化类型', + `direction` VARCHAR(16) NOT NULL COMMENT '方向', + `point_delta` BIGINT NOT NULL COMMENT '本次变化积分,增加为正,减少为负', + `balance_before` BIGINT NOT NULL COMMENT '变化前余额', + `balance_after` BIGINT NOT NULL COMMENT '变化后余额', + `rule_id` VARCHAR(36) NULL COMMENT '积分规则 ID', + `event_id` VARCHAR(36) NULL COMMENT '登记事件 ID', + `event_record_id` VARCHAR(36) NULL COMMENT '登记记录 ID', + `source_payload` JSON NULL COMMENT '来源快照', + `idempotency_key` VARCHAR(128) NOT NULL COMMENT '幂等键', + `remark` TEXT NULL COMMENT '备注', + `operator_user_id` VARCHAR(36) NULL COMMENT '操作用户 ID', + `occurred_at` BIGINT NOT NULL COMMENT '发生时间', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_point_ledger_idempotency` (`idempotency_key`), + KEY `idx_gao_point_ledger_account_time` (`account_id`, `deleted_at`, `occurred_at`), + KEY `idx_gao_point_ledger_customer_time` (`store_id`, `customer_id`, `deleted_at`, `occurred_at`), + KEY `idx_gao_point_ledger_event_record` (`event_record_id`, `change_type`), + KEY `idx_gao_point_ledger_rule_time` (`rule_id`, `occurred_at`) +) COMMENT='GAO 客户积分流水'; diff --git a/src/main/resources/mapper/timi-server/AttachmentMapper.xml b/src/main/resources/mapper/timi-server/AttachmentMapper.xml index e06d291..65bbae8 100644 --- a/src/main/resources/mapper/timi-server/AttachmentMapper.xml +++ b/src/main/resources/mapper/timi-server/AttachmentMapper.xml @@ -14,7 +14,7 @@ - AND 1 = 0 + AND FALSE AND `deleted_at` IS NULL AND (`destroy_at` IS NULL OR UNIX_TIMESTAMP() < `destroy_at`) @@ -24,6 +24,35 @@ #{attachType} - ORDER BY `created_at` ASC + ORDER BY `created_at` + + diff --git a/src/main/resources/mapper/timi-server/GaoCustomerMapper.xml b/src/main/resources/mapper/timi-server/GaoCustomerMapper.xml index b7db501..ae69942 100644 --- a/src/main/resources/mapper/timi-server/GaoCustomerMapper.xml +++ b/src/main/resources/mapper/timi-server/GaoCustomerMapper.xml @@ -1,99 +1,32 @@ - - 1 = 1 - AND `deleted_at` IS NULL - - AND ( - `code` LIKE CONCAT('%', #{keyword}, '%') - OR `name` LIKE CONCAT('%', #{keyword}, '%') - OR `telephone` LIKE CONCAT('%', #{keyword}, '%') - OR `address` LIKE CONCAT('%', #{keyword}, '%') - OR `disease` LIKE CONCAT('%', #{keyword}, '%') - OR CAST(`conditioning_part` AS CHAR) LIKE CONCAT('%', #{keyword}, '%') - ) - - - AND `introducer_customer_id` = #{introducerCustomerId} - - - - - - - - - - - - - - - UPDATE `gao_customer` - SET - `invited_count` = GREATEST(IFNULL(`invited_count`, 0) + #{delta}, 0), - `updated_at` = UNIX_TIMESTAMP() * 1000 - WHERE - `id` = #{customerId} - AND `deleted_at` IS NULL - - - SELECT CAST(DATE_FORMAT(FROM_UNIXTIME(`created_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`, COUNT(1) AS `newCustomerCount` FROM `gao_customer` WHERE - `created_at` >= #{beginCreatedAt} + `store_id` = #{storeId} + AND `created_at` >= #{beginCreatedAt} AND `created_at` <= #{endCreatedAt} AND `deleted_at` IS NULL GROUP BY DATE_FORMAT(FROM_UNIXTIME(`created_at` / 1000), '%Y%m%d') @@ -105,17 +38,19 @@ COUNT(1) FROM `gao_customer` WHERE - `created_at` < #{beginCreatedAt} + `store_id` = #{storeId} + AND `created_at` < #{beginCreatedAt} AND `deleted_at` IS NULL - SELECT IFNULL(CAST(`gender` AS CHAR), 'UNKNOWN') AS `genderCode`, COUNT(1) AS `count` FROM `gao_customer` WHERE - `deleted_at` IS NULL + `store_id` = #{storeId} + AND `deleted_at` IS NULL GROUP BY IFNULL(CAST(`gender` AS CHAR), 'UNKNOWN') diff --git a/src/main/resources/mapper/timi-server/GaoRegistrationEventMapper.xml b/src/main/resources/mapper/timi-server/GaoEventMapper.xml similarity index 63% rename from src/main/resources/mapper/timi-server/GaoRegistrationEventMapper.xml rename to src/main/resources/mapper/timi-server/GaoEventMapper.xml index 9709039..9b08997 100644 --- a/src/main/resources/mapper/timi-server/GaoRegistrationEventMapper.xml +++ b/src/main/resources/mapper/timi-server/GaoEventMapper.xml @@ -1,12 +1,12 @@ - - SELECT * - FROM `gao_registration_event` + FROM `gao_event` WHERE - 1 = 1 + TRUE AND `id` IN @@ -14,20 +14,21 @@ - AND 1 = 0 + AND FALSE AND `deleted_at` IS NULL - SELECT * - FROM `gao_registration_event` + FROM `gao_event` WHERE - (`status` IS NULL OR `status` = 'ACTIVE') + `store_id` = #{storeId} + AND `status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}' AND (`begin_at` IS NULL OR `begin_at` <= #{now}) AND (`end_at` IS NULL OR #{now} <= `end_at`) AND `deleted_at` IS NULL - ORDER BY `sort` ASC, `created_at` ASC + ORDER BY `sort`, `created_at` diff --git a/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml new file mode 100644 index 0000000..796b272 --- /dev/null +++ b/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml @@ -0,0 +1,295 @@ + + + + + + + + + UPDATE `gao_event_record` + SET + `business_day_no` = #{businessDayNo}, + `updated_at` = ROUND(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000) + WHERE + `store_id` = #{storeId} + AND `registered_date_value` = #{registeredDateValue} + AND `deleted_at` IS NULL + + + + FROM `gao_event_record` r + LEFT JOIN `gao_customer` c ON r.`customer_id` = c.`id` + + + TRUE + + AND r.`registered_at` >= #{beginAt} + + + AND r.`registered_at` <= #{endAt} + + AND r.`deleted_at` IS NULL + + AND r.`store_id` = #{storeId} + + + AND r.`customer_id` = #{customerId} + + + AND r.`event_id` IN + + #{eventId} + + + + AND ( + c.`code` LIKE CONCAT('%', #{keyword}, '%') + OR c.`name` LIKE CONCAT('%', #{keyword}, '%') + OR r.`remark` LIKE CONCAT('%', #{keyword}, '%') + OR r.`operator_user_id` LIKE CONCAT('%', #{keyword}, '%') + ) + + + + + + + r.`deleted_at` IS NULL + AND c.`deleted_at` IS NULL + + AND r.`store_id` = #{storeId} + + + AND r.`customer_id` = #{customerId} + + + AND r.`registered_at` >= #{beginAt} + + + AND r.`registered_at` <= #{endAt} + + + AND r.`event_id` IN + + #{eventId} + + + + + + + + r.`store_id` = #{storeId} + AND r.`registered_at` >= #{beginAt} + AND r.`registered_at` <= #{endAt} + AND r.`deleted_at` IS NULL + + AND r.`event_id` IN + + #{eventId} + + + + + + + + r.`store_id` = #{storeId} + AND r.`customer_id` = #{customerId} + AND r.`registered_at` >= #{beginAt} + AND r.`registered_at` <= #{endAt} + AND r.`deleted_at` IS NULL + + AND r.`event_id` IN + + #{eventId} + + + + + + diff --git a/src/main/resources/mapper/timi-server/GaoEventRoleRelationMapper.xml b/src/main/resources/mapper/timi-server/GaoEventRoleRelationMapper.xml new file mode 100644 index 0000000..410bb9e --- /dev/null +++ b/src/main/resources/mapper/timi-server/GaoEventRoleRelationMapper.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/src/main/resources/mapper/timi-server/GaoPointAccountMapper.xml b/src/main/resources/mapper/timi-server/GaoPointAccountMapper.xml new file mode 100644 index 0000000..c2000b0 --- /dev/null +++ b/src/main/resources/mapper/timi-server/GaoPointAccountMapper.xml @@ -0,0 +1,44 @@ + + + + + + + INSERT INTO `gao_point_account` ( + `id`, `store_id`, `customer_id`, `balance`, `total_earned`, `total_revoked`, `total_adjusted`, `version`, `created_at`, `updated_at`, `deleted_at` + ) VALUES ( + #{account.id}, #{account.storeId}, #{account.customerId}, #{account.balance}, #{account.totalEarned}, #{account.totalRevoked}, #{account.totalAdjusted}, #{account.version}, #{account.createdAt}, #{account.updatedAt}, #{account.deletedAt} + ) + ON DUPLICATE KEY UPDATE `id` = `id` + + + + + + UPDATE `gao_point_account` + SET + `balance` = `balance` + #{delta}, + `total_earned` = `total_earned` + #{totalEarnedDelta}, + `total_revoked` = `total_revoked` + #{totalRevokedDelta}, + `total_adjusted` = `total_adjusted` + #{totalAdjustedDelta}, + `version` = `version` + 1, + `updated_at` = #{updatedAt} + WHERE `id` = #{id} + AND `balance` + #{delta} >= 0 + AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/GaoPointLedgerMapper.xml b/src/main/resources/mapper/timi-server/GaoPointLedgerMapper.xml new file mode 100644 index 0000000..3a4d6ac --- /dev/null +++ b/src/main/resources/mapper/timi-server/GaoPointLedgerMapper.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + `deleted_at` IS NULL + + AND `store_id` = #{storeId} + + + AND `customer_id` = #{customerId} + + + AND `account_id` = #{accountId} + + + AND `change_type` = #{changeType} + + + AND `occurred_at` >= #{beginAt} + + + AND `occurred_at` <= #{endAt} + + + + + + + diff --git a/src/main/resources/mapper/timi-server/GaoPointRuleMapper.xml b/src/main/resources/mapper/timi-server/GaoPointRuleMapper.xml new file mode 100644 index 0000000..c5434d5 --- /dev/null +++ b/src/main/resources/mapper/timi-server/GaoPointRuleMapper.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/src/main/resources/mapper/timi-server/GaoRegistrationEventRecordMapper.xml b/src/main/resources/mapper/timi-server/GaoRegistrationEventRecordMapper.xml deleted file mode 100644 index 12e0902..0000000 --- a/src/main/resources/mapper/timi-server/GaoRegistrationEventRecordMapper.xml +++ /dev/null @@ -1,389 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - r.`registered_at` >= #{req.beginRegisteredAt} - AND r.`registered_at` <= #{req.endRegisteredAt} - AND r.`deleted_at` IS NULL - - AND r.`customer_id` = #{req.customerId} - - - AND r.`registration_event_id` = #{req.registrationEventId} - - - AND r.`registration_event_id` IN - - #{registrationEventId} - - - - AND ( - c.`code` LIKE CONCAT('%', #{req.keyword}, '%') - OR c.`name` LIKE CONCAT('%', #{req.keyword}, '%') - OR e.`name` LIKE CONCAT('%', #{req.keyword}, '%') - OR r.`remark` LIKE CONCAT('%', #{req.keyword}, '%') - OR u.`name` LIKE CONCAT('%', #{req.keyword}, '%') - OR u.`nick` LIKE CONCAT('%', #{req.keyword}, '%') - OR r.`operator_user_id` LIKE CONCAT('%', #{req.keyword}, '%') - ) - - - - - FROM `gao_registration_event_record` r - LEFT JOIN `gao_customer` c ON r.`customer_id` = c.`id` - LEFT JOIN `gao_registration_event` e ON r.`registration_event_id` = e.`id` - LEFT JOIN `user` u ON r.`operator_user_id` = u.`id` - - - - - - - - - - diff --git a/src/main/resources/mapper/timi-server/GaoStoreMapper.xml b/src/main/resources/mapper/timi-server/GaoStoreMapper.xml new file mode 100644 index 0000000..1bf3891 --- /dev/null +++ b/src/main/resources/mapper/timi-server/GaoStoreMapper.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/src/main/resources/mapper/timi-server/PermissionMapper.xml b/src/main/resources/mapper/timi-server/PermissionMapper.xml new file mode 100644 index 0000000..3a14d3b --- /dev/null +++ b/src/main/resources/mapper/timi-server/PermissionMapper.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/src/main/resources/mapper/timi-server/RoleMapper.xml b/src/main/resources/mapper/timi-server/RoleMapper.xml index ddda0ca..2ae6f1b 100644 --- a/src/main/resources/mapper/timi-server/RoleMapper.xml +++ b/src/main/resources/mapper/timi-server/RoleMapper.xml @@ -1,12 +1,21 @@ + - - + + diff --git a/src/main/resources/mapper/timi-server/RolePermissionDelegationMapper.xml b/src/main/resources/mapper/timi-server/RolePermissionDelegationMapper.xml new file mode 100644 index 0000000..5a00cd6 --- /dev/null +++ b/src/main/resources/mapper/timi-server/RolePermissionDelegationMapper.xml @@ -0,0 +1,53 @@ + + + + + + + INSERT INTO `role_permission_delegation` ( + id, + role_id, + permission_id, + created_by, + created_at + ) VALUES + + (#{item.id}, #{item.roleId}, #{item.permissionId}, #{item.createdBy}, #{item.createdAt}) + + + + UPDATE `role_permission_delegation` + SET `deleted_at` = UNIX_TIMESTAMP() + WHERE + `role_id` = #{roleId} + AND `permission_id` IN + + #{item} + + AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/RoleRelationMapper.xml b/src/main/resources/mapper/timi-server/RolePermissionInheritMapper.xml similarity index 53% rename from src/main/resources/mapper/timi-server/RoleRelationMapper.xml rename to src/main/resources/mapper/timi-server/RolePermissionInheritMapper.xml index dea0b6e..8b7510a 100644 --- a/src/main/resources/mapper/timi-server/RoleRelationMapper.xml +++ b/src/main/resources/mapper/timi-server/RolePermissionInheritMapper.xml @@ -1,10 +1,10 @@ - - SELECT * - FROM `role_relation` + FROM `role_permission_inherit` WHERE `parent_role_id` = #{parentRoleId} AND `deleted_at` IS NULL @@ -12,15 +12,26 @@ + - INSERT INTO `role_relation` ( + INSERT INTO `role_permission_inherit` ( id, parent_role_id, child_role_id, + owner_type, + owner_id, + created_by, created_at ) VALUES - - (#{item.id}, #{item.parentRoleId}, #{item.childRoleId}, #{item.createdAt}) + + (#{item.id}, #{item.parentRoleId}, #{item.childRoleId}, #{item.ownerType}, #{item.ownerId}, #{item.createdBy}, #{item.createdAt}) - UPDATE `role_relation` - SET `deleted_at` = UNIX_TIMESTAMP() + UPDATE `role_permission_inherit` + SET `deleted_at` = UNIX_TIMESTAMP() * 1000 WHERE `parent_role_id` = #{parentRoleId} AND `child_role_id` IN #{item} + AND `owner_type` = #{ownerType} + + AND `owner_id` = #{ownerId} + + + AND `owner_id` IS NULL + diff --git a/src/main/resources/mapper/timi-server/RolePermissionMapper.xml b/src/main/resources/mapper/timi-server/RolePermissionMapper.xml index 8efab7c..3c223c7 100644 --- a/src/main/resources/mapper/timi-server/RolePermissionMapper.xml +++ b/src/main/resources/mapper/timi-server/RolePermissionMapper.xml @@ -13,7 +13,7 @@ AND `role_permission`.`deleted_at` IS NULL diff --git a/src/main/resources/mapper/timi-server/UserRoleMapper.xml b/src/main/resources/mapper/timi-server/UserRoleMapper.xml index 55ce81d..e3e8a16 100644 --- a/src/main/resources/mapper/timi-server/UserRoleMapper.xml +++ b/src/main/resources/mapper/timi-server/UserRoleMapper.xml @@ -12,6 +12,23 @@ (#{item.id}, #{item.userId}, #{item.roleId}, #{item.createdAt}) + UPDATE `user_role` SET `deleted_at` = UNIX_TIMESTAMP()