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/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 3f25cb5..845e349 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 @@ -11,6 +11,16 @@ import lombok.Getter; /// @since 2026-07-26 public enum GaoPermissionCode implements BuiltinPermissionCode { + STORE_CREATE("STORE:CREATE", "门店创建", false, true), + + STORE_READ("STORE:READ", "门店读取", true, true), + + STORE_EXPORT("STORE:EXPORT", "门店导出", false, true), + + STORE_UPDATE("STORE:UPDATE", "门店修改", true, true), + + STORE_DELETE("STORE:DELETE", "门店删除", false, true), + CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建"), CUSTOMER_READ("CUSTOMER:READ", "客户读取"), @@ -58,14 +68,23 @@ public enum GaoPermissionCode implements BuiltinPermissionCode { @Getter final boolean grantToStoreManager; + /// `true` 为初始化时授权给全局管理员角色 + @Getter + final boolean grantToGlobalManager; + GaoPermissionCode(String value, String defaultName) { - this(value, defaultName, false); + this(value, defaultName, false, false); } GaoPermissionCode(String value, String defaultName, boolean grantToStoreManager) { + this(value, defaultName, grantToStoreManager, false); + } + + GaoPermissionCode(String value, String defaultName, boolean grantToStoreManager, boolean grantToGlobalManager) { this.value = value; this.defaultName = defaultName; this.grantToStoreManager = grantToStoreManager; + this.grantToGlobalManager = grantToGlobalManager; } @Override 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..04e0b9b 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,16 @@ import lombok.AllArgsConstructor; @AllArgsConstructor public enum GaoRoleCode implements BuiltinRoleCode { - /// 店长 - STORE_MANAGER("店长"), + /// 全局管理员 + GLOBAL_MANAGER("全局管理员", null), - EMPLOYEE("员工"); + /// 店长 + STORE_MANAGER("店长", GLOBAL_MANAGER.name()), + + EMPLOYEE("员工", STORE_MANAGER.name()); final String defaultName; + final String parentCode; @Override public ModuleCode getModuleCode() { @@ -36,6 +40,6 @@ 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/GaoCustomerController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerController.java index 260ea13..d237a78 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 @@ -15,6 +15,7 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq; 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; @@ -111,13 +112,25 @@ public class GaoCustomerController { service.delete(id); } + @AOPLog + @RequestRateLimit + @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("/find/code") @JsonView(ResponseView.Public.class) - public GaoCustomer findByCode(@RequestParam String code) { - GaoCustomer customer = service.getByCode(code); + public GaoCustomer findByCode(@RequestParam String storeId, @RequestParam String code) { + GaoCustomer customer = service.getByCode(storeId, code); if (customer == null) { return null; } @@ -130,8 +143,8 @@ public class GaoCustomerController { @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) @PostMapping("/find/name") @JsonView(ResponseView.Public.class) - public GaoCustomer findByName(@RequestParam String name) { - GaoCustomer customer = service.getByName(name); + public GaoCustomer findByName(@RequestParam String storeId, @RequestParam String name) { + GaoCustomer customer = service.getByName(storeId, name); if (customer == null) { return null; } @@ -197,7 +210,7 @@ public class GaoCustomerController { @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.CUSTOMER_READ) @PostMapping("/state/gender") - public List genderStat() { - return service.stateGender(); + 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 index 6103e94..08a19d2 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java @@ -49,8 +49,8 @@ public class GaoEventController { @AOPLog @RequestRateLimit @PostMapping("/list/valid") - public List listValid() { - return service.listValid(); + public List listValid(@RequestParam String storeId) { + return service.listValid(storeId); } @AOPLog 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/GaoStoreController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java new file mode 100644 index 0000000..86f6229 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java @@ -0,0 +1,103 @@ +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.api.bean.RoleMatchMode; +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; + +/// GAO 门店接口 +/// +/// @author Codex +/// @since 2026-08-06 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/store") +@RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER) +public class GaoStoreController { + + private final GaoStoreService service; + private final UserLoginService userLoginService; + + @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("/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 + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoStore store) { + service.update(store); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_DELETE) + @PostMapping("/delete") + public void delete(@RequestParam String id) { + service.delete(id); + } + + @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 + @RequireGaoRole(value = {GaoRoleCode.GLOBAL_MANAGER, GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY) + @PostMapping("/detail/user") + public GaoStore getByLoginUser() { + 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 cf26252..dbc0e8b 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,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; /// GAO 用户接口 /// @@ -34,8 +44,14 @@ import java.util.List; @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 @@ -43,7 +59,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 @@ -52,16 +83,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 @@ -84,6 +113,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 af788d8..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 @@ -48,6 +48,9 @@ public class GaoCustomer extends UUIDEntity { /// 客户编码 private String code; + /// 门店 ID + private String storeId; + /// 姓名 private String name; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java index 1142178..0a354b1 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java @@ -73,6 +73,9 @@ public class GaoEvent extends UUIDEntity { /// 登记事件名称 private String name; + /// 门店 ID + private String storeId; + /// 说明 private String description; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java index 4dacd05..9a2a53b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java @@ -31,6 +31,9 @@ public class GaoEventRecord extends UUIDEntity { /// 客户 ID private String customerId; + /// 门店 ID + private String storeId; + /// 登记登记事件 ID private String eventId; @@ -43,6 +46,12 @@ public class GaoEventRecord extends UUIDEntity { /// 登记时间 private Long registeredAt; + /// 登记日期值,格式为 yyyyMMdd + private Integer registeredDateValue; + + /// 营业日递增序号 + private Long businessDayNo; + @Transient private GaoCustomer customer; 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 1ee6b93..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 @@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.entity.GaoCustomer; 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 java.util.Collection; import java.util.List; @@ -22,16 +23,16 @@ public interface GaoCustomerMapper extends BaseMapper { /// @param beginCreatedAt 开始创建时间 /// @param endCreatedAt 结束创建时间 /// @return 每日新增客户数 - List stateDailyNewCustomer(Long beginCreatedAt, Long endCreatedAt); + List stateDailyNewCustomer(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt, @Param("endCreatedAt") Long endCreatedAt); /// 统计开始时间前客户总数 /// /// @param beginCreatedAt 开始创建时间 /// @return 客户总数 - Long countCreatedBefore(Long beginCreatedAt); + Long countCreatedBefore(@Param("storeId") String storeId, @Param("beginCreatedAt") Long beginCreatedAt); /// 统计客户性别分布 /// /// @return 性别统计列表 - List stateGender(); + 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 index 56040d3..259b345 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventMapper.java @@ -2,6 +2,7 @@ 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; @@ -15,5 +16,5 @@ public interface GaoEventMapper extends BaseMapper { List selectByIdList(Collection idList); - List selectValidList(Long now); + 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 index 460900a..f5ac28f 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java @@ -5,6 +5,7 @@ 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.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; import java.util.List; @@ -14,7 +15,21 @@ import java.util.List; /// @since 2026-07-27 14:11 public interface GaoEventRecordMapper extends BaseMapper { - Long count(String customerId, String eventId, Long beginAt, Long endAt); + 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); 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..36eaf89 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStoreMapper.java @@ -0,0 +1,19 @@ +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; + +/// +/// GAO 门店 Mapper +/// +/// @author Codex +/// @since 2026-08-06 +public interface GaoStoreMapper extends BaseMapper { + + /// 查询用户所属的可用门店 + /// + /// @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..157595a 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 @@ -14,4 +14,7 @@ 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); + + @Select("SELECT * FROM `gao_user` WHERE `store_id` = #{storeId} AND `user_id` = #{userId} AND `deleted_at` IS NULL LIMIT 1") + GaoUser selectByStoreIdAndUserId(@Param("storeId") String storeId, @Param("userId") String userId); } 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 b9b0ca0..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 @@ -23,11 +23,17 @@ public interface GaoCustomerService extends BaseService { /// 按编码查询客户 /// + /// @param storeId 门店 ID /// @param code 客户编码 /// @return 客户 - GaoCustomer getByCode(String code); + GaoCustomer getByCode(String storeId, String code); - GaoCustomer getByName(String name); + /// 按姓名查询客户 + /// + /// @param storeId 门店 ID + /// @param name 姓名 + /// @return 客户 + GaoCustomer getByName(String storeId, String name); List listByIdList(Collection idList); @@ -65,6 +71,7 @@ public interface GaoCustomerService extends BaseService { /// 统计客户性别分布 /// + /// @param storeId 门店 ID /// @return 性别统计列表 - List stateGender(); + List stateGender(String storeId); } 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 index f37f2db..6331a21 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java @@ -20,8 +20,9 @@ public interface GaoEventService extends BaseService { /// 查询启用的登记事件 /// + /// @param storeId 门店 ID /// @return 登记事件列表 - List listValid(); + List listValid(String storeId); /// 查询登记事件要求的角色 /// 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/GaoStoreService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java new file mode 100644 index 0000000..5036e71 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java @@ -0,0 +1,27 @@ +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; + +/// +/// GAO 门店服务 +/// +/// @author Codex +/// @since 2026-08-06 +public interface GaoStoreService extends BaseService { + + /// 查询用户所属门店 + /// + /// @param userId 用户 ID + /// @return 门店 + GaoStore getByUserId(String userId); + + /// 导出门店列表 + /// + /// @param page 分页查询条件 + /// @return xlsx 文件字节 + byte[] export(Page page) throws IOException; +} 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/GaoCustomerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerServiceImplement.java index 9053b0a..66cf3e5 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 @@ -34,8 +34,10 @@ 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; /// 客户服务实现 /// @@ -66,21 +68,24 @@ public class GaoCustomerServiceImplement extends AbstractEntityService listByIdList(Collection idList) { - return mapper.selectByIdList(idList); + return mapper.selectByIdList(new HashSet<>(idList)); } @Override @@ -252,6 +263,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService 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"); @@ -325,10 +338,10 @@ public class GaoCustomerServiceImplement extends AbstractEntityService newCustomerCountMap = new HashMap<>(); - for (GaoCustomerDailyStateView view : mapper.stateDailyNewCustomer(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); + 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)); @@ -344,8 +357,9 @@ public class GaoCustomerServiceImplement extends AbstractEntityService stateGender() { - return mapper.stateGender(); + public List stateGender(String storeId) { + TimiException.required(storeId, "not found storeId"); + return mapper.stateGender(storeId); } private void changeInvitedCount(String customerId, long delta) { 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 index 7f1c93a..e3af9a0 100644 --- 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 @@ -12,6 +12,7 @@ import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; 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.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; @@ -34,8 +35,10 @@ 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; @@ -50,6 +53,8 @@ import java.util.List; @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; @@ -57,6 +62,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService customerIdList = TimiJava.defaultIfEmpty(record.getCustomerIdList(), List.of(record.getCustomerId())); TimiException.required(customerIdList, "not found customerId or empty customerIdList"); @@ -91,13 +100,19 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService 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)); @@ -205,12 +229,14 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService 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)); @@ -241,18 +267,18 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService { long beginAt = Time.today(registeredAt); long endAt = beginAt + Time.D; - Long count = mapper.count(customerId, event.getId(), beginAt, endAt); + 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(customerId, event.getId(), beginAt, registeredAt); + Long count = mapper.count(event.getStoreId(), customerId, event.getId(), beginAt, registeredAt); TimiException.requiredTrue(TimiJava.defaultIfNull(count, 0L) < 1, "该客户登记该事件间隔不足"); } case NATURAL_MONTH -> { @@ -260,17 +286,22 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService { - Long count = mapper.count(null, event.getId(), null, null); + 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(customerId, event.getId(), null, null); + 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 index 6f42c65..cac18ba 100644 --- 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 @@ -56,6 +56,7 @@ public class GaoEventServiceImplement extends AbstractEntityService listValid() { - return mapper.selectValidList(Time.now()); + public List listValid(String storeId) { + TimiException.required(storeId, "not found storeId"); + return mapper.selectValidList(storeId, Time.now()); } @Override 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/GaoStoreServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java new file mode 100644 index 0000000..9d66219 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java @@ -0,0 +1,160 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.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 java.io.IOException; +import java.util.List; + +/// +/// 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 GaoStoreMapper mapper; + private final StoreXSSFBuilder storeXSSFBuilder; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @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); + } + } + + @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()); + dbStore.setSort(store.getSort()); + 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); + } + } + + @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 byte[] export(Page page) throws IOException { + page.setIndex(0); + page.setSize(Long.MAX_VALUE); + List list = mapper.selectByPage(page); + return storeXSSFBuilder.build(list); + } + + 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..a77d6e2 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,26 @@ 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 +31,13 @@ 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 +46,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/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/GaoCustomerTrendStateReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStateReq.java index 2763f5e..881d521 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStateReq.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerTrendStateReq.java @@ -10,6 +10,9 @@ import lombok.Data; @Data public class GaoCustomerTrendStateReq { + /// 门店 ID + private String storeId; + /// 开始创建时间 private Long beginCreatedAt; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java index d64917d..870c859 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordPage.java @@ -15,6 +15,9 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public class GaoEventRecordPage extends BasePage { + /// 门店 ID + private String storeId; + /// 客户 ID,为空查询全部客户 private String customerId; 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 index 7073947..537b853 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordRankPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoEventRecordRankPage.java @@ -14,6 +14,9 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public class GaoEventRecordRankPage extends BasePage { + /// 门店 ID + private String storeId; + /// 客户 ID,为空查询全部客户 private String customerId; 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..cfe1425 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 @@ -10,7 +10,8 @@ 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.UserRoleService; +import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeRequest; import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.PageResult; @@ -37,6 +38,7 @@ import java.util.List; public class RoleController { private final RoleService service; + private final UserRoleService userRoleService; private final PermissionService permissionService; @RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY) @@ -86,26 +88,26 @@ public class RoleController { @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) { if (moduleCode != null) { - return service.listByUserId(userId, moduleCode); + return userRoleService.listRoleByUserId(moduleCode, userId); } - return service.listByUserId(userId); + return userRoleService.listRoleByUserId(userId); } @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); + return userRoleService.listAllPermissionByUserId(userId); } private void fillRoleDetail(Role role) { 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..cd58258 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; @@ -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/RoleMapper.java b/src/main/java/com/imyeyu/api/modules/user/mapper/RoleMapper.java index 459e5f4..931b5ab 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 @@ -3,8 +3,8 @@ package com.imyeyu.api.modules.user.mapper; import com.imyeyu.api.bean.ModuleCode; 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 +15,7 @@ import java.util.List; */ public interface RoleMapper extends BaseMapper { - List selectByIdList(List idList); + List selectByIdList(Collection idList); - List selectByModuleCodeAndCodeList(@Param("moduleCode") ModuleCode moduleCode, @Param("codeList") List codeList); - - List selectByModuleCode(@Param("moduleCode") ModuleCode moduleCode); + List selectByModuleCode(ModuleCode moduleCode); } 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..9314e55 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); + List selectByUserIdList(@Param("userIdList") Collection userIdList); + void deleteByRoleIdList(String userId, List roleIdList); } 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..06bf481 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,13 @@ 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(); - } - - /// 查询用户全部角色编码 - /// - /// @param userId 用户 ID - /// @return 包含子角色的角色编码列表,格式为 MODULE:ROLE - List listAllCodeByUserId(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(); - } + List listByIdList(Collection idList); /// 查询直属子角色 /// @@ -80,4 +33,10 @@ public interface RoleService extends BaseService { /// @param roleId 角色 ID /// @return 权限列表 List listPermissionByRoleId(String roleId); + + /// 递归获取角色及其所有子角色的 ID 列表 + /// + /// @param idList 角色 ID 列表 + /// @return 包含所有子角色的 ID 列表 + List listAllRoleIdIncludingChildren(Collection idList); } 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/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/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/RoleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java index eb9aa83..6569e88 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,17 @@ 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.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.UserRole; import com.imyeyu.api.modules.user.mapper.RoleMapper; import com.imyeyu.api.modules.user.mapper.RolePermissionMapper; import com.imyeyu.api.modules.user.mapper.RoleRelationMapper; import com.imyeyu.api.modules.user.service.PermissionService; 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.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,6 +22,7 @@ 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.Set; @@ -42,9 +37,7 @@ 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 RoleMapper mapper; @@ -153,67 +146,6 @@ public class RoleServiceImplement extends AbstractEntityService im } } - @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(); - } - @Override public List listByModuleCode(ModuleCode moduleCode) { TimiException.required(moduleCode, "not found moduleCode"); @@ -221,20 +153,8 @@ 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(); - } - - @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 listByIdList(Collection idList) { + return mapper.selectByIdList(new HashSet<>(idList)); } @Override @@ -248,20 +168,13 @@ public class RoleServiceImplement extends AbstractEntityService im @Override public List listPermissionByRoleId(String roleId) { - List allRoleIdList = getAllRoleIdsIncludingChildren(List.of(roleId)); + List allRoleIdList = listAllRoleIdIncludingChildren(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); + public List listAllRoleIdIncludingChildren(Collection idList) { + Set allRoleIdSet = new HashSet<>(idList); + List currentLevelRoleIdList = new ArrayList<>(idList); while (TimiJava.isNotEmpty(currentLevelRoleIdList)) { List childRoleIdList = roleRelationMapper.selectChildRoleIdListByParentRoleIdList(currentLevelRoleIdList); @@ -277,18 +190,4 @@ public class RoleServiceImplement extends AbstractEntityService im } return new ArrayList<>(allRoleIdSet); } - - private String saveNameMultilingual(String langId, String name) { - if (TimiJava.isEmpty(name)) { - return langId; - } - if (TimiJava.isEmpty(langId)) { - return multilingualService.create(name); - } - Multilingual multilingual = new Multilingual(); - multilingual.setId(langId); - multilingual.setZhCN(name); - multilingualMapper.updateSelective(multilingual); - return langId; - } } 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..c9d9455 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,8 +88,8 @@ 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.setPermissionList(userRoleService.listAllPermissionCodeByUserId(user.getId())); + user.setRoleList(userRoleService.listAllRoleCodeByUserId(user.getId())); user.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.USER, user.getId())); return result; } 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..769a1ea 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,34 @@ 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.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.PermissionService; +import com.imyeyu.api.modules.user.service.RoleService; 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 +38,9 @@ import java.util.List; @RequiredArgsConstructor public class UserRoleServiceImplement extends AbstractEntityService implements UserRoleService { + private final RoleService roleService; + private final PermissionService permissionService; + private final UserRoleMapper mapper; @Override @@ -30,6 +53,49 @@ 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); + + if (TimiJava.isNotEmpty(deleteIdList)) { + 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 +103,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 = roleService.listAllRoleIdIncludingChildren(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 = roleService.listAllRoleIdIncludingChildren(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/InitBuiltinUser.java b/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java index 5850f68..6dfad63 100644 --- a/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java +++ b/src/main/java/com/imyeyu/api/util/InitBuiltinUser.java @@ -192,6 +192,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,6 +205,9 @@ public class InitBuiltinUser implements ApplicationRunner { if (item.isGrantToAdmin() && adminRole != null) { createRolePermissionIfAbsent(adminRole.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()); } 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/mapper/timi-server/GaoCustomerMapper.xml b/src/main/resources/mapper/timi-server/GaoCustomerMapper.xml index 6cbed41..ae69942 100644 --- a/src/main/resources/mapper/timi-server/GaoCustomerMapper.xml +++ b/src/main/resources/mapper/timi-server/GaoCustomerMapper.xml @@ -6,10 +6,16 @@ * FROM `gao_customer` WHERE - `id` IN - - #{id} - + TRUE + + AND `id` IN + + #{id} + + + + AND FALSE + AND `deleted_at` IS NULL @@ -19,7 +25,8 @@ 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') @@ -31,7 +38,8 @@ COUNT(1) FROM `gao_customer` WHERE - `created_at` < #{beginCreatedAt} + `store_id` = #{storeId} + AND `created_at` < #{beginCreatedAt} AND `deleted_at` IS NULL @@ -41,7 +49,8 @@ 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/GaoEventMapper.xml b/src/main/resources/mapper/timi-server/GaoEventMapper.xml index 8750dcd..9b08997 100644 --- a/src/main/resources/mapper/timi-server/GaoEventMapper.xml +++ b/src/main/resources/mapper/timi-server/GaoEventMapper.xml @@ -24,7 +24,8 @@ * FROM `gao_event` WHERE - `status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}' + `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 diff --git a/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml index aaea72d..2a05053 100644 --- a/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml @@ -8,6 +8,9 @@ `gao_event_record` WHERE TRUE + + AND `store_id` = #{storeId} + AND `customer_id` = #{customerId} @@ -18,19 +21,48 @@ AND `registered_at` >= #{beginAt} - AND `registered_at` >= #{endAt} + AND `registered_at` <= #{endAt} AND `deleted_at` IS NULL + + + + 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` - r.`registered_at` >= #{beginAt} - AND r.`registered_at` <= #{endAt} + 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} @@ -69,6 +101,9 @@ r.`deleted_at` IS NULL AND c.`deleted_at` IS NULL + + AND r.`store_id` = #{storeId} + AND r.`customer_id` = #{customerId} @@ -97,7 +132,7 @@ WITH `daily_stat` AS ( SELECT r.`customer_id` AS `customerId`, - DATE(FROM_UNIXTIME(r.`registered_at` / 1000)) AS `eventDate`, + r.`business_day_no` AS `businessDayNo`, COUNT(1) AS `dailyCount`, MAX(r.`registered_at`) AS `latestRegisteredAt` FROM `gao_event_record` r @@ -106,7 +141,7 @@ GROUP BY r.`customer_id`, - DATE(FROM_UNIXTIME(r.`registered_at` / 1000)) + r.`business_day_no` ), `rank_stat` AS ( SELECT @@ -120,14 +155,14 @@ `continuous_ordered` AS ( SELECT `customerId`, - `eventDate`, - ROW_NUMBER() OVER (PARTITION BY `customerId` ORDER BY `eventDate` DESC) AS `dayIndex` + `businessDayNo`, + ROW_NUMBER() OVER (PARTITION BY `customerId` ORDER BY `businessDayNo` DESC) AS `dayIndex` FROM `daily_stat` ), `latest_date` AS ( SELECT `customerId`, - MAX(`eventDate`) AS `latestEventDate` + MAX(`businessDayNo`) AS `latestBusinessDayNo` FROM `daily_stat` GROUP BY `customerId` ), @@ -137,7 +172,7 @@ COUNT(1) AS `recentContinuousEventDays` FROM `continuous_ordered` o LEFT JOIN `latest_date` l ON o.`customerId` = l.`customerId` - WHERE DATE_ADD(o.`eventDate`, INTERVAL o.`dayIndex` DAY) = DATE_ADD(l.`latestEventDate`, INTERVAL 1 DAY) + WHERE o.`businessDayNo` + o.`dayIndex` = l.`latestBusinessDayNo` + 1 GROUP BY o.`customerId` ), `ranked` AS ( 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..b30c92e --- /dev/null +++ b/src/main/resources/mapper/timi-server/GaoStoreMapper.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/src/main/resources/mapper/timi-server/RoleMapper.xml b/src/main/resources/mapper/timi-server/RoleMapper.xml index ddda0ca..736551c 100644 --- a/src/main/resources/mapper/timi-server/RoleMapper.xml +++ b/src/main/resources/mapper/timi-server/RoleMapper.xml @@ -6,7 +6,7 @@ * FROM `role` WHERE - 1 = 1 + TRUE AND `id` IN @@ -14,29 +14,10 @@ - AND 1 = 0 + AND FALSE AND `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()