Compare commits

..
34 Commits
Author SHA1 Message Date
timi 619369104c Merge pull request 'v1.0.12' (#19) from dev into master
Reviewed-on: #19
2026-08-12 17:02:43 +00:00
Timi c0b04fc554 v1.0.12
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-13 01:02:22 +08:00
Timi 5e5748b521 add customer in gao point ledger list api 2026-08-13 00:46:32 +08:00
Timi 67d79f7527 fix gao event rank 2026-08-13 00:24:39 +08:00
Timi ee530d2a8c refactor mapper xml 2026-08-12 16:11:08 +08:00
Timi 5f36844279 update GaoStore update 2026-08-12 12:10:21 +08:00
Timi 5b9391a50a add GaoStore.autoClosedDay 2026-08-12 12:03:30 +08:00
Timi 6bc73414d9 remove @RequireGaoRole 2026-08-12 11:06:48 +08:00
timi 4f1408b9d0 Merge pull request 'v1.0.11' (#18) from dev into master
Reviewed-on: #18
2026-08-11 16:49:08 +00:00
Timi d555aad9ac v1.0.11
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-12 00:48:54 +08:00
timi 029eea70cc Merge pull request 'v1.0.10' (#17) from dev into master
Reviewed-on: #17
2026-08-11 16:45:36 +00:00
Timi 2f42204794 v1.0.10
CI / build-deploy (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-12 00:42:35 +08:00
timi 22b1e65385 Merge pull request 'v1.0.9' (#16) from dev into master
Reviewed-on: #16
2026-08-11 16:01:10 +00:00
Timi 72bdb8d49b v1.0.9
CI / build-deploy (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Has been skipped
2026-08-11 23:58:00 +08:00
Timi 5ec6bc472f add chart api 2026-08-11 23:58:00 +08:00
Timi 81b7356d67 nullable point account detail api 2026-08-11 23:58:00 +08:00
Timi 44af16f451 fix new permission D authorize fail 2026-08-11 23:57:59 +08:00
Timi c6157db0de add gao point 2026-08-11 23:57:59 +08:00
Timi 39ac73dc66 refactor permission system 2026-08-11 23:57:59 +08:00
timi 2170c48763 Merge pull request 'v1.0.8' (#15) from dev into master
Reviewed-on: #15
2026-08-03 16:36:19 +00:00
timi c2d1c48aae Merge pull request 'v0.0.18' (#14) from dev into master
Reviewed-on: #14
2026-08-03 11:59:20 +00:00
timi e14958c643 Merge pull request 'v0.0.18' (#13) from dev into master
Reviewed-on: #13
2026-08-03 11:58:16 +00:00
timi da288338cd Merge pull request 'v1.0.7' (#12) from dev into master
Reviewed-on: #12
2026-08-01 09:34:18 +00:00
timi ea430f2468 Merge pull request 'v1.0.6' (#11) from dev into master
Reviewed-on: #11
2026-08-01 09:03:48 +00:00
timi f76f79744a Merge pull request 'v1.0.5' (#10) from dev into master
Reviewed-on: #10
2026-08-01 08:52:52 +00:00
timi 19daaff907 Merge pull request 'v1.0.4' (#9) from dev into master
Reviewed-on: #9
2026-08-01 03:13:00 +00:00
timi db94d8f932 Merge pull request 'v1.0.3' (#8) from dev into master
Reviewed-on: #8
2026-07-31 17:19:48 +00:00
timi 53618fe362 Merge pull request 'v1.0.3' (#7) from dev into master
Reviewed-on: #7
2026-07-31 17:13:37 +00:00
timi 523edffc4e Merge pull request 'v1.0.2' (#6) from dev into master
Reviewed-on: #6
2026-04-13 10:28:11 +00:00
timi c7ddb1a8b0 Merge pull request 'v1.0.1' (#5) from dev into master
Reviewed-on: #5
2026-04-12 16:17:02 +00:00
timi 45c9fc814a Merge pull request 'v1.0.0' (#4) from dev into master
Reviewed-on: #4
2026-04-09 05:16:04 +00:00
timi 407dc13ac4 Merge pull request 'v1.0.0' (#3) from dev into master
Reviewed-on: #3
2026-04-09 04:09:16 +00:00
timi 0c06bf16c2 Merge pull request 'v1.0.0' (#2) from dev into master
Reviewed-on: #2
2026-04-09 04:03:43 +00:00
timi 1db39e77d3 Merge pull request 'v1.0.0' (#1) from dev into master
Reviewed-on: #1
2026-04-08 08:48:12 +00:00
130 changed files with 4906 additions and 524 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/resources/db/migration/timiserver/V1__init_schema.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/main/resources/db/migration/timiserver/V1__init_schema.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/main/resources/db/migration/timiserver/V2__init_system_data.sql" dialect="GenericSQL" /> <file url="file://$PROJECT_DIR$/src/main/resources/db/migration/timiserver/V2__init_system_data.sql" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/src/main/resources/mapper/timi-server/GaoEventRecordMapper.xml" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml" dialect="MariaDB" />
<file url="PROJECT" dialect="MariaDB" /> <file url="PROJECT" dialect="MariaDB" />
</component> </component>
</project> </project>
+2 -2
View File
@@ -11,7 +11,7 @@
<groupId>com.imyeyu.timiserverapi</groupId> <groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId> <artifactId>TimiServerAPI</artifactId>
<version>1.0.8</version> <version>1.0.12</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>TimiServerAPI</name> <name>TimiServerAPI</name>
<description>imyeyu.com API</description> <description>imyeyu.com API</description>
@@ -178,7 +178,7 @@
<dependency> <dependency>
<groupId>com.imyeyu.network</groupId> <groupId>com.imyeyu.network</groupId>
<artifactId>timi-network</artifactId> <artifactId>timi-network</artifactId>
<version>0.0.13</version> <version>0.0.14</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.imyeyu.lang</groupId> <groupId>com.imyeyu.lang</groupId>
@@ -1,29 +0,0 @@
package com.imyeyu.api.annotation;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.bean.CoreRoleCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
///
/// 要求 CORE 模块角色
///
/// @author Codex
/// @since 2026-07-26
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireCoreRole {
/// 所需角色
///
/// @return 角色列表
CoreRoleCode[] value();
/// 匹配模式
///
/// @return 匹配模式
RoleMatchMode mode() default RoleMatchMode.ALL;
}
@@ -1,46 +0,0 @@
package com.imyeyu.api.annotation;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.java.TimiJava;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
///
/// CORE 角色注解处理器
///
/// @author Codex
/// @since 2026-07-27
@Component
public class RequireCoreRoleInterceptor implements HandlerInterceptor {
private final RoleChecker roleChecker;
public RequireCoreRoleInterceptor(RoleChecker roleChecker) {
this.roleChecker = roleChecker;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
RequireCoreRole annotation = handlerMethod.getMethodAnnotation(RequireCoreRole.class);
if (annotation == null || TimiJava.isEmpty(annotation.value())) {
return true;
}
String[] roleCodeList = java.util.Arrays.stream(annotation.value()).map(CoreRoleCode::name).toArray(String[]::new);
if (annotation.mode() == RoleMatchMode.ANY) {
roleChecker.checkAny(ModuleCode.CORE, roleCodeList);
} else {
roleChecker.checkAll(ModuleCode.CORE, roleCodeList);
}
return true;
}
}
@@ -25,9 +25,9 @@ public enum CorePermissionCode implements BuiltinPermissionCode {
ROLE_PERMISSION_READ("ROLE_PERMISSION:READ", "角色权限读取", false, true), ROLE_PERMISSION_READ("ROLE_PERMISSION:READ", "角色权限读取", false, true),
ROLE_PERMISSION_DELETE("ROLE_PERMISSION:DELETE", "角色权限删除", false, true), ROLE_PERMISSION_DELETE("ROLE_PERMISSION:DELETE", "角色权限删除", false, true),
ROLE_RELATION_CREATE("ROLE_RELATION:CREATE", "角色关联创建", false, true), ROLE_PERMISSION_INHERIT_CREATE("ROLE_PERMISSION_INHERIT:CREATE", "角色权限继承创建", false, true),
ROLE_RELATION_READ("ROLE_RELATION:READ", "角色关联读取", false, true), ROLE_PERMISSION_INHERIT_READ("ROLE_PERMISSION_INHERIT:READ", "角色权限继承读取", false, true),
ROLE_RELATION_DELETE("ROLE_RELATION:DELETE", "角色关联删除", false, true), ROLE_PERMISSION_INHERIT_DELETE("ROLE_PERMISSION_INHERIT:DELETE", "角色权限继承删除", false, true),
SETTING_CREATE("SETTING:CREATE", "设置创建", false, true), SETTING_CREATE("SETTING:CREATE", "设置创建", false, true),
SETTING_READ("SETTING:READ", "设置读取", false, true), SETTING_READ("SETTING:READ", "设置读取", false, true),
@@ -39,6 +39,12 @@ public enum CorePermissionCode implements BuiltinPermissionCode {
MULTILINGUAL_UPDATE("MULTILINGUAL:UPDATE", "多语言修改", false, true), MULTILINGUAL_UPDATE("MULTILINGUAL:UPDATE", "多语言修改", false, true),
MULTILINGUAL_DELETE("MULTILINGUAL:DELETE", "多语言删除", false, true), MULTILINGUAL_DELETE("MULTILINGUAL:DELETE", "多语言删除", false, true),
TAG_CREATE("TAG:CREATE", "标签创建", false, true),
TAG_READ("TAG:READ", "标签读取", false, true),
TAG_APPLY("TAG:APPLY", "标签应用", false, true),
TAG_UPDATE("TAG:UPDATE", "标签修改", false, true),
TAG_DELETE("TAG:DELETE", "标签删除", false, true),
USER_CREATE("USER:CREATE", "用户创建", false, true), USER_CREATE("USER:CREATE", "用户创建", false, true),
USER_READ("USER:READ", "用户读取", false, true), USER_READ("USER:READ", "用户读取", false, true),
USER_UPDATE("USER:UPDATE", "用户修改", false, true), USER_UPDATE("USER:UPDATE", "用户修改", false, true),
@@ -13,9 +13,7 @@ public enum CoreRoleCode implements BuiltinRoleCode {
SYSTEM("系统", true, null), SYSTEM("系统", true, null),
ADMIN("管理员", false, SYSTEM.name()), ADMIN("管理员", false, SYSTEM.name());
USER_ROLE_ADMIN("授权管理", false, ADMIN.name());
final String defaultName; final String defaultName;
final boolean protectedRole; final boolean protectedRole;
@@ -40,4 +38,5 @@ public enum CoreRoleCode implements BuiltinRoleCode {
public String getParentCode() { public String getParentCode() {
return parentCode; return parentCode;
} }
} }
@@ -9,9 +9,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.imyeyu.api.annotation.EnableSettingInterceptor; import com.imyeyu.api.annotation.EnableSettingInterceptor;
import com.imyeyu.api.annotation.RequestRateLimitInterceptor; import com.imyeyu.api.annotation.RequestRateLimitInterceptor;
import com.imyeyu.api.annotation.RequireCorePermissionInterceptor; import com.imyeyu.api.annotation.RequireCorePermissionInterceptor;
import com.imyeyu.api.annotation.RequireCoreRoleInterceptor;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermissionInterceptor;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRoleInterceptor;
import com.imyeyu.api.modules.journal.util.JournalAPIInterceptor; import com.imyeyu.api.modules.journal.util.JournalAPIInterceptor;
import com.imyeyu.api.modules.system.util.SystemAPIInterceptor; import com.imyeyu.api.modules.system.util.SystemAPIInterceptor;
import com.imyeyu.spring.annotation.RequestBodyValueArgumentResolver; import com.imyeyu.spring.annotation.RequestBodyValueArgumentResolver;
@@ -43,8 +41,6 @@ public class WebConfig implements WebMvcConfigurer {
private final SystemAPIInterceptor systemAPIInterceptor; private final SystemAPIInterceptor systemAPIInterceptor;
private final JournalAPIInterceptor journalAPIInterceptor; private final JournalAPIInterceptor journalAPIInterceptor;
private final EnableSettingInterceptor enableSettingInterceptor; private final EnableSettingInterceptor enableSettingInterceptor;
private final RequireGaoRoleInterceptor requireGaoRoleInterceptor;
private final RequireCoreRoleInterceptor requireCoreRoleInterceptor;
private final RequestRateLimitInterceptor requestRateLimitInterceptor; private final RequestRateLimitInterceptor requestRateLimitInterceptor;
private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor; private final RequireGaoPermissionInterceptor requireGaoPermissionInterceptor;
private final RequireCorePermissionInterceptor requireCorePermissionInterceptor; private final RequireCorePermissionInterceptor requireCorePermissionInterceptor;
@@ -59,9 +55,7 @@ public class WebConfig implements WebMvcConfigurer {
registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH); registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH);
registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH); registry.addInterceptor(journalAPIInterceptor).addPathPatterns(JournalAPIInterceptor.PATH);
registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**"); registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireCoreRoleInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireCorePermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireGaoRoleInterceptor).addPathPatterns("/**");
registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**"); registry.addInterceptor(requireGaoPermissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**"); registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**");
} }
@@ -2,9 +2,7 @@ package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission; import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.annotation.RequireCoreRole;
import com.imyeyu.api.bean.CorePermissionCode; import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.modules.common.entity.Multilingual; import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.service.MultilingualService; import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
@@ -40,7 +38,6 @@ public class MultilingualController {
*/ */
@JsonView(ResponseView.Admin.class) @JsonView(ResponseView.Admin.class)
@RequestRateLimit @RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_READ) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_READ)
@PostMapping("/detail") @PostMapping("/detail")
public Multilingual detail(@RequestParam String id) { public Multilingual detail(@RequestParam String id) {
@@ -56,7 +53,6 @@ public class MultilingualController {
@AOPLog @AOPLog
@JsonView(ResponseView.Admin.class) @JsonView(ResponseView.Admin.class)
@RequestRateLimit @RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_CREATE) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_CREATE)
@PostMapping("/create") @PostMapping("/create")
public Multilingual create(@RequestBody Multilingual multilingual) { public Multilingual create(@RequestBody Multilingual multilingual) {
@@ -71,7 +67,6 @@ public class MultilingualController {
*/ */
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_UPDATE) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_UPDATE)
@PostMapping("/update") @PostMapping("/update")
public void update(@RequestBody Multilingual multilingual) { public void update(@RequestBody Multilingual multilingual) {
@@ -85,7 +80,6 @@ public class MultilingualController {
*/ */
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_DELETE) @RequireCorePermission(CorePermissionCode.MULTILINGUAL_DELETE)
@PostMapping("/delete") @PostMapping("/delete")
public void delete(@RequestParam String id) { public void delete(@RequestParam String id) {
@@ -0,0 +1,111 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.api.modules.common.service.TagService;
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 com.imyeyu.spring.util.ResponseView;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
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;
/// 标签接口
///
/// @author 夜雨
/// @since 2026-08-12 15:09
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/tag")
public class TagController {
private final TagService service;
/// 查询标签列表
///
/// @param page 分页参数
/// @return 标签分页
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@PostMapping("/list")
public PageResult<Tag> list(@RequestBody Page<Tag> page) {
return service.page(page);
}
/// 查询标签详情
///
/// @param id 标签 ID
/// @return 标签详情
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@PostMapping("/detail")
public Tag detail(@RequestParam String id) {
return service.get(id);
}
/// 创建标签
///
/// @param tag 标签
/// @return 标签
@AOPLog
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@PostMapping("/create")
public Tag create(@RequestBody Tag tag) {
service.create(tag);
return tag;
}
/// 更新标签
///
/// @param tag 标签
@AOPLog
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody Tag tag) {
service.update(tag);
}
/// 删除标签
///
/// @param id 标签 ID
@AOPLog
@RequestRateLimit
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
/// 按业务查询标签
///
/// @param bizType 业务类型
/// @param bizId 业务 ID
/// @return 标签列表
@RequestRateLimit
@JsonView(ResponseView.Public.class)
@GetMapping("/list/biz/id")
public List<Tag> listByBizType(@RequestParam TagApply.BizType bizType, @RequestParam String bizId) {
return service.listByBizType(bizType, bizId);
}
/// 差分保存业务标签
///
/// @param apply 标签请求
@AOPLog
@RequestRateLimit
@PostMapping("/apply")
public void apply(@RequestBody TagApply apply) {
service.apply(apply.getBizType(), apply.getBizId(), apply.getTagIdList());
}
}
@@ -1,23 +1,28 @@
package com.imyeyu.api.modules.common.entity; package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.annotation.MultilingualField;
import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
/** /// 标签
* @author 夜雨 ///
* @since 2024-08-28 14:26 /// @author 夜雨
*/ /// @since 2024-08-28 14:26
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class Tag extends UUIDEntity { public class Tag extends UUIDEntity {
protected String langKey; /// 名称多语言 ID
protected String nameLangId;
/// 当前语言名称
@Transient @Transient
protected String value; @MultilingualField("nameLangId")
protected String name;
/// 中文名称
@Transient @Transient
protected String zhCN; protected String zhCN;
} }
@@ -1,9 +1,12 @@
package com.imyeyu.api.modules.common.entity; package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.List;
/** /**
* 标签应用 * 标签应用
* *
@@ -14,12 +17,25 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class TagApply extends UUIDEntity { public class TagApply extends UUIDEntity {
///
///
/// @author 夜雨
/// @since 2026-08-12 14:27
public enum BizType {
GAO_CUSTOMER
}
/** 业务类型 */ /** 业务类型 */
protected String bizType; protected BizType bizType;
/** 业务 ID */ /** 业务 ID */
protected String bizId; protected String bizId;
/** 标签 ID */ /** 标签 ID */
protected String tagId; protected String tagId;
/// 标签 ID 列表
@Transient
private List<String> tagIdList;
} }
@@ -0,0 +1,11 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.spring.mapper.BaseMapper;
/// 标签应用 Mapper
///
/// @author 夜雨
/// @since 2026-08-12 15:05
public interface TagApplyMapper extends BaseMapper<TagApply, String> {
}
@@ -3,9 +3,14 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Tag; import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import java.util.Collection;
import java.util.List;
/** /**
* @author 夜雨 * @author 夜雨
* @since 2025-05-30 22:48 * @since 2025-05-30 22:48
*/ */
public interface TagMapper extends BaseMapper<Tag, String> { public interface TagMapper extends BaseMapper<Tag, String> {
List<Tag> selectByIdList(Collection<String> idList);
} }
@@ -1,11 +1,31 @@
package com.imyeyu.api.modules.common.service; package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Tag; import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
/** import java.util.Collection;
* @author 夜雨 import java.util.List;
* @since 2025-05-30 22:46
*/ /// 标签服务
///
/// @author 夜雨
/// @since 2025-05-30 22:46
public interface TagService extends BaseService<Tag, String> { public interface TagService extends BaseService<Tag, String> {
List<Tag> listByIdList(Collection<String> idList);
/// 按业务查询标签列表
///
/// @param bizType 业务类型
/// @param bizId 业务 ID
/// @return 标签列表
List<Tag> listByBizType(TagApply.BizType bizType, String bizId);
/// 差分保存业务标签
///
/// @param bizType 业务类型
/// @param bizId 业务 ID
/// @param tagIdList 标签 ID 列表
void apply(TagApply.BizType bizType, String bizId, List<String> tagIdList);
} }
@@ -41,6 +41,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -347,6 +348,8 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void updateByBizId(Attachment.BizType bizType, String bizId, List<Attachment> list) { public void updateByBizId(Attachment.BizType bizType, String bizId, List<Attachment> list) {
list = TimiJava.defaultIfNull(list, new ArrayList<>());
// 数据库现有附件 // 数据库现有附件
List<Attachment> dbList = mapper.selectAllByBizId(bizType, bizId); List<Attachment> dbList = mapper.selectAllByBizId(bizType, bizId);
Set<String> dbIds = dbList.stream().map(Attachment::getId).collect(Collectors.toSet()); Set<String> dbIds = dbList.stream().map(Attachment::getId).collect(Collectors.toSet());
@@ -1,27 +1,100 @@
package com.imyeyu.api.modules.common.service.implement; package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.Tag; import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.entity.TagApply;
import com.imyeyu.api.modules.common.mapper.TagApplyMapper;
import com.imyeyu.api.modules.common.mapper.TagMapper; import com.imyeyu.api.modules.common.mapper.TagMapper;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.api.modules.common.service.TagService; import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** import java.util.Collection;
* @author 夜雨 import java.util.HashSet;
* @since 2025-05-30 22:47 import java.util.LinkedHashSet;
*/ import java.util.List;
/// 标签服务实现
///
/// @author 夜雨
/// @since 2025-05-30 22:47
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class TagServiceImplement extends AbstractEntityService<Tag, String> implements TagService { public class TagServiceImplement extends AbstractEntityService<Tag, String> implements TagService {
private final TagMapper mapper; private final TagMapper mapper;
private final TagApplyMapper applyMapper;
private final MultilingualService multilingualService;
@Override @Override
protected BaseMapper<Tag, String> mapper() { protected BaseMapper<Tag, String> mapper() {
return mapper; return mapper;
} }
@Override
public void create(Tag tag) {
TimiException.required(tag, "not found tag");
TimiException.required(tag.getZhCN(), "not found tag.zhCN");
tag.setNameLangId(multilingualService.create(tag.getZhCN()));
super.create(tag);
}
@Override
public void update(Tag tag) {
TimiException.required(tag, "not found tag");
TimiException.required(tag.getId(), "not found tag.id");
if (tag.getZhCN() != null) {
if (tag.getNameLangId() == null) {
Tag dbTag = get(tag.getId());
TimiException.required(dbTag, "not found tag");
tag.setNameLangId(dbTag.getNameLangId());
}
tag.setNameLangId(multilingualService.createIfDifferent(tag.getNameLangId(), tag.getZhCN()));
}
super.update(tag);
}
@Override
public List<Tag> listByIdList(Collection<String> idList) {
return mapper.selectByIdList(new HashSet<>(idList));
}
@Override
public List<Tag> listByBizType(TagApply.BizType bizType, String bizId) {
return listByIdList(listApply(bizType, bizId).stream().map(TagApply::getTagId).toList());
}
@Override
public void apply(TagApply.BizType bizType, String bizId, List<String> tagIdList) {
LinkedHashSet<String> tagIdSet = new LinkedHashSet<>(tagIdList == null ? List.of() : tagIdList);
for (TagApply apply : listApply(bizType, bizId)) {
if (tagIdSet.remove(apply.getTagId())) {
continue;
}
applyMapper.delete(apply.getId());
}
List<Tag> tagList = listByIdList(tagIdSet);
for (Tag tag : tagList) {
TagApply apply = new TagApply();
apply.setBizType(bizType);
apply.setBizId(bizId);
apply.setTagId(tag.getId());
applyMapper.insert(apply);
}
}
private List<TagApply> listApply(TagApply.BizType bizType, String bizId) {
TimiException.required(bizType, "not found bizType");
TimiException.required(bizId, "not found bizId");
TagApply example = new TagApply();
example.setBizType(bizType);
example.setBizId(bizId);
return applyMapper.selectAllByExample(example);
}
} }
@@ -1,15 +0,0 @@
package com.imyeyu.api.modules.common.vo.tag;
import lombok.Data;
/**
* @author 夜雨
* @since 2025-05-30 22:58
*/
@Data
public class TagRequest {
private String bizId;
private String zhCN;
}
@@ -1,29 +0,0 @@
package com.imyeyu.api.modules.gao.annotation;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
///
/// 要求 GAO 模块角色
///
/// @author Codex
/// @since 2026-07-26
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireGaoRole {
/// 所需角色
///
/// @return 角色列表
GaoRoleCode[] value();
/// 匹配模式
///
/// @return 匹配模式
RoleMatchMode mode() default RoleMatchMode.ALL;
}
@@ -1,50 +0,0 @@
package com.imyeyu.api.modules.gao.annotation;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.java.TimiJava;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
///
/// GAO 角色注解处理器
///
/// @author Codex
/// @since 2026-07-27
@Component
public class RequireGaoRoleInterceptor implements HandlerInterceptor {
private final RoleChecker roleChecker;
public RequireGaoRoleInterceptor(RoleChecker roleChecker) {
this.roleChecker = roleChecker;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
RequireGaoRole annotation = handlerMethod.getMethodAnnotation(RequireGaoRole.class);
if (annotation == null) {
annotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequireGaoRole.class);
}
if (annotation == null || TimiJava.isEmpty(annotation.value())) {
return true;
}
String[] roleCodeList = java.util.Arrays.stream(annotation.value()).map(GaoRoleCode::name).toArray(String[]::new);
if (annotation.mode() == RoleMatchMode.ANY) {
roleChecker.checkAny(ModuleCode.GAO, roleCodeList);
} else {
roleChecker.checkAll(ModuleCode.GAO, roleCodeList);
}
return true;
}
}
@@ -21,45 +21,61 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
STORE_DELETE("STORE:DELETE", "门店删除", false, true), STORE_DELETE("STORE:DELETE", "门店删除", false, true),
CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建"), CUSTOMER_CREATE("CUSTOMER:CREATE", "客户创建", false, false),
CUSTOMER_READ("CUSTOMER:READ", "客户读取"), CUSTOMER_READ("CUSTOMER:READ", "客户读取", false, false),
CUSTOMER_EXPORT("CUSTOMER:EXPORT", "客户导出", true), CUSTOMER_EXPORT("CUSTOMER:EXPORT", "客户导出", true, false),
CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改"), CUSTOMER_UPDATE("CUSTOMER:UPDATE", "客户修改", false, false),
CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除"), CUSTOMER_DELETE("CUSTOMER:DELETE", "客户删除", false, false),
EVENT_CREATE("EVENT:CREATE", "登记事件创建"), EVENT_CREATE("EVENT:CREATE", "登记事件创建", false, false),
EVENT_READ("EVENT:READ", "登记事件读取"), EVENT_READ("EVENT:READ", "登记事件读取", false, false),
EVENT_UPDATE("EVENT:UPDATE", "登记事件修改"), EVENT_UPDATE("EVENT:UPDATE", "登记事件修改", false, false),
EVENT_DELETE("EVENT:DELETE", "登记事件删除"), EVENT_DELETE("EVENT:DELETE", "登记事件删除", false, false),
EVENT_RECORD_CREATE("EVENT_RECORD:CREATE", "登记事件记录创建"), EVENT_RECORD_CREATE("EVENT_RECORD:CREATE", "登记事件记录创建", false, false),
EVENT_RECORD_READ("EVENT_RECORD:READ", "登记事件记录读取"), EVENT_RECORD_READ("EVENT_RECORD:READ", "登记事件记录读取", false, false),
EVENT_RECORD_EXPORT("EVENT_RECORD:EXPORT", "登记事件记录导出", true), EVENT_RECORD_EXPORT("EVENT_RECORD:EXPORT", "登记事件记录导出", true, false),
EVENT_RECORD_UPDATE("EVENT_RECORD:UPDATE", "登记事件记录修改"), EVENT_RECORD_UPDATE("EVENT_RECORD:UPDATE", "登记事件记录修改", false, false),
EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除"), EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false),
STAT_READ("STAT:READ", "统计读取"), POINT_ACCOUNT_READ("POINT_ACCOUNT:READ", "客户积分账户读取", true, true),
QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建"), POINT_LEDGER_READ("POINT_LEDGER:READ", "客户积分流水读取", true, true),
USER_CREATE("USER:CREATE", "用户创建", true), POINT_RULE_CREATE("POINT_RULE:CREATE", "客户积分规则创建", true, true),
USER_READ("USER:READ", "用户读取", true), POINT_RULE_READ("POINT_RULE:READ", "客户积分规则读取", true, true),
USER_UPDATE("USER:UPDATE", "用户修改", true), POINT_RULE_UPDATE("POINT_RULE:UPDATE", "客户积分规则修改", true, true),
USER_DELETE("USER:DELETE", "用户删除", true); POINT_RULE_DELETE("POINT_RULE:DELETE", "客户积分规则删除", true, true),
POINT_ADJUST("POINT:ADJUST", "客户积分人工调整", true, true),
POINT_SOURCE_VALUE("POINT:SOURCE_VALUE", "登记提交自定义积分值", true, true),
STAT_READ("STAT:READ", "统计读取", false, false),
QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建", false, false),
USER_CREATE("USER:CREATE", "用户创建", true, false),
USER_READ("USER:READ", "用户读取", true, false),
USER_UPDATE("USER:UPDATE", "用户修改", true, false),
USER_DELETE("USER:DELETE", "用户删除", true, false);
final String value; final String value;
final String defaultName; final String defaultName;
@@ -72,14 +88,6 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
@Getter @Getter
final boolean grantToGlobalManager; final boolean grantToGlobalManager;
GaoPermissionCode(String value, String defaultName) {
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) { GaoPermissionCode(String value, String defaultName, boolean grantToStoreManager, boolean grantToGlobalManager) {
this.value = value; this.value = value;
this.defaultName = defaultName; this.defaultName = defaultName;
@@ -16,9 +16,7 @@ public enum GaoRoleCode implements BuiltinRoleCode {
GLOBAL_MANAGER("全局管理员", null), GLOBAL_MANAGER("全局管理员", null),
/// 店长 /// 店长
STORE_MANAGER("店长", GLOBAL_MANAGER.name()), STORE_MANAGER("店长", GLOBAL_MANAGER.name());
EMPLOYEE("员工", STORE_MANAGER.name());
final String defaultName; final String defaultName;
final String parentCode; final String parentCode;
@@ -42,4 +40,5 @@ public enum GaoRoleCode implements BuiltinRoleCode {
public String getParentCode() { public String getParentCode() {
return parentCode; return parentCode;
} }
} }
@@ -0,0 +1,47 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// GAO 客户报表接口
///
/// @author Codex
/// @since 2026-08-11
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/customer/chart")
public class GaoCustomerChartController {
private final GaoCustomerChartService service;
/// 查询客户登记每日统计,可同时支撑登记趋势和登记事件结构图表
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/daily")
public List<GaoEventRecordDailyStatView> eventRecordDaily(@RequestBody GaoCustomerChartReq req) {
return service.eventRecordDailyStat(req);
}
/// 查询客户登记日历记录
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/calendar")
public List<GaoEventRecordCalendarView> eventRecordCalendar(@RequestBody GaoCustomerChartReq req) {
return service.eventRecordCalendar(req);
}
}
@@ -2,11 +2,9 @@ package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; 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.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomer;
@@ -46,7 +44,6 @@ import java.util.Map;
/// @author 夜雨 /// @author 夜雨
/// @since 2026-07-27 14:12 /// @since 2026-07-27 14:12
@RestController @RestController
@RequireGaoRole(value = {GaoRoleCode.GLOBAL_MANAGER, GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY)
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/gao/customer") @RequestMapping("/gao/customer")
public class GaoCustomerController { public class GaoCustomerController {
@@ -1,9 +1,7 @@
package com.imyeyu.api.modules.gao.controller; package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; 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.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoEvent;
@@ -30,7 +28,6 @@ import java.util.Map;
/// @author 夜雨 /// @author 夜雨
/// @since 2026-07-27 14:13 /// @since 2026-07-27 14:13
@RestController @RestController
@RequireGaoRole(value = {GaoRoleCode.GLOBAL_MANAGER, GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY)
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/gao/event") @RequestMapping("/gao/event")
public class GaoEventController { public class GaoEventController {
@@ -2,11 +2,9 @@ package com.imyeyu.api.modules.gao.controller;
import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; 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.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomer;
@@ -48,7 +46,6 @@ import java.util.Map;
/// @author 夜雨 /// @author 夜雨
/// @since 2026-07-27 14:11 /// @since 2026-07-27 14:11
@RestController @RestController
@RequireGaoRole(value = {GaoRoleCode.GLOBAL_MANAGER, GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY)
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/gao/event/record") @RequestMapping("/gao/event/record")
public class GaoEventRecordController { public class GaoEventRecordController {
@@ -0,0 +1,47 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/// GAO 客户积分账户接口
///
/// @author Codex
/// @since 2026-08-11
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/customer/point")
public class GaoPointAccountController {
private final RoleChecker roleChecker;
private final GaoStoreService storeService;
private final GaoPointAccountService accountService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
@PostMapping("/account/detail")
public GaoPointAccount detail(@RequestParam String customerId, @RequestParam(required = false) String storeId) {
storeId = resolveStoreId(storeId);
return accountService.getByCustomerId(storeId, customerId);
}
private String resolveStoreId(String requestedStoreId) {
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
return requestedStoreId;
}
return storeService.getBelongIdByRequiredLoginUserId();
}
}
@@ -0,0 +1,63 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/// GAO 客户积分流水接口
///
/// @author Codex
/// @since 2026-08-11
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/customer/point")
public class GaoPointLedgerController {
private final RoleChecker roleChecker;
private final GaoStoreService storeService;
private final GaoPointLedgerService ledgerService;
private final GaoPointTriggerService triggerService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_LEDGER_READ)
@PostMapping("/ledger/list")
public PageResult<GaoPointLedger> list(@RequestBody GaoPointLedgerPage page) {
TimiException.required(page, "not found page");
page.setStoreId(resolveStoreId(page.getStoreId()));
return ledgerService.pageByQuery(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_ADJUST)
@PostMapping("/adjust")
public void adjust(@RequestBody GaoPointAdjustRequest request) {
request.setStoreId(resolveStoreId(request.getStoreId()));
triggerService.adjust(request);
}
private String resolveStoreId(String requestedStoreId) {
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
return requestedStoreId;
}
return storeService.getBelongIdByRequiredLoginUserId();
}
}
@@ -0,0 +1,108 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/// GAO 客户积分规则接口
///
/// @author Codex
/// @since 2026-08-11
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/customer/point")
public class GaoPointRuleController {
private final RoleChecker roleChecker;
private final GaoStoreService storeService;
private final GaoPointRuleService ruleService;
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ)
@PostMapping("/rule/list")
public PageResult<GaoPointRule> list(@RequestBody Page<GaoPointRule> page) {
TimiException.required(page, "not found page");
if (!isGlobalManager()) {
GaoPointRule example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoPointRule());
example.setStoreId(resolveStoreId(null));
page.setEqualsExample(example);
}
return ruleService.page(page);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ)
@PostMapping("/rule/detail")
public GaoPointRule detail(@RequestParam String id) {
GaoPointRule rule = ruleService.get(id);
TimiException.required(rule, "not found point rule");
checkStore(rule.getStoreId());
return rule;
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE)
@PostMapping("/rule/create")
public String create(@RequestBody GaoPointRule rule) {
rule.setStoreId(resolveStoreId(rule.getStoreId()));
ruleService.create(rule);
return rule.getId();
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_UPDATE)
@PostMapping("/rule/update")
public void update(@RequestBody GaoPointRule rule) {
rule.setStoreId(resolveStoreId(rule.getStoreId()));
ruleService.update(rule);
}
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_DELETE)
@PostMapping("/rule/delete")
public void delete(@RequestParam String id) {
GaoPointRule rule = ruleService.get(id);
TimiException.required(rule, "not found point rule");
checkStore(rule.getStoreId());
ruleService.delete(id);
}
private String resolveStoreId(String requestedStoreId) {
if (isGlobalManager()) {
return requestedStoreId;
}
return storeService.getBelongIdByRequiredLoginUserId();
}
private void checkStore(String storeId) {
if (!isGlobalManager()) {
TimiException.requiredTrue(resolveStoreId(null).equals(storeId), "无权操作其他门店数据");
}
}
private boolean isGlobalManager() {
return roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name());
}
}
@@ -1,10 +1,11 @@
package com.imyeyu.api.modules.gao.controller; package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoRole;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.service.RoleService; import com.imyeyu.api.modules.user.service.AuthorizationScopeService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit; import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -19,17 +20,18 @@ import java.util.List;
/// @author 夜雨 /// @author 夜雨
/// @since 2026-07-28 14:15 /// @since 2026-07-28 14:15
@RestController @RestController
@RequireGaoRole(GaoRoleCode.STORE_MANAGER)
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/gao/role") @RequestMapping("/gao/role")
public class GaoRoleController { public class GaoRoleController {
private final RoleService roleService; private final UserLoginService userLoginService;
private final AuthorizationScopeService authorizationScopeService;
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireCorePermission(CorePermissionCode.ROLE_READ)
@PostMapping("/available/list") @PostMapping("/available/list")
public List<Role> availableList() { public List<Role> availableList() {
return roleService.listByModuleCode(ModuleCode.GAO); return authorizationScopeService.listGrantableRole(userLoginService.getRequireLoginUserId(), ModuleCode.GAO);
} }
} }
@@ -1,9 +1,7 @@
package com.imyeyu.api.modules.gao.controller; package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; 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.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay;
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
@@ -26,7 +24,6 @@ import java.util.List;
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/gao/store/business/day") @RequestMapping("/gao/store/business/day")
@RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER)
public class GaoStoreBusinessDayController { public class GaoStoreBusinessDayController {
private final GaoStoreBusinessDayService service; private final GaoStoreBusinessDayService service;
@@ -57,7 +54,7 @@ public class GaoStoreBusinessDayController {
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STORE_CREATE) @RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
@PostMapping("/create") @PostMapping("/create")
public String create(@RequestBody GaoStoreBusinessDay businessDay) { public String create(@RequestBody GaoStoreBusinessDay businessDay) {
service.create(businessDay); service.create(businessDay);
@@ -0,0 +1,68 @@
package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// GAO 门店报表接口
///
/// @author Codex
/// @since 2026-08-11
@RestController
@RequiredArgsConstructor
@RequestMapping("/gao/store/chart")
public class GaoStoreChartController {
private final GaoStoreChartService service;
/// 查询客户增长趋势
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/daily")
public List<GaoCustomerDailyStateView> customerDaily(@RequestBody GaoStoreChartReq req) {
return service.customerDailyTrend(req);
}
/// 查询客户性别统计
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/gender")
public List<GaoCustomerGenderStatView> customerGender(@RequestParam String storeId) {
return service.customerGender(storeId);
}
/// 查询登记事件每日统计,可同时支撑登记趋势和事件结构图表
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/daily")
public List<GaoEventRecordDailyStatView> eventRecordDaily(@RequestBody GaoStoreChartReq req) {
return service.eventRecordDailyStat(req);
}
/// 查询登记事件日历记录
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/event/record/calendar")
public List<GaoEventRecordCalendarView> eventRecordCalendar(@RequestBody GaoStoreChartReq req) {
return service.eventRecordCalendar(req);
}
}
@@ -1,13 +1,13 @@
package com.imyeyu.api.modules.gao.controller; package com.imyeyu.api.modules.gao.controller;
import com.imyeyu.api.bean.RoleMatchMode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; 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.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.service.GaoStoreService; import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.network.Network; import com.imyeyu.network.Network;
import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
@@ -34,11 +34,11 @@ import java.util.List;
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/gao/store") @RequestMapping("/gao/store")
@RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER)
public class GaoStoreController { public class GaoStoreController {
private final GaoStoreService service; private final GaoStoreService service;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final PermissionChecker permissionChecker;
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@@ -70,7 +70,19 @@ public class GaoStoreController {
@RequireGaoPermission(GaoPermissionCode.STORE_UPDATE) @RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
@PostMapping("/update") @PostMapping("/update")
public void update(@RequestBody GaoStore store) { public void update(@RequestBody GaoStore store) {
if (permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.STORE_CREATE.getValue())) {
// 门店管理
service.update(store); service.update(store);
} else {
// 店长
GaoStore dbStore = service.get(store.getId());
dbStore.setName(store.getName());
dbStore.setTelephone(store.getTelephone());
dbStore.setAddress(store.getAddress());
dbStore.setRemark(store.getRemark());
dbStore.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
service.update(dbStore);
}
} }
@AOPLog @AOPLog
@@ -104,7 +116,6 @@ public class GaoStoreController {
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoRole(value = {GaoRoleCode.GLOBAL_MANAGER, GaoRoleCode.STORE_MANAGER, GaoRoleCode.EMPLOYEE}, mode = RoleMatchMode.ANY)
@PostMapping("/user") @PostMapping("/user")
public GaoStore getLoginStore() { public GaoStore getLoginStore() {
return service.getByUserId(userLoginService.getRequireLoginUserId()); return service.getByUserId(userLoginService.getRequireLoginUserId());
@@ -5,7 +5,6 @@ import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; 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.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoUser; import com.imyeyu.api.modules.gao.entity.GaoUser;
@@ -39,7 +38,6 @@ import java.util.Map;
/// @author 夜雨 /// @author 夜雨
/// @since 2026-07-30 14:15 /// @since 2026-07-30 14:15
@RestController @RestController
@RequireGaoRole(GaoRoleCode.STORE_MANAGER)
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/gao/user") @RequestMapping("/gao/user")
public class GaoUserController { public class GaoUserController {
@@ -118,7 +116,6 @@ public class GaoUserController {
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoRole(GaoRoleCode.GLOBAL_MANAGER)
@RequireGaoPermission(GaoPermissionCode.USER_UPDATE) @RequireGaoPermission(GaoPermissionCode.USER_UPDATE)
@PostMapping("/transfer/store") @PostMapping("/transfer/store")
@JsonView(ResponseView.Public.class) @JsonView(ResponseView.Public.class)
@@ -52,6 +52,10 @@ public class GaoEventRecord extends UUIDEntity {
/// 营业日递增序号 /// 营业日递增序号
private Long businessDayNo; private Long businessDayNo;
/// 本次登记提交的积分值,只参与积分触发,不落库
@Transient
private Long pointValue;
@Transient @Transient
private GaoCustomer customer; private GaoCustomer customer;
@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.gao.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// GAO 客户积分账户
///
/// @author Codex
/// @since 2026-08-11
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPointAccount extends UUIDEntity {
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 当前可用积分余额
private Long balance;
/// 累计获得积分
private Long totalEarned;
/// 累计撤销积分
private Long totalRevoked;
/// 累计人工调整积分,可正可负
private Long totalAdjusted;
/// 乐观锁版本
private Long version;
}
@@ -0,0 +1,91 @@
package com.imyeyu.api.modules.gao.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// GAO 客户积分流水
///
/// @author Codex
/// @since 2026-08-11
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPointLedger extends UUIDEntity {
/// 流水变化类型
public enum ChangeType {
/// 登记事件获得积分
EARN,
/// 删除登记记录撤销积分
REVOKE,
/// 人工调整积分
ADJUST
}
/// 流水方向
public enum Direction {
/// 增加积分
INCREASE,
/// 减少积分
DECREASE
}
/// 客户积分账户 ID
private String accountId;
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 变化类型
private ChangeType changeType;
/// 方向
private Direction direction;
/// 本次变化积分,增加为正,减少为负
private Long pointDelta;
/// 变化前余额
private Long balanceBefore;
/// 变化后余额
private Long balanceAfter;
/// 命中的积分规则 ID
private String ruleId;
/// 登记事件 ID
private String eventId;
/// 登记记录 ID
private String eventRecordId;
/// 来源快照
private JsonNode sourcePayload;
/// 幂等键
private String idempotencyKey;
/// 备注
private String remark;
/// 操作用户 ID
private String operatorUserId;
/// 发生时间
private Long occurredAt;
/// 客户信息
@Transient
private GaoCustomer customer;
}
@@ -0,0 +1,96 @@
package com.imyeyu.api.modules.gao.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// GAO 客户积分规则
///
/// @author Codex
/// @since 2026-08-11
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPointRule extends UUIDEntity {
/// 规则状态
public enum Status {
/// 草稿,不参与触发
DRAFT,
/// 启用,参与触发
ACTIVE,
/// 停用,不参与触发
INACTIVE,
/// 已删除,不参与触发
DELETED
}
/// 触发类型
public enum TriggerType {
/// 每登记一次触发一次
EVERY_TIME,
/// 客户首次登记该事件触发一次
FIRST_TIME,
/// 客户累计登记达到 N 次后每次触发
AFTER_COUNT_EVERY_TIME,
/// 使用登记接口提交的积分值触发
SOURCE_VALUE
}
/// 积分值模式
public enum PointMode {
/// 使用规则固定积分值
FIXED,
/// 使用登记接口提交的积分值
SOURCE_VALUE
}
/// 门店 ID
private String storeId;
/// 登记事件 ID
private String eventId;
/// 规则名称
private String name;
/// 规则状态
private Status status;
/// 触发类型
private TriggerType triggerType;
/// 积分值模式
private PointMode pointMode;
/// 固定积分值
private Long pointValue;
/// 条件配置
private JsonNode conditionJson;
/// 生效开始时间戳,毫秒,空为不限制
private Long beginAt;
/// 生效结束时间戳,毫秒,空为不限制
private Long endAt;
/// 优先级,数值小优先
private Integer priority;
/// true 为命中后停止继续匹配后续规则
private Boolean exclusive;
/// 备注
private String remark;
}
@@ -48,6 +48,9 @@ public class GaoStore extends UUIDEntity {
/// 备注 /// 备注
private String remark; private String remark;
/// true 为启用自动计算营业日休息日
private Boolean autoClosedDay;
/// 排序值 /// 排序值
private Integer sort; private Integer sort;
@@ -18,7 +18,7 @@ public class GaoStoreBusinessDay extends UUIDEntity {
/// ///
/// @author Codex /// @author Codex
/// @since 2026-08-06 /// @since 2026-08-06
public enum OpenStatus { public enum Status {
/// 营业 /// 营业
OPEN, OPEN,
@@ -34,7 +34,7 @@ public class GaoStoreBusinessDay extends UUIDEntity {
private Integer dateValue; private Integer dateValue;
/// 营业状态 /// 营业状态
private OpenStatus openStatus; private Status status;
/// 营业日递增序号,仅营业日有值 /// 营业日递增序号,仅营业日有值
private Long businessDayNo; private Long businessDayNo;
@@ -1,9 +1,13 @@
package com.imyeyu.api.modules.gao.mapper; package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -49,4 +53,28 @@ public interface GaoEventRecordMapper extends BaseMapper<GaoEventRecord, String>
/// @param page 排行榜分页请求 /// @param page 排行榜分页请求
/// @return 客户登记排行榜 /// @return 客户登记排行榜
List<GaoCustomerEventRankView> selectRankByRange(GaoEventRecordRankPage page); List<GaoCustomerEventRankView> selectRankByRange(GaoEventRecordRankPage page);
/// 按时间范围和事件批量统计每日登记数
///
/// @param req 门店报表查询请求
/// @return 每日登记数
List<GaoEventRecordDailyStatView> stateDailyByRange(GaoStoreChartReq req);
/// 按时间范围查询登记日历记录
///
/// @param req 门店报表查询请求
/// @return 日历记录列表
List<GaoEventRecordCalendarView> selectCalendarByRange(GaoStoreChartReq req);
/// 按客户、时间范围和事件批量统计每日登记数
///
/// @param req 客户报表查询请求
/// @return 每日登记数
List<GaoEventRecordDailyStatView> stateCustomerDailyByRange(GaoCustomerChartReq req);
/// 按客户和时间范围查询登记日历记录
///
/// @param req 客户报表查询请求
/// @return 日历记录列表
List<GaoEventRecordCalendarView> selectCustomerCalendarByRange(GaoCustomerChartReq req);
} }
@@ -0,0 +1,41 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
/// GAO 客户积分账户 Mapper
///
/// @author Codex
/// @since 2026-08-11
public interface GaoPointAccountMapper extends BaseMapper<GaoPointAccount, String> {
/// 查询客户当前有效积分账户
///
/// @param storeId 门店 ID
/// @param customerId 客户 ID
/// @return 积分账户
GaoPointAccount selectByStoreIdAndCustomerId(@Param("storeId") String storeId, @Param("customerId") String customerId);
/// 创建客户积分账户,已存在时保持原账户不变
///
/// @param account 积分账户
void insertIfAbsent(@Param("account") GaoPointAccount account);
/// 锁定积分账户
///
/// @param id 账户 ID
/// @return 积分账户
GaoPointAccount selectForUpdate(@Param("id") String id);
/// 原子更新积分账户余额和统计值
///
/// @param id 账户 ID
/// @param delta 本次余额变化
/// @param totalEarnedDelta 累计获得积分变化
/// @param totalRevokedDelta 累计撤销积分变化
/// @param totalAdjustedDelta 累计人工调整积分变化
/// @param updatedAt 更新时间
/// @return 更新行数
int updateBalance(@Param("id") String id, @Param("delta") long delta, @Param("totalEarnedDelta") long totalEarnedDelta, @Param("totalRevokedDelta") long totalRevokedDelta, @Param("totalAdjustedDelta") long totalAdjustedDelta, @Param("updatedAt") long updatedAt);
}
@@ -0,0 +1,45 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 客户积分流水 Mapper
///
/// @author Codex
/// @since 2026-08-11
public interface GaoPointLedgerMapper extends BaseMapper<GaoPointLedger, String> {
/// 按幂等键查询流水
///
/// @param idempotencyKey 幂等键
/// @return 流水
GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey);
/// 查询登记记录产生的原始获得积分流水
///
/// @param eventRecordId 登记记录 ID
/// @return 获得积分流水
List<GaoPointLedger> selectEarnByEventRecordId(@Param("eventRecordId") String eventRecordId);
/// 批量查询幂等键对应的流水
///
/// @param idempotencyKeyList 幂等键列表
/// @return 已存在流水
List<GaoPointLedger> selectByIdempotencyKeyList(@Param("idempotencyKeyList") List<String> idempotencyKeyList);
/// 查询积分流水总数
///
/// @param page 查询条件
/// @return 流水总数
long countByQuery(GaoPointLedgerPage page);
/// 分页查询积分流水
///
/// @param page 查询条件
/// @return 流水列表
List<GaoPointLedger> selectByQuery(GaoPointLedgerPage page);
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// GAO 客户积分规则 Mapper
///
/// @author Codex
/// @since 2026-08-11
public interface GaoPointRuleMapper extends BaseMapper<GaoPointRule, String> {
/// 查询当前时间可匹配的启用规则
///
/// @param storeId 门店 ID
/// @param eventId 登记事件 ID
/// @param now 当前时间戳
/// @return 按优先级排序的规则列表
List<GaoPointRule> selectActiveByStoreIdAndEventId(@Param("storeId") String storeId, @Param("eventId") String eventId, @Param("now") long now);
}
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List; import java.util.List;
@@ -27,7 +28,7 @@ public interface GaoStoreBusinessDayMapper extends BaseMapper<GaoStoreBusinessDa
/// @param storeId 门店 ID /// @param storeId 门店 ID
/// @param dateValue 日期值,格式为 yyyyMMdd /// @param dateValue 日期值,格式为 yyyyMMdd
/// @return 营业日序号 /// @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") @Select("SELECT MAX(`business_day_no`) FROM `gao_store_business_day` WHERE `store_id` = #{storeId} AND `date_value` < #{dateValue} AND `status` = 'OPEN' AND `deleted_at` IS NULL")
Long selectLastBusinessDayNoBefore(@Param("storeId") String storeId, @Param("dateValue") Integer dateValue); Long selectLastBusinessDayNoBefore(@Param("storeId") String storeId, @Param("dateValue") Integer dateValue);
/// 查询指定日期及之后的营业日 /// 查询指定日期及之后的营业日
@@ -35,9 +36,16 @@ public interface GaoStoreBusinessDayMapper extends BaseMapper<GaoStoreBusinessDa
/// @param storeId 门店 ID /// @param storeId 门店 ID
/// @param beginDateValue 开始日期值,格式为 yyyyMMdd /// @param beginDateValue 开始日期值,格式为 yyyyMMdd
/// @return 营业日列表 /// @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") @Select("SELECT * FROM `gao_store_business_day` WHERE `store_id` = #{storeId} AND `date_value` >= #{beginDateValue} AND `status` = 'OPEN' AND `deleted_at` IS NULL ORDER BY `date_value` ASC")
List<GaoStoreBusinessDay> selectOpenListFromDate(@Param("storeId") String storeId, @Param("beginDateValue") Integer beginDateValue); List<GaoStoreBusinessDay> selectOpenListFromDate(@Param("storeId") String storeId, @Param("beginDateValue") Integer beginDateValue);
/// 清空指定日期及之后的营业日序号
///
/// @param storeId 门店 ID
/// @param beginDateValue 开始日期值,格式为 yyyyMMdd
@Update("UPDATE `gao_store_business_day` SET `business_day_no` = NULL, `updated_at` = ROUND(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000) WHERE `store_id` = #{storeId} AND `date_value` >= #{beginDateValue} AND `status` = 'OPEN' AND `deleted_at` IS NULL")
void clearBusinessDayNoFromDate(@Param("storeId") String storeId, @Param("beginDateValue") Integer beginDateValue);
/// 查询日期范围内的营业日 /// 查询日期范围内的营业日
/// ///
/// @param storeId 门店 ID /// @param storeId 门店 ID
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.gao.mapper;
import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -16,6 +17,12 @@ public interface GaoStoreMapper extends BaseMapper<GaoStore, String> {
List<GaoStore> selectByIdList(@Param("idList") Collection<String> idList); List<GaoStore> selectByIdList(@Param("idList") Collection<String> idList);
/// 查询启用自动计算营业日休息日的可用门店
///
/// @return 门店列表
@Select("SELECT * FROM `gao_store` WHERE `auto_closed_day` = TRUE AND `status` = 'ACTIVE' " + NOT_DELETE)
List<GaoStore> selectAllByAutoClosedDay();
/// 查询用户所属的可用门店 /// 查询用户所属的可用门店
/// ///
/// @param userId 用户 ID /// @param userId 用户 ID
@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import java.util.List;
/// 客户报表服务
///
/// @author Codex
/// @since 2026-08-11
public interface GaoCustomerChartService {
/// 查询客户登记每日统计
List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoCustomerChartReq req);
/// 查询客户登记日历记录
List<GaoEventRecordCalendarView> eventRecordCalendar(GaoCustomerChartReq req);
}
@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.spring.service.BaseService;
/// GAO 客户积分账户服务
///
/// @author Codex
/// @since 2026-08-11
public interface GaoPointAccountService extends BaseService<GaoPointAccount, String> {
/// 查询客户当前有效积分账户
///
/// @param storeId 门店 ID
/// @param customerId 客户 ID
/// @return 积分账户,不存在时返回 null
GaoPointAccount getByCustomerId(String storeId, String customerId);
/// 获取并锁定客户积分账户,不存在时创建
///
/// @param storeId 门店 ID
/// @param customerId 客户 ID
/// @return 已锁定的积分账户
GaoPointAccount getOrCreateForUpdate(String storeId, String customerId);
/// 原子更新账户余额
///
/// @param accountId 账户 ID
/// @param delta 本次余额变化
/// @param totalEarnedDelta 累计获得积分变化
/// @param totalRevokedDelta 累计撤销积分变化
/// @param totalAdjustedDelta 累计人工调整积分变化
/// @return 更新行数
int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta);
}
@@ -0,0 +1,46 @@
package com.imyeyu.api.modules.gao.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
/// GAO 客户积分流水服务
///
/// @author Codex
/// @since 2026-08-11
public interface GaoPointLedgerService extends BaseService<GaoPointLedger, String> {
/// 写入登记获得积分流水
///
/// @param record 登记记录
/// @param rule 命中规则
/// @param pointDelta 积分变化
/// @param sourcePayload 来源快照
/// @return 积分流水
GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload);
/// 写入删除登记记录的撤销流水
///
/// @param originLedger 原始获得积分流水
/// @param record 登记记录
/// @return 撤销流水
GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record);
/// 写入人工调整流水
///
/// @param request 人工调整请求
/// @param operatorUserId 操作用户 ID
/// @return 调整流水
GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId);
/// 分页查询积分流水
///
/// @param page 查询条件
/// @return 分页结果
PageResult<GaoPointLedger> pageByQuery(GaoPointLedgerPage page);
}
@@ -0,0 +1,36 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// GAO 客户积分规则服务
///
/// @author Codex
/// @since 2026-08-11
public interface GaoPointRuleService extends BaseService<GaoPointRule, String> {
/// 查询当前时间可匹配的启用规则
///
/// @param storeId 门店 ID
/// @param eventId 登记事件 ID
/// @param now 当前时间戳
/// @return 规则列表
List<GaoPointRule> listActive(String storeId, String eventId, long now);
/// 创建积分规则
///
/// @param rule 积分规则
void create(GaoPointRule rule);
/// 修改积分规则
///
/// @param rule 积分规则
void update(GaoPointRule rule);
/// 删除积分规则
///
/// @param id 规则 ID
void delete(String id);
}
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
/// GAO 客户积分触发服务
///
/// @author Codex
/// @since 2026-08-11
public interface GaoPointTriggerService {
/// 在登记记录创建成功后触发积分
///
/// @param record 登记记录
void onEventRecordCreated(GaoEventRecord record);
/// 撤销登记记录产生的全部获得积分
///
/// @param record 登记记录
void revokeByEventRecord(GaoEventRecord record);
/// 执行人工调整
///
/// @param request 人工调整请求
/// @return 调整流水
GaoPointLedger adjust(GaoPointAdjustRequest request);
}
@@ -23,6 +23,12 @@ public interface GaoStoreBusinessDayService extends BaseService<GaoStoreBusiness
/// @param beginDateValue 开始日期值,格式为 yyyyMMdd /// @param beginDateValue 开始日期值,格式为 yyyyMMdd
void rebuildBusinessDayNo(String storeId, Integer beginDateValue); void rebuildBusinessDayNo(String storeId, Integer beginDateValue);
/// 自动补记门店休息日
///
/// @param storeId 门店 ID
/// @param dateValue 日期值,格式为 yyyyMMdd
void autoCloseBusinessDay(String storeId, Integer dateValue);
/// 查询日期范围内的营业日 /// 查询日期范围内的营业日
/// ///
/// @param storeId 门店 ID /// @param storeId 门店 ID
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import java.util.List;
/// 门店报表服务
///
/// @author Codex
/// @since 2026-08-11
public interface GaoStoreChartService {
/// 查询客户每日趋势
List<GaoCustomerDailyStateView> customerDailyTrend(GaoStoreChartReq req);
/// 查询客户性别统计
List<GaoCustomerGenderStatView> customerGender(String storeId);
/// 查询登记事件每日统计
List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req);
/// 查询登记事件日历记录
List<GaoEventRecordCalendarView> eventRecordCalendar(GaoStoreChartReq req);
}
@@ -22,6 +22,8 @@ public interface GaoStoreService extends BaseService<GaoStore, String> {
String getBelongIdByRequiredLoginUserId(); String getBelongIdByRequiredLoginUserId();
List<GaoStore> listByAutoClosedDay();
/// 导出门店列表 /// 导出门店列表
/// ///
/// @param page 分页查询条件 /// @param page 分页查询条件
@@ -0,0 +1,44 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/// 客户报表服务实现
///
/// @author Codex
/// @since 2026-08-11
@Service
@RequiredArgsConstructor
public class GaoCustomerChartServiceImplement implements GaoCustomerChartService {
private final GaoEventRecordMapper eventRecordMapper;
@Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoCustomerChartReq req) {
checkRangeReq(req);
return eventRecordMapper.stateCustomerDailyByRange(req);
}
@Override
public List<GaoEventRecordCalendarView> eventRecordCalendar(GaoCustomerChartReq req) {
checkRangeReq(req);
return eventRecordMapper.selectCustomerCalendarByRange(req);
}
private void checkRangeReq(GaoCustomerChartReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getStoreId(), "not found req.storeId");
TimiException.required(req.getCustomerId(), "not found req.customerId");
TimiException.required(req.getBeginAt(), "not found req.beginAt");
TimiException.required(req.getEndAt(), "not found req.endAt");
TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "req.endAt lt req.beginAt");
}
}
@@ -65,6 +65,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
return mapper; return mapper;
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void create(GaoCustomer customer) { public void create(GaoCustomer customer) {
TimiException.required(customer.getName(), "not found customer.name"); TimiException.required(customer.getName(), "not found customer.name");
@@ -118,6 +119,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
} }
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void update(GaoCustomer customer) { public void update(GaoCustomer customer) {
TimiException.required(customer, "not found customer"); TimiException.required(customer, "not found customer");
@@ -196,6 +198,7 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
} }
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void updateWithAttachment(GaoCustomer customer) { public void updateWithAttachment(GaoCustomer customer) {
update(customer); update(customer);
@@ -10,6 +10,7 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
import com.imyeyu.api.modules.gao.service.GaoEventService; import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
@@ -63,6 +64,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final GaoStoreBusinessDayService businessDayService; private final GaoStoreBusinessDayService businessDayService;
private final GaoPointTriggerService pointTriggerService;
private final GaoEventRecordMapper mapper; private final GaoEventRecordMapper mapper;
@@ -116,6 +118,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
dbRecord.setRegisteredAt(registeredAt); dbRecord.setRegisteredAt(registeredAt);
dbRecord.setRegisteredDateValue(registeredDateValue); dbRecord.setRegisteredDateValue(registeredDateValue);
dbRecord.setBusinessDayNo(businessDayNo); dbRecord.setBusinessDayNo(businessDayNo);
dbRecord.setPointValue(record.getPointValue());
super.create(dbRecord); super.create(dbRecord);
// 附件 // 附件
if (i == 0) { if (i == 0) {
@@ -131,6 +134,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
attachmentService.update(clone); attachmentService.update(clone);
} }
} }
pointTriggerService.onEventRecordCreated(dbRecord);
logIdList.add(dbRecord.getId()); logIdList.add(dbRecord.getId());
} }
logger.setLevel(Logger.Level.INFO); logger.setLevel(Logger.Level.INFO);
@@ -149,6 +153,14 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
} }
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void createBatch(List<GaoEventRecord> recordList) {
for (GaoEventRecord record : TimiJava.safeIterable(recordList)) {
create(record);
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void update(GaoEventRecord record) { public void update(GaoEventRecord record) {
@@ -193,6 +205,33 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
} }
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
TimiException.required(id, "not found record.id");
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_DELETE");
try {
logger.setContent(id);
GaoEventRecord record = get(id);
TimiException.required(record, "not found event record");
pointTriggerService.revokeByEventRecord(record);
super.delete(id);
logger.setLevel(Logger.Level.INFO);
logger.setResult(id);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("删除 GAO 登记记录失败", e);
throw new TimiException(TimiCode.ERROR, "删除 GAO 登记记录失败", e);
} finally {
loggerService.create(logger);
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void createWithCustomer(GaoCustomer customer) { public void createWithCustomer(GaoCustomer customer) {
@@ -240,6 +279,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
@Override @Override
public PageResult<GaoCustomerEventRankView> pageRankByRange(GaoEventRecordRankPage page) { public PageResult<GaoCustomerEventRankView> pageRankByRange(GaoEventRecordRankPage page) {
TimiException.required(page.getStoreId(), "not found page.storeId"); TimiException.required(page.getStoreId(), "not found page.storeId");
page.setTodayDateValue(dateValue(Time.now()));
PageResult<GaoCustomerEventRankView> result = new PageResult<>(); PageResult<GaoCustomerEventRankView> result = new PageResult<>();
result.setTotal(mapper.countRankByRange(page)); result.setTotal(mapper.countRankByRange(page));
result.setList(mapper.selectRankByRange(page)); result.setList(mapper.selectRankByRange(page));
@@ -0,0 +1,71 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/// GAO 客户积分账户服务实现
///
/// @author Codex
/// @since 2026-08-11
@Service
@RequiredArgsConstructor
public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPointAccount, String> implements GaoPointAccountService {
private final GaoPointAccountMapper mapper;
@Override
protected BaseMapper<GaoPointAccount, String> mapper() {
return mapper;
}
@Override
public GaoPointAccount getByCustomerId(String storeId, String customerId) {
TimiException.required(storeId, "not found storeId");
TimiException.required(customerId, "not found customerId");
return mapper.selectByStoreIdAndCustomerId(storeId, customerId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoPointAccount getOrCreateForUpdate(String storeId, String customerId) {
TimiException.required(storeId, "not found storeId");
TimiException.required(customerId, "not found customerId");
GaoPointAccount account = mapper.selectByStoreIdAndCustomerId(storeId, customerId);
if (account == null) {
account = new GaoPointAccount();
account.setId(UUID.randomUUID().toString());
account.setStoreId(storeId);
account.setCustomerId(customerId);
account.setBalance(0L);
account.setTotalEarned(0L);
account.setTotalRevoked(0L);
account.setTotalAdjusted(0L);
account.setVersion(0L);
account.setCreatedAt(Time.now());
mapper.insertIfAbsent(account);
account = mapper.selectByStoreIdAndCustomerId(storeId, customerId);
}
TimiException.required(account, "not found customer point account");
GaoPointAccount lockedAccount = mapper.selectForUpdate(account.getId());
TimiException.required(lockedAccount, "not found customer point account");
return lockedAccount;
}
@Override
public int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta) {
TimiException.required(accountId, "not found accountId");
return mapper.updateBalance(accountId, delta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta, Time.now());
}
}
@@ -0,0 +1,170 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/// GAO 客户积分流水服务实现
///
/// @author Codex
/// @since 2026-08-11
@Service
@RequiredArgsConstructor
public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPointLedger, String> implements GaoPointLedgerService {
private final GaoPointLedgerMapper mapper;
private final GaoPointAccountService accountService;
private final GaoCustomerService customerService;
private final AttachmentService attachmentService;
private final UserLoginService userLoginService;
@Override
protected BaseMapper<GaoPointLedger, String> mapper() {
return mapper;
}
@Override
public void update(GaoPointLedger ledger) {
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止修改");
}
@Override
public void delete(String id) {
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止删除");
}
@Override
public void destroy(String id) {
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止物理删除");
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload) {
TimiException.required(record, "not found event record");
TimiException.required(rule, "not found point rule");
TimiException.requiredTrue(0 < pointDelta, "pointDelta invalid");
String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId());
return apply(record.getStoreId(), record.getCustomerId(), GaoPointLedger.ChangeType.EARN, pointDelta, rule.getId(), record.getEventId(), record.getId(), sourcePayload, idempotencyKey, rule.getRemark(), record.getOperatorUserId(), record.getRegisteredAt());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record) {
TimiException.required(originLedger, "not found origin ledger");
TimiException.required(record, "not found event record");
TimiException.requiredTrue(0 < originLedger.getPointDelta(), "origin ledger pointDelta invalid");
String idempotencyKey = "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId());
return apply(originLedger.getStoreId(), originLedger.getCustomerId(), GaoPointLedger.ChangeType.REVOKE, -originLedger.getPointDelta(), originLedger.getRuleId(), originLedger.getEventId(), originLedger.getEventRecordId(), originLedger.getSourcePayload(), idempotencyKey, "撤销登记记录积分", userLoginService.getRequireLoginUserId(), Time.now());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId) {
TimiException.required(request, "not found adjust request");
TimiException.requiredTrue(TimiJava.isNotEmpty(request.getAdjustRequestId()), "not found adjustRequestId");
TimiException.required(request.getStoreId(), "not found storeId");
TimiException.required(request.getCustomerId(), "not found customerId");
TimiException.required(request.getPointDelta(), "not found pointDelta");
TimiException.requiredTrue(request.getPointDelta() != 0, "pointDelta invalid");
TimiException.requiredTrue(TimiJava.isNotEmpty(request.getRemark()), "not found adjust remark");
TimiException.required(operatorUserId, "not found operatorUserId");
String idempotencyKey = "MANUAL_ADJUST:%s".formatted(request.getAdjustRequestId());
return apply(request.getStoreId(), request.getCustomerId(), GaoPointLedger.ChangeType.ADJUST, request.getPointDelta(), null, null, null, null, idempotencyKey, request.getRemark(), operatorUserId, Time.now());
}
@Override
public PageResult<GaoPointLedger> pageByQuery(GaoPointLedgerPage page) {
TimiException.required(page, "not found page");
PageResult<GaoPointLedger> result = new PageResult<>();
result.setTotal(mapper.countByQuery(page));
result.setList(mapper.selectByQuery(page));
fillCustomer(result.getList());
return result;
}
private void fillCustomer(List<GaoPointLedger> ledgerList) {
List<String> customerIdList = ledgerList.stream().map(GaoPointLedger::getCustomerId).filter(TimiJava::isNotEmpty).distinct().toList();
if (customerIdList.isEmpty()) {
return;
}
Map<String, GaoCustomer> customerMap = customerService.mapByIdList(customerIdList);
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList);
for (GaoPointLedger ledger : ledgerList) {
GaoCustomer customer = customerMap.get(ledger.getCustomerId());
if (customer == null) {
continue;
}
customer.setAttachmentList(attachmentMap.get(customer.getId()));
ledger.setCustomer(customer);
}
}
private GaoPointLedger apply(String storeId, String customerId, GaoPointLedger.ChangeType changeType, long pointDelta, String ruleId, String eventId, String eventRecordId, JsonNode sourcePayload, String idempotencyKey, String remark, String operatorUserId, Long occurredAt) {
GaoPointLedger existing = mapper.selectByIdempotencyKey(idempotencyKey);
if (existing != null) {
return existing;
}
GaoPointAccount account = accountService.getOrCreateForUpdate(storeId, customerId);
existing = mapper.selectByIdempotencyKey(idempotencyKey);
if (existing != null) {
return existing;
}
long balanceBefore = account.getBalance();
long balanceAfter = Math.addExact(balanceBefore, pointDelta);
long totalEarnedDelta = changeType == GaoPointLedger.ChangeType.EARN ? pointDelta : 0L;
long totalRevokedDelta = changeType == GaoPointLedger.ChangeType.REVOKE ? -pointDelta : 0L;
long totalAdjustedDelta = changeType == GaoPointLedger.ChangeType.ADJUST ? pointDelta : 0L;
GaoPointLedger ledger = new GaoPointLedger();
ledger.setAccountId(account.getId());
ledger.setStoreId(storeId);
ledger.setCustomerId(customerId);
ledger.setChangeType(changeType);
ledger.setDirection(0 < pointDelta ? GaoPointLedger.Direction.INCREASE : GaoPointLedger.Direction.DECREASE);
ledger.setPointDelta(pointDelta);
ledger.setBalanceBefore(balanceBefore);
ledger.setBalanceAfter(balanceAfter);
ledger.setRuleId(ruleId);
ledger.setEventId(eventId);
ledger.setEventRecordId(eventRecordId);
ledger.setSourcePayload(sourcePayload);
ledger.setIdempotencyKey(idempotencyKey);
ledger.setRemark(remark);
ledger.setOperatorUserId(operatorUserId);
ledger.setOccurredAt(occurredAt);
create(ledger);
int updated = accountService.updateBalance(account.getId(), pointDelta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta);
TimiException.requiredTrue(0 < updated, "积分余额不足");
return ledger;
}
}
@@ -0,0 +1,154 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.api.modules.gao.entity.GaoEvent;
import com.imyeyu.api.modules.gao.mapper.GaoPointRuleMapper;
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/// GAO 客户积分规则服务实现
///
/// @author Codex
/// @since 2026-08-11
@Service
@RequiredArgsConstructor
public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPointRule, String> implements GaoPointRuleService {
private final GaoPointRuleMapper mapper;
private final GaoEventService eventService;
@Override
protected BaseMapper<GaoPointRule, String> mapper() {
return mapper;
}
@Override
public List<GaoPointRule> listActive(String storeId, String eventId, long now) {
TimiException.required(storeId, "not found storeId");
TimiException.required(eventId, "not found eventId");
return mapper.selectActiveByStoreIdAndEventId(storeId, eventId, now);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(GaoPointRule rule) {
TimiException.required(rule, "not found rule");
TimiException.required(rule.getStoreId(), "not found rule.storeId");
TimiException.required(rule.getEventId(), "not found rule.eventId");
TimiException.required(rule.getName(), "not found rule.name");
checkEvent(rule.getStoreId(), rule.getEventId());
rule.setStatus(TimiJava.defaultIfNull(rule.getStatus(), GaoPointRule.Status.DRAFT));
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
verifyRule(rule);
super.create(rule);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(GaoPointRule rule) {
TimiException.required(rule, "not found rule");
TimiException.required(rule.getId(), "not found rule.id");
TimiException.required(rule.getStoreId(), "not found rule.storeId");
TimiException.required(rule.getEventId(), "not found rule.eventId");
TimiException.required(rule.getName(), "not found rule.name");
GaoPointRule dbRule = get(rule.getId());
TimiException.required(dbRule, "not found rule");
TimiException.requiredTrue(dbRule.getStoreId().equals(rule.getStoreId()), "rule.storeId invalid");
checkEvent(rule.getStoreId(), rule.getEventId());
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
verifyRule(rule);
dbRule.setEventId(rule.getEventId());
dbRule.setName(rule.getName());
dbRule.setStatus(rule.getStatus());
dbRule.setTriggerType(rule.getTriggerType());
dbRule.setPointMode(rule.getPointMode());
dbRule.setPointValue(rule.getPointValue());
dbRule.setConditionJson(rule.getConditionJson());
dbRule.setBeginAt(rule.getBeginAt());
dbRule.setEndAt(rule.getEndAt());
dbRule.setPriority(rule.getPriority());
dbRule.setExclusive(rule.getExclusive());
dbRule.setRemark(rule.getRemark());
mapper.update(dbRule);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
TimiException.required(id, "not found rule.id");
GaoPointRule rule = get(id);
TimiException.required(rule, "not found rule");
rule.setStatus(GaoPointRule.Status.DELETED);
super.update(rule);
super.delete(id);
}
private void checkEvent(String storeId, String eventId) {
GaoEvent event = eventService.get(eventId);
TimiException.required(event, "not found event");
TimiException.requiredTrue(storeId.equals(event.getStoreId()), "规则和登记事件不属于同一门店");
}
private void verifyRule(GaoPointRule rule) {
TimiException.required(rule.getStatus(), "not found rule.status");
TimiException.required(rule.getTriggerType(), "not found rule.triggerType");
TimiException.required(rule.getPointMode(), "not found rule.pointMode");
TimiException.requiredTrue(rule.getPriority() != null && 0 <= rule.getPriority(), "rule.priority invalid");
TimiException.requiredTrue(rule.getBeginAt() == null || rule.getEndAt() == null || rule.getBeginAt() <= rule.getEndAt(), "rule.beginAt or rule.endAt invalid");
if (rule.getTriggerType() == GaoPointRule.TriggerType.SOURCE_VALUE) {
TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.SOURCE_VALUE, "SOURCE_VALUE 规则必须使用 SOURCE_VALUE 积分值模式");
TimiException.requiredNull(rule.getPointValue(), "SOURCE_VALUE 规则不能填写固定积分值");
verifySourceCondition(rule.getConditionJson());
return;
}
TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.FIXED, "固定触发规则必须使用 FIXED 积分值模式");
TimiException.required(rule.getPointValue(), "not found rule.pointValue");
TimiException.requiredTrue(0 < rule.getPointValue(), "rule.pointValue invalid");
if (rule.getTriggerType() == GaoPointRule.TriggerType.AFTER_COUNT_EVERY_TIME) {
JsonNode condition = requireObjectCondition(rule.getConditionJson());
JsonNode threshold = condition.get("threshold");
TimiException.requiredTrue(threshold != null && threshold.isIntegralNumber() && 0 < threshold.longValue(), "AFTER_COUNT_EVERY_TIME.threshold invalid");
JsonNode includeCurrent = condition.get("includeCurrent");
TimiException.requiredTrue(includeCurrent == null || includeCurrent.isNull() || includeCurrent.isBoolean(), "AFTER_COUNT_EVERY_TIME.includeCurrent invalid");
verifyScope(condition);
} else if (rule.getTriggerType() == GaoPointRule.TriggerType.FIRST_TIME) {
verifyScope(requireObjectCondition(rule.getConditionJson()));
}
}
private void verifySourceCondition(JsonNode conditionJson) {
JsonNode condition = conditionJson == null ? null : requireObjectCondition(conditionJson);
Long min = condition == null || condition.get("min") == null || condition.get("min").isNull() ? null : condition.get("min").isIntegralNumber() ? condition.get("min").longValue() : null;
Long max = condition == null || condition.get("max") == null || condition.get("max").isNull() ? null : condition.get("max").isIntegralNumber() ? condition.get("max").longValue() : null;
TimiException.requiredTrue(condition == null || min != null || condition.get("min") == null || condition.get("min").isNull(), "SOURCE_VALUE.min invalid");
TimiException.requiredTrue(condition == null || max != null || condition.get("max") == null || condition.get("max").isNull(), "SOURCE_VALUE.max invalid");
TimiException.requiredTrue(min == null || 0 < min, "SOURCE_VALUE.min invalid");
TimiException.requiredTrue(max == null || 0 < max, "SOURCE_VALUE.max invalid");
TimiException.requiredTrue(min == null || max == null || min <= max, "SOURCE_VALUE.min or SOURCE_VALUE.max invalid");
}
private JsonNode requireObjectCondition(JsonNode conditionJson) {
TimiException.requiredTrue(conditionJson != null && conditionJson.isObject(), "rule.conditionJson must be object");
return conditionJson;
}
private void verifyScope(JsonNode condition) {
JsonNode scope = condition.get("scope");
TimiException.requiredTrue(scope == null || scope.isNull() || "CUSTOMER_EVENT".equals(scope.asText()), "rule.conditionJson.scope invalid");
}
}
@@ -0,0 +1,159 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
import com.imyeyu.api.modules.user.service.PermissionChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/// GAO 客户积分触发服务实现
///
/// @author Codex
/// @since 2026-08-11
@Service
@RequiredArgsConstructor
public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
private final ObjectMapper jackson;
private final PermissionChecker permissionChecker;
private final UserLoginService userLoginService;
private final GaoPointRuleService ruleService;
private final GaoPointLedgerService ledgerService;
private final GaoCustomerService customerService;
private final GaoPointLedgerMapper ledgerMapper;
private final GaoEventRecordMapper eventRecordMapper;
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void onEventRecordCreated(GaoEventRecord record) {
TimiException.required(record, "not found event record");
TimiException.required(record.getStoreId(), "not found record.storeId");
TimiException.required(record.getCustomerId(), "not found record.customerId");
TimiException.required(record.getEventId(), "not found record.eventId");
List<GaoPointRule> ruleList = ruleService.listActive(record.getStoreId(), record.getEventId(), Time.now());
Long count = null;
for (GaoPointRule rule : ruleList) {
Long pointDelta = resolvePointDelta(record, rule, count);
if (pointDelta == null) {
continue;
}
if (count == null) {
count = eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null);
}
JsonNode sourcePayload = sourcePayload(record, rule, pointDelta);
ledgerService.earn(record, rule, pointDelta, sourcePayload);
if (Boolean.TRUE.equals(rule.getExclusive())) {
break;
}
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void revokeByEventRecord(GaoEventRecord record) {
TimiException.required(record, "not found event record");
List<GaoPointLedger> originLedgerList = ledgerMapper.selectEarnByEventRecordId(record.getId());
if (originLedgerList.isEmpty()) {
return;
}
Set<String> revokedKeySet = new HashSet<>();
List<String> revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList();
for (GaoPointLedger ledger : ledgerMapper.selectByIdempotencyKeyList(revokeKeyList)) {
revokedKeySet.add(ledger.getIdempotencyKey());
}
for (GaoPointLedger originLedger : originLedgerList) {
if (!revokedKeySet.contains(revokeKey(record, originLedger))) {
ledgerService.revoke(originLedger, record);
}
}
}
@Override
public GaoPointLedger adjust(GaoPointAdjustRequest request) {
TimiException.required(request, "not found adjust request");
TimiException.required(request.getStoreId(), "not found storeId");
TimiException.required(request.getCustomerId(), "not found customerId");
GaoCustomer customer = customerService.get(request.getCustomerId());
TimiException.required(customer, "not found customer");
TimiException.requiredTrue(request.getStoreId().equals(customer.getStoreId()), "客户和积分调整门店不一致");
return ledgerService.adjust(request, userLoginService.getRequireLoginUserId());
}
private Long resolvePointDelta(GaoEventRecord record, GaoPointRule rule, Long count) {
return switch (rule.getTriggerType()) {
case EVERY_TIME -> rule.getPointValue();
case FIRST_TIME -> {
long currentCount = count == null ? count(record) : count;
yield currentCount == 1 ? rule.getPointValue() : null;
}
case AFTER_COUNT_EVERY_TIME -> {
JsonNode condition = rule.getConditionJson();
long threshold = condition.get("threshold").longValue();
boolean includeCurrent = condition.get("includeCurrent") == null || condition.get("includeCurrent").isNull() || condition.get("includeCurrent").asBoolean();
long currentCount = count == null ? count(record) : count;
yield includeCurrent ? threshold <= currentCount ? rule.getPointValue() : null : threshold < currentCount ? rule.getPointValue() : null;
}
case SOURCE_VALUE -> resolveSourceValue(record, rule);
};
}
private Long resolveSourceValue(GaoEventRecord record, GaoPointRule rule) {
if (record.getPointValue() == null) {
return null;
}
permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.POINT_SOURCE_VALUE.getValue());
long pointValue = record.getPointValue();
TimiException.requiredTrue(0 < pointValue, "pointValue invalid");
JsonNode condition = rule.getConditionJson();
if (condition != null) {
JsonNode min = condition.get("min");
JsonNode max = condition.get("max");
TimiException.requiredTrue(min == null || min.isNull() || min.longValue() <= pointValue, "pointValue 小于规则最小值");
TimiException.requiredTrue(max == null || max.isNull() || pointValue <= max.longValue(), "pointValue 大于规则最大值");
}
return pointValue;
}
private JsonNode sourcePayload(GaoEventRecord record, GaoPointRule rule, long pointDelta) {
if (rule.getTriggerType() != GaoPointRule.TriggerType.SOURCE_VALUE) {
return null;
}
ObjectNode payload = jackson.createObjectNode();
payload.put("submittedPointValue", record.getPointValue());
payload.put("validatedPointValue", pointDelta);
return payload;
}
private long count(GaoEventRecord record) {
return TimiJava.defaultIfNull(eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null), 0L);
}
private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) {
return "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId());
}
}
@@ -0,0 +1,113 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.user.bean.AuthOwnerType;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.entity.UserRole;
import com.imyeyu.api.modules.user.mapper.RoleMapper;
import com.imyeyu.api.modules.user.mapper.UserRoleMapper;
import com.imyeyu.api.modules.user.service.ModuleResourceScopeResolver;
import com.imyeyu.api.modules.user.service.RoleHierarchyService;
import com.imyeyu.java.TimiJava;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/// GAO 资源范围解析器
///
/// @author Codex
/// @since 2026-08-07
@Service
@RequiredArgsConstructor
public class GaoResourceScopeResolver implements ModuleResourceScopeResolver {
private final RoleMapper roleMapper;
private final UserRoleMapper userRoleMapper;
private final GaoStoreService gaoStoreService;
private final RoleHierarchyService roleHierarchyService;
@Override
public ModuleCode moduleCode() {
return ModuleCode.GAO;
}
@Override
public boolean canManageUser(String operatorUserId, String targetUserId) {
if (hasCoreRole(operatorUserId, CoreRoleCode.ADMIN)) {
return true;
}
if (hasGaoRole(operatorUserId, GaoRoleCode.GLOBAL_MANAGER)) {
return true;
}
if (!hasGaoRole(operatorUserId, GaoRoleCode.STORE_MANAGER)) {
return false;
}
GaoStore operatorStore = gaoStoreService.getByUserId(operatorUserId);
GaoStore targetStore = gaoStoreService.getByUserId(targetUserId);
return operatorStore != null && targetStore != null && operatorStore.getId().equals(targetStore.getId());
}
@Override
public boolean canManageRole(String operatorUserId, Role role) {
if (hasCoreRole(operatorUserId, CoreRoleCode.ADMIN)) {
return true;
}
if (hasGaoRole(operatorUserId, GaoRoleCode.GLOBAL_MANAGER)) {
return true;
}
if (!hasGaoRole(operatorUserId, GaoRoleCode.STORE_MANAGER)) {
return false;
}
if (canUseOwnerScope(operatorUserId, role.getOwnerType(), role.getOwnerId())) {
return true;
}
return ModuleCode.GAO == role.getModuleCode()
&& role.getOwnerType() == AuthOwnerType.MODULE
&& (TimiJava.isEmpty(role.getOwnerId()) || ModuleCode.GAO.name().equals(role.getOwnerId()));
}
@Override
public boolean canUseOwnerScope(String operatorUserId, AuthOwnerType ownerType, String ownerId) {
if (hasCoreRole(operatorUserId, CoreRoleCode.ADMIN)) {
return ownerType == AuthOwnerType.MODULE && ModuleCode.GAO.name().equals(ownerId) || ownerType == AuthOwnerType.STORE;
}
if (hasGaoRole(operatorUserId, GaoRoleCode.GLOBAL_MANAGER)) {
return ownerType == AuthOwnerType.MODULE && ModuleCode.GAO.name().equals(ownerId) || ownerType == AuthOwnerType.STORE;
}
if (!hasGaoRole(operatorUserId, GaoRoleCode.STORE_MANAGER) || ownerType != AuthOwnerType.STORE) {
return false;
}
GaoStore store = gaoStoreService.getByUserId(operatorUserId);
return store != null && store.getId().equals(ownerId);
}
private boolean hasGaoRole(String userId, GaoRoleCode roleCode) {
if (TimiJava.isEmpty(userId)) {
return false;
}
List<String> roleIdList = userRoleMapper.selectByUserIdList(List.of(userId)).stream().map(UserRole::getRoleId).toList();
if (TimiJava.isEmpty(roleIdList)) {
return false;
}
// 身份角色只依赖系统、模块级继承,不能依赖当前正在计算的门店作用域
List<String> allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(roleIdList);
return roleMapper.selectByIdList(allRoleIdList).stream().anyMatch(role -> ModuleCode.GAO == role.getModuleCode() && roleCode.name().equals(role.getCode()));
}
private boolean hasCoreRole(String userId, CoreRoleCode roleCode) {
if (TimiJava.isEmpty(userId)) {
return false;
}
List<String> roleIdList = userRoleMapper.selectByUserIdList(List.of(userId)).stream().map(UserRole::getRoleId).toList();
if (TimiJava.isEmpty(roleIdList)) {
return false;
}
List<String> allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(roleIdList);
return roleMapper.selectByIdList(allRoleIdList).stream().anyMatch(role -> ModuleCode.CORE == role.getModuleCode() && roleCode.name().equals(role.getCode()));
}
}
@@ -37,7 +37,7 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
public void create(GaoStoreBusinessDay businessDay) { public void create(GaoStoreBusinessDay businessDay) {
prepareBusinessDay(businessDay); prepareBusinessDay(businessDay);
super.create(businessDay); super.create(businessDay);
if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.OPEN) { if (businessDay.getStatus() == GaoStoreBusinessDay.Status.OPEN) {
rebuildBusinessDayNo(businessDay.getStoreId(), businessDay.getDateValue()); rebuildBusinessDayNo(businessDay.getStoreId(), businessDay.getDateValue());
} }
} }
@@ -51,7 +51,7 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
TimiException.required(dbBusinessDay, "not found dbBusinessDay"); TimiException.required(dbBusinessDay, "not found dbBusinessDay");
TimiException.requiredTrue(dbBusinessDay.getStoreId().equals(businessDay.getStoreId()), "businessDay.storeId invalid"); TimiException.requiredTrue(dbBusinessDay.getStoreId().equals(businessDay.getStoreId()), "businessDay.storeId invalid");
TimiException.requiredTrue(dbBusinessDay.getDateValue().equals(businessDay.getDateValue()), "businessDay.dateValue invalid"); TimiException.requiredTrue(dbBusinessDay.getDateValue().equals(businessDay.getDateValue()), "businessDay.dateValue invalid");
if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.CLOSED) { if (businessDay.getStatus() == GaoStoreBusinessDay.Status.CLOSED) {
checkNoRecord(dbBusinessDay); checkNoRecord(dbBusinessDay);
} }
prepareBusinessDay(businessDay); prepareBusinessDay(businessDay);
@@ -64,7 +64,7 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
public void delete(String id) { public void delete(String id) {
GaoStoreBusinessDay businessDay = get(id); GaoStoreBusinessDay businessDay = get(id);
TimiException.required(businessDay, "not found businessDay"); TimiException.required(businessDay, "not found businessDay");
if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.OPEN) { if (businessDay.getStatus() == GaoStoreBusinessDay.Status.OPEN) {
checkNoRecord(businessDay); checkNoRecord(businessDay);
} }
super.delete(id); super.delete(id);
@@ -78,14 +78,14 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
TimiException.required(dateValue, "not found dateValue"); TimiException.required(dateValue, "not found dateValue");
GaoStoreBusinessDay businessDay = mapper.selectByStoreIdAndDateValue(storeId, dateValue); GaoStoreBusinessDay businessDay = mapper.selectByStoreIdAndDateValue(storeId, dateValue);
if (businessDay != null) { if (businessDay != null) {
TimiException.requiredTrue(businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.OPEN, "门店当日休息,不能登记"); TimiException.requiredTrue(businessDay.getStatus() == GaoStoreBusinessDay.Status.OPEN, "门店当日休息,不能登记");
TimiException.required(businessDay.getBusinessDayNo(), "not found businessDay.businessDayNo"); TimiException.required(businessDay.getBusinessDayNo(), "not found businessDay.businessDayNo");
return businessDay; return businessDay;
} }
businessDay = new GaoStoreBusinessDay(); businessDay = new GaoStoreBusinessDay();
businessDay.setStoreId(storeId); businessDay.setStoreId(storeId);
businessDay.setDateValue(dateValue); businessDay.setDateValue(dateValue);
businessDay.setOpenStatus(GaoStoreBusinessDay.OpenStatus.OPEN); businessDay.setStatus(GaoStoreBusinessDay.Status.OPEN);
super.create(businessDay); super.create(businessDay);
rebuildBusinessDayNo(storeId, dateValue); rebuildBusinessDayNo(storeId, dateValue);
return mapper.selectByStoreIdAndDateValue(storeId, dateValue); return mapper.selectByStoreIdAndDateValue(storeId, dateValue);
@@ -97,16 +97,35 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
TimiException.required(storeId, "not found storeId"); TimiException.required(storeId, "not found storeId");
TimiException.required(beginDateValue, "not found beginDateValue"); TimiException.required(beginDateValue, "not found beginDateValue");
long businessDayNo = TimiJava.defaultIfNull(mapper.selectLastBusinessDayNoBefore(storeId, beginDateValue), 0L); long businessDayNo = TimiJava.defaultIfNull(mapper.selectLastBusinessDayNoBefore(storeId, beginDateValue), 0L);
mapper.clearBusinessDayNoFromDate(storeId, beginDateValue);
for (GaoStoreBusinessDay businessDay : mapper.selectOpenListFromDate(storeId, beginDateValue)) { for (GaoStoreBusinessDay businessDay : mapper.selectOpenListFromDate(storeId, beginDateValue)) {
businessDayNo++; businessDayNo++;
if (!Long.valueOf(businessDayNo).equals(businessDay.getBusinessDayNo())) {
businessDay.setBusinessDayNo(businessDayNo); businessDay.setBusinessDayNo(businessDayNo);
mapper.update(businessDay); mapper.update(businessDay);
}
eventRecordMapper.updateBusinessDayNoByStoreIdAndRegisteredDateValue(storeId, businessDay.getDateValue(), businessDayNo); eventRecordMapper.updateBusinessDayNoByStoreIdAndRegisteredDateValue(storeId, businessDay.getDateValue(), businessDayNo);
} }
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void autoCloseBusinessDay(String storeId, Integer dateValue) {
TimiException.required(storeId, "not found storeId");
TimiException.required(dateValue, "not found dateValue");
if (mapper.selectByStoreIdAndDateValue(storeId, dateValue) != null) {
return;
}
Long count = eventRecordMapper.countByStoreIdAndRegisteredDateValue(storeId, dateValue);
if (0 < TimiJava.defaultIfNull(count, 0L)) {
return;
}
GaoStoreBusinessDay businessDay = new GaoStoreBusinessDay();
businessDay.setStoreId(storeId);
businessDay.setDateValue(dateValue);
businessDay.setStatus(GaoStoreBusinessDay.Status.CLOSED);
businessDay.setReason("系统自动判定休息");
super.create(businessDay);
}
@Override @Override
public List<GaoStoreBusinessDay> listByDateRange(String storeId, Integer beginDateValue, Integer endDateValue) { public List<GaoStoreBusinessDay> listByDateRange(String storeId, Integer beginDateValue, Integer endDateValue) {
TimiException.required(storeId, "not found storeId"); TimiException.required(storeId, "not found storeId");
@@ -120,8 +139,8 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
TimiException.required(businessDay, "not found businessDay"); TimiException.required(businessDay, "not found businessDay");
TimiException.required(businessDay.getStoreId(), "not found businessDay.storeId"); TimiException.required(businessDay.getStoreId(), "not found businessDay.storeId");
TimiException.required(businessDay.getDateValue(), "not found businessDay.dateValue"); TimiException.required(businessDay.getDateValue(), "not found businessDay.dateValue");
businessDay.setOpenStatus(TimiJava.defaultIfNull(businessDay.getOpenStatus(), GaoStoreBusinessDay.OpenStatus.OPEN)); businessDay.setStatus(TimiJava.defaultIfNull(businessDay.getStatus(), GaoStoreBusinessDay.Status.OPEN));
if (businessDay.getOpenStatus() == GaoStoreBusinessDay.OpenStatus.CLOSED) { if (businessDay.getStatus() == GaoStoreBusinessDay.Status.CLOSED) {
businessDay.setBusinessDayNo(null); businessDay.setBusinessDayNo(null);
return; return;
} }
@@ -0,0 +1,64 @@
package com.imyeyu.api.modules.gao.service.implement;
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoStoreChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerDailyStateView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerGenderStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerTrendStateReq;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/// 门店报表服务实现
///
/// @author Codex
/// @since 2026-08-11
@Service
@RequiredArgsConstructor
public class GaoStoreChartServiceImplement implements GaoStoreChartService {
private final GaoCustomerService customerService;
private final GaoEventRecordMapper eventRecordMapper;
@Override
public List<GaoCustomerDailyStateView> customerDailyTrend(GaoStoreChartReq req) {
checkRangeReq(req);
GaoCustomerTrendStateReq statReq = new GaoCustomerTrendStateReq();
statReq.setStoreId(req.getStoreId());
statReq.setBeginCreatedAt(req.getBeginAt());
statReq.setEndCreatedAt(req.getEndAt());
return customerService.stateDailyTrend(statReq);
}
@Override
public List<GaoCustomerGenderStatView> customerGender(String storeId) {
TimiException.required(storeId, "not found storeId");
return customerService.stateGender(storeId);
}
@Override
public List<GaoEventRecordDailyStatView> eventRecordDailyStat(GaoStoreChartReq req) {
checkRangeReq(req);
return eventRecordMapper.stateDailyByRange(req);
}
@Override
public List<GaoEventRecordCalendarView> eventRecordCalendar(GaoStoreChartReq req) {
checkRangeReq(req);
return eventRecordMapper.selectCalendarByRange(req);
}
private void checkRangeReq(GaoStoreChartReq req) {
TimiException.required(req, "not found req");
TimiException.required(req.getStoreId(), "not found req.storeId");
TimiException.required(req.getBeginAt(), "not found req.beginAt");
TimiException.required(req.getEndAt(), "not found req.endAt");
TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "req.endAt lt req.beginAt");
}
}
@@ -50,6 +50,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
return mapper; return mapper;
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void create(GaoStore store) { public void create(GaoStore store) {
TimiException.required(store, "not found store"); TimiException.required(store, "not found store");
@@ -61,6 +62,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
logger.setContent(jackson.writeValueAsString(store)); logger.setContent(jackson.writeValueAsString(store));
checkUnique(store); checkUnique(store);
store.setStatus(TimiJava.defaultIfNull(store.getStatus(), GaoStore.Status.ACTIVE)); store.setStatus(TimiJava.defaultIfNull(store.getStatus(), GaoStore.Status.ACTIVE));
store.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
store.setSort(TimiJava.defaultIfNull(store.getSort(), 0)); store.setSort(TimiJava.defaultIfNull(store.getSort(), 0));
super.create(store); super.create(store);
logger.setLevel(Logger.Level.INFO); logger.setLevel(Logger.Level.INFO);
@@ -72,13 +74,14 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
} catch (Exception e) { } catch (Exception e) {
logger.setLevel(Logger.Level.ERROR); logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e)); logger.setException(TimiJava.serializeThrowable(e));
log.error("新增 GAO 门店失败", e); log.error("create store error", e);
throw new TimiException(TimiCode.ERROR, "新增 GAO 门店失败", e); throw new TimiException(TimiCode.ERROR, "新增门店失败", e);
} finally { } finally {
loggerService.create(logger); loggerService.create(logger);
} }
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void update(GaoStore store) { public void update(GaoStore store) {
TimiException.required(store, "not found store"); TimiException.required(store, "not found store");
@@ -90,15 +93,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
try { try {
logger.setContent(jackson.writeValueAsString(store)); logger.setContent(jackson.writeValueAsString(store));
checkUnique(store); checkUnique(store);
GaoStore dbStore = get(store.getId()); super.update(store);
dbStore.setCode(store.getCode());
dbStore.setName(store.getName());
dbStore.setStatus(store.getStatus());
dbStore.setTelephone(store.getTelephone());
dbStore.setAddress(store.getAddress());
dbStore.setRemark(store.getRemark());
super.update(dbStore);
logger.setLevel(Logger.Level.INFO);
logger.setResult(store.getId()); logger.setResult(store.getId());
} catch (TimiException e) { } catch (TimiException e) {
logger.setLevel(Logger.Level.WARN); logger.setLevel(Logger.Level.WARN);
@@ -107,13 +102,14 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
} catch (Exception e) { } catch (Exception e) {
logger.setLevel(Logger.Level.ERROR); logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e)); logger.setException(TimiJava.serializeThrowable(e));
log.error("修改 GAO 门店失败", e); log.error("update store error", e);
throw new TimiException(TimiCode.ERROR, "修改 GAO 门店失败", e); throw new TimiException(TimiCode.ERROR, "修改门店失败", e);
} finally { } finally {
loggerService.create(logger); loggerService.create(logger);
} }
} }
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override @Override
public void delete(String id) { public void delete(String id) {
Logger logger = new Logger(Logger.Module.GAO, "GAO_STORE_DELETE"); Logger logger = new Logger(Logger.Module.GAO, "GAO_STORE_DELETE");
@@ -132,8 +128,8 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
} catch (Exception e) { } catch (Exception e) {
logger.setLevel(Logger.Level.ERROR); logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e)); logger.setException(TimiJava.serializeThrowable(e));
log.error("删除 GAO 门店失败", e); log.error("delete store error", e);
throw new TimiException(TimiCode.ERROR, "删除 GAO 门店失败", e); throw new TimiException(TimiCode.ERROR, "删除门店失败", e);
} finally { } finally {
loggerService.create(logger); loggerService.create(logger);
} }
@@ -150,6 +146,11 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
return getByUserId(userLoginService.getRequireLoginUserId()).getId(); return getByUserId(userLoginService.getRequireLoginUserId()).getId();
} }
@Override
public List<GaoStore> listByAutoClosedDay() {
return mapper.selectAllByAutoClosedDay();
}
@Override @Override
public byte[] export(Page<GaoStore> page) throws IOException { public byte[] export(Page<GaoStore> page) throws IOException {
page.setIndex(0); page.setIndex(0);
@@ -164,7 +165,9 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
if (TimiJava.isEmpty(idList)) { if (TimiJava.isEmpty(idList)) {
return; return;
} }
Logger logger = new Logger(Logger.Module.GAO, "GAO_STORE_SORT");
try {
logger.setContent(jackson.writeValueAsString(idList));
List<GaoStore> storeList = mapper.selectByIdList(new LinkedHashSet<>(idList)); List<GaoStore> storeList = mapper.selectByIdList(new LinkedHashSet<>(idList));
Map<String, GaoStore> storeMap = storeList.stream().collect(Collectors.toMap(GaoStore::getId, Function.identity())); Map<String, GaoStore> storeMap = storeList.stream().collect(Collectors.toMap(GaoStore::getId, Function.identity()));
for (int i = 0; i < idList.size(); i++) { for (int i = 0; i < idList.size(); i++) {
@@ -175,6 +178,19 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
store.setSort(i); store.setSort(i);
mapper.update(store); mapper.update(store);
} }
logger.setLevel(Logger.Level.INFO);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("删除 GAO 门店失败", e);
throw new TimiException(TimiCode.ERROR, "删除 GAO 门店失败", e);
} finally {
loggerService.create(logger);
}
} }
private void checkUnique(GaoStore store) { private void checkUnique(GaoStore store) {
@@ -0,0 +1,43 @@
package com.imyeyu.api.modules.gao.task;
import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
///
/// GAO 门店营业日自动休息任务
///
/// @author Codex
/// @since 2026-08-12
@Slf4j
@Service
@RequiredArgsConstructor
public class GaoStoreAutoCloseDayTask {
private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private final GaoStoreService storeService;
private final GaoStoreBusinessDayService businessDayService;
/// 每天零点后自动补记前一天无登记门店的休息日
@Scheduled(cron = "0 1 0 * * ?")
public void run() {
int dateValue = Integer.parseInt(DAY_FORMATTER.format(LocalDate.now().minusDays(1)));
List<GaoStore> storeList = storeService.listByAutoClosedDay();
for (GaoStore store : storeList) {
try {
businessDayService.autoCloseBusinessDay(store.getId(), dateValue);
} catch (Exception e) {
log.error("自动补记 GAO 门店休息日失败: storeId[{}], dateValue[{}]", store.getId(), dateValue, e);
}
}
}
}
@@ -35,7 +35,7 @@ public class StoreXSSFBuilder extends BaseXSSFBuilder<GaoStore> {
appendRow(sheet, 0, titleStyle, "门店列表导出"); appendRow(sheet, 0, titleStyle, "门店列表导出");
appendRow(sheet, 1, null, "生成时间", Time.nowString()); appendRow(sheet, 1, null, "生成时间", Time.nowString());
appendRow(sheet, 2, null, "记录总数", String.valueOf(list.size())); appendRow(sheet, 2, null, "记录总数", String.valueOf(list.size()));
appendRow(sheet, 3, headerStyle, "门店编码", "门店名称", "状态", "电话", "地址", "备注", "排序", "创建时间"); appendRow(sheet, 3, headerStyle, "门店编码", "门店名称", "状态", "电话", "地址", "备注", "自动计算休息日", "排序", "创建时间");
int rowIndex = 4; int rowIndex = 4;
for (GaoStore store : list) { for (GaoStore store : list) {
appendRow(sheet, rowIndex, null, appendRow(sheet, rowIndex, null,
@@ -45,12 +45,13 @@ public class StoreXSSFBuilder extends BaseXSSFBuilder<GaoStore> {
text(store.getTelephone()), text(store.getTelephone()),
text(store.getAddress()), text(store.getAddress()),
text(store.getRemark()), text(store.getRemark()),
Boolean.TRUE.equals(store.getAutoClosedDay()) ? "" : "",
store.getSort() == null ? "" : String.valueOf(store.getSort()), store.getSort() == null ? "" : String.valueOf(store.getSort()),
Time.toDateTime(store.getCreatedAt()) Time.toDateTime(store.getCreatedAt())
); );
rowIndex++; rowIndex++;
} }
for (int index = 0; index < 8; index++) { for (int index = 0; index < 9; index++) {
sheet.autoSizeColumn(index); sheet.autoSizeColumn(index);
sheet.setColumnWidth(index, Math.clamp(sheet.getColumnWidth(index), 10 * 256, 36 * 256)); sheet.setColumnWidth(index, Math.clamp(sheet.getColumnWidth(index), 10 * 256, 36 * 256));
} }
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.util.List;
/// 客户报表查询请求
///
/// @author Codex
/// @since 2026-08-11
@Data
public class GaoCustomerChartReq {
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 开始时间
private Long beginAt;
/// 结束时间
private Long endAt;
/// 登记事件 ID 列表
private List<String> eventIdList;
}
@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// 登记事件日历记录视图
///
/// @author Codex
/// @since 2026-08-11
@Data
public class GaoEventRecordCalendarView {
/// 登记记录 ID
private String id;
/// 客户 ID
private String customerId;
/// 客户编码
private String customerCode;
/// 客户姓名
private String customerName;
/// 登记事件 ID
private String eventId;
/// 登记时间
private Long registeredAt;
}
@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// 登记事件每日统计视图
///
/// @author Codex
/// @since 2026-08-11
@Data
public class GaoEventRecordDailyStatView {
/// 登记事件 ID
private String eventId;
/// 日期值,格式为 yyyyMMdd
private Integer dateValue;
/// 当日登记数
private Long dailyCount;
}
@@ -23,9 +23,24 @@ public class GaoEventRecordRankPage extends BasePage {
/// 登记事件 ID 列表,为空查询全部登记事件 /// 登记事件 ID 列表,为空查询全部登记事件
private List<String> eventIdList; private List<String> eventIdList;
/// 排行类型
private RankType rankType;
/// 开始登记时间 /// 开始登记时间
private Long beginAt; private Long beginAt;
/// 结束登记时间 /// 结束登记时间
private Long endAt; private Long endAt;
/// 查询当天日期值,服务端内部填充,格式为 yyyyMMdd
private Integer todayDateValue;
public enum RankType {
/// 按累计登记天数
TOTAL_DAYS,
/// 按累计登记次数
TOTAL_COUNT,
/// 按最近连续登记天数
CONTINUOUS_DAYS
}
} }
@@ -0,0 +1,26 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
/// GAO 客户积分人工调整请求
///
/// @author Codex
/// @since 2026-08-11
@Data
public class GaoPointAdjustRequest {
/// 人工调整请求 ID,用于生成幂等键
private String adjustRequestId;
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 调整积分,正数增加,负数减少
private Long pointDelta;
/// 调整原因
private String remark;
}
@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
import com.imyeyu.java.bean.BasePage;
import lombok.Data;
import lombok.EqualsAndHashCode;
/// GAO 客户积分流水查询请求
///
/// @author Codex
/// @since 2026-08-11
@Data
@EqualsAndHashCode(callSuper = true)
public class GaoPointLedgerPage extends BasePage {
/// 门店 ID
private String storeId;
/// 客户 ID
private String customerId;
/// 客户积分账户 ID
private String accountId;
/// 流水变化类型
private GaoPointLedger.ChangeType changeType;
/// 开始发生时间
private Long beginAt;
/// 结束发生时间
private Long endAt;
}
@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.gao.vo;
import lombok.Data;
import java.util.List;
/// 门店报表查询请求
///
/// @author Codex
/// @since 2026-08-11
@Data
public class GaoStoreChartReq {
/// 门店 ID
private String storeId;
/// 开始时间
private Long beginAt;
/// 结束时间
private Long endAt;
/// 登记事件 ID 列表
private List<String> eventIdList;
}
@@ -3,10 +3,14 @@ package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.mapper.LoggerMapper; import com.imyeyu.api.modules.system.mapper.LoggerMapper;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@@ -15,6 +19,7 @@ import org.springframework.stereotype.Service;
* @author 夜雨 * @author 夜雨
* @since 2026-05-13 14:46 * @since 2026-05-13 14:46
*/ */
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoggerServiceImplement extends AbstractEntityService<Logger, String> implements LoggerService { public class LoggerServiceImplement extends AbstractEntityService<Logger, String> implements LoggerService {
@@ -31,19 +36,13 @@ public class LoggerServiceImplement extends AbstractEntityService<Logger, String
@Override @Override
public void create(Logger logger) { public void create(Logger logger) {
// 默认异步 // 默认异步
fillContext(logger);
Thread.startVirtualThread(() -> createSync(logger)); Thread.startVirtualThread(() -> createSync(logger));
} }
@Override @Override
public void createSync(Logger logger) { public void createSync(Logger logger) {
// TODO 异步请求上下文已丢失 fillContext(logger);
// if (TimiJava.isEmpty(logger.getUserId())) {
// User loginUser = userLoginService.getLoginUser();
// if (TimiJava.isNotEmpty(loginUser)) {
// logger.setUserId(loginUser.getId());
// }
// }
// logger.setIp(TimiJava.defaultIfEmpty(logger.getIp(), TimiSpring.getRequestIP()));
super.create(logger); super.create(logger);
} }
@@ -53,4 +52,18 @@ public class LoggerServiceImplement extends AbstractEntityService<Logger, String
log.setResult(result); log.setResult(result);
create(log); create(log);
} }
private void fillContext(Logger logger) {
try {
if (TimiJava.isEmpty(logger.getUserId())) {
User loginUser = userLoginService.getLoginUser();
if (TimiJava.isNotEmpty(loginUser)) {
logger.setUserId(loginUser.getId());
}
}
logger.setIp(TimiJava.defaultIfEmpty(logger.getIp(), TimiSpring.getRequestIP()));
} catch (Exception e) {
// 无请求上下文的后台任务允许继续写日志
}
}
} }
@@ -0,0 +1,23 @@
package com.imyeyu.api.modules.user.bean;
/// 授权对象归属类型
///
/// @author Codex
/// @since 2026-08-07
public enum AuthOwnerType {
/// 系统级
SYSTEM,
/// 模块级
MODULE,
/// 租户级
TENANT,
/// 门店级
STORE,
/// 用户级
USER
}
@@ -33,4 +33,25 @@ public interface BuiltinPermissionCode {
/// ///
/// @return 是否授权给 ADMIN 角色 /// @return 是否授权给 ADMIN 角色
boolean isGrantToAdmin(); boolean isGrantToAdmin();
/// true 为受保护权限
///
/// @return 是否受保护
default boolean isProtected() {
return false;
}
/// 归属类型
///
/// @return 归属类型
default AuthOwnerType getOwnerType() {
return AuthOwnerType.MODULE;
}
/// 归属 ID
///
/// @return 归属 ID
default String getOwnerId() {
return getModuleCode().name();
}
} }
@@ -28,4 +28,18 @@ public interface BuiltinRoleCode {
/// ///
/// @return 父角色代码 /// @return 父角色代码
String getParentCode(); String getParentCode();
/// 归属类型
///
/// @return 归属类型
default AuthOwnerType getOwnerType() {
return AuthOwnerType.MODULE;
}
/// 归属 ID
///
/// @return 归属 ID
default String getOwnerId() {
return getModuleCode().name();
}
} }
@@ -1,12 +1,12 @@
package com.imyeyu.api.modules.user.controller; package com.imyeyu.api.modules.user.controller;
import com.imyeyu.api.annotation.RequireCorePermission; import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.annotation.RequireCoreRole;
import com.imyeyu.api.bean.CorePermissionCode; import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.service.AuthorizationScopeService;
import com.imyeyu.api.modules.user.service.PermissionService; import com.imyeyu.api.modules.user.service.PermissionService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.vo.permission.BatchCreateReq; import com.imyeyu.api.modules.user.vo.permission.BatchCreateReq;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
@@ -19,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/** /**
* 权限 SDK 控制器 * 权限 SDK 控制器
* *
@@ -32,16 +34,22 @@ import org.springframework.web.bind.annotation.RestController;
public class PermissionController { public class PermissionController {
private final PermissionService service; private final PermissionService service;
private final UserLoginService userLoginService;
private final AuthorizationScopeService authorizationScopeService;
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.PERMISSION_READ) @RequireCorePermission(CorePermissionCode.PERMISSION_READ)
@PostMapping("/list") @PostMapping("/list")
public PageResult<Permission> list(@RequestBody Page<Permission> page) { public PageResult<Permission> list(@RequestBody Page<Permission> page) {
return service.page(page); return service.page(page);
} }
@RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ)
@PostMapping("/grantable/list")
public List<Permission> listGrantable(@RequestParam ModuleCode moduleCode) {
return authorizationScopeService.listGrantablePermission(userLoginService.getRequireLoginUserId(), moduleCode);
}
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.PERMISSION_READ) @RequireCorePermission(CorePermissionCode.PERMISSION_READ)
@PostMapping("/create") @PostMapping("/create")
public void create(@RequestBody Permission permission) { public void create(@RequestBody Permission permission) {
@@ -49,7 +57,6 @@ public class PermissionController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.PERMISSION_CREATE) @RequireCorePermission(CorePermissionCode.PERMISSION_CREATE)
@PostMapping("/create/batch") @PostMapping("/create/batch")
public void createBatch(@RequestBody BatchCreateReq req) { public void createBatch(@RequestBody BatchCreateReq req) {
@@ -57,7 +64,6 @@ public class PermissionController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.PERMISSION_UPDATE) @RequireCorePermission(CorePermissionCode.PERMISSION_UPDATE)
@PostMapping("/update") @PostMapping("/update")
public void update(@RequestBody Permission permission) { public void update(@RequestBody Permission permission) {
@@ -65,7 +71,6 @@ public class PermissionController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.PERMISSION_DELETE) @RequireCorePermission(CorePermissionCode.PERMISSION_DELETE)
@PostMapping("/delete") @PostMapping("/delete")
public void delete(@RequestParam("id") String id) { public void delete(@RequestParam("id") String id) {
@@ -1,18 +1,19 @@
package com.imyeyu.api.modules.user.controller; package com.imyeyu.api.modules.user.controller;
import com.imyeyu.api.annotation.RequireCorePermission; import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.annotation.RequireCoreRole;
import com.imyeyu.api.bean.CorePermissionCode; import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.bean.RoleMatchMode;
import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.service.PermissionService; import com.imyeyu.api.modules.user.service.PermissionService;
import com.imyeyu.api.modules.user.service.RoleService; 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.UserRoleService;
import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeRequest; import com.imyeyu.api.modules.user.vo.role.UserRoleAuthorizeRequest;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -23,7 +24,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/** /**
* 角色 SDK 控制器 * 角色 SDK 控制器
@@ -39,30 +43,52 @@ public class RoleController {
private final RoleService service; private final RoleService service;
private final UserRoleService userRoleService; private final UserRoleService userRoleService;
private final UserLoginService userLoginService;
private final PermissionService permissionService; private final PermissionService permissionService;
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.ROLE_READ) @RequireCorePermission(CorePermissionCode.ROLE_READ)
@PostMapping("/list") @PostMapping("/list")
public PageResult<Role> list(@RequestBody Page<Role> page) { public PageResult<Role> list(@RequestBody Page<Role> page) {
Set<String> manageableRoleIdSet = listManageableRoleId();
PageResult<Role> result = service.page(page); PageResult<Role> result = service.page(page);
if (result.getList() != null) { if (result.getList() != null) {
result.setList(result.getList().stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList());
result.getList().forEach(this::fillRoleDetail); result.getList().forEach(this::fillRoleDetail);
} }
return result; return result;
} }
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY) @RequireCorePermission(CorePermissionCode.ROLE_READ)
@PostMapping("/manageable/list")
public List<Role> listManageableRole(@RequestParam ModuleCode moduleCode) {
return service.listManageableRole(moduleCode);
}
@RequireCorePermission(CorePermissionCode.USER_ROLE_READ)
@PostMapping("/grantable/list")
public List<Role> listGrantableRole(@RequestParam ModuleCode moduleCode) {
return service.listGrantableRole(moduleCode);
}
@RequiredToken
@PostMapping("/current/detail/list")
public List<Role> listCurrentRoleDetail(@RequestParam(required = false) ModuleCode moduleCode) {
String loginUserId = userLoginService.getRequireLoginUserId();
List<Role> roleList = moduleCode == null ? userRoleService.listRoleByUserId(loginUserId) : userRoleService.listRoleByUserId(moduleCode, loginUserId);
roleList.forEach(this::fillRoleDetail);
return roleList;
}
@RequireCorePermission(CorePermissionCode.ROLE_READ) @RequireCorePermission(CorePermissionCode.ROLE_READ)
@PostMapping("/detail") @PostMapping("/detail")
public Role detail(@RequestParam String id) { public Role detail(@RequestParam String id) {
Role role = service.get(id); Role role = service.get(id);
requireManageableRole(role.getId());
fillRoleDetail(role); fillRoleDetail(role);
return role; return role;
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.ROLE_CREATE) @RequireCorePermission(CorePermissionCode.ROLE_CREATE)
@PostMapping("/create") @PostMapping("/create")
public void create(@RequestBody Role role) { public void create(@RequestBody Role role) {
@@ -70,49 +96,110 @@ public class RoleController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.ROLE_UPDATE) @RequireCorePermission(CorePermissionCode.ROLE_UPDATE)
@PostMapping("/update") @PostMapping("/update")
public void update(@RequestBody Role role) { public void update(@RequestBody Role role) {
service.update(role); service.update(role);
} }
@RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ)
@PostMapping("/permission/grantable/list")
public List<Permission> listGrantableRolePermission(@RequestParam String roleId) {
requireManageableRole(roleId);
return service.listGrantableRolePermission(roleId);
}
@RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ)
@PostMapping("/delegation/list")
public List<Permission> listDelegationPermission(@RequestParam String roleId) {
requireManageableRole(roleId);
return service.listDelegationPermissionByRoleId(roleId);
}
@RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ)
@PostMapping("/delegation/grantable/list")
public List<Permission> listGrantableDelegationPermission(@RequestParam String roleId) {
requireManageableRole(roleId);
return service.listGrantableDelegationPermission(roleId);
}
@AOPLog
@RequireCorePermission(value = {CorePermissionCode.ROLE_PERMISSION_CREATE, CorePermissionCode.ROLE_PERMISSION_DELETE})
@PostMapping("/delegation/update")
public void updateRoleDelegation(@RequestBody Role role) {
service.updateRoleDelegation(role);
}
@RequireCorePermission(CorePermissionCode.ROLE_READ)
@PostMapping("/parent/grantable/list")
public List<Role> listGrantableParentRole(@RequestParam String roleId) {
requireManageableRole(roleId);
return service.listGrantableParentRole(roleId);
}
@AOPLog
@RequireCorePermission(CorePermissionCode.ROLE_UPDATE)
@PostMapping("/parent/update")
public void updateRoleParent(@RequestBody Role role) {
service.updateRoleParent(role);
}
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.ROLE_DELETE) @RequireCorePermission(CorePermissionCode.ROLE_DELETE)
@PostMapping("/delete") @PostMapping("/delete")
public void delete(@RequestParam String id) { public void delete(@RequestParam String id) {
service.delete(id); service.delete(id);
} }
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.USER_ROLE_READ) @RequireCorePermission(CorePermissionCode.USER_ROLE_READ)
@PostMapping("/authorized/list") @PostMapping("/authorized/list")
public List<Role> listUserRole(@RequestParam(required = false) ModuleCode moduleCode, @RequestParam String userId) { public List<Role> listUserRole(@RequestParam(required = false) ModuleCode moduleCode, @RequestParam String userId) {
Set<String> manageableRoleIdSet = listManageableRoleId();
if (moduleCode != null) { if (moduleCode != null) {
return userRoleService.listRoleByUserId(moduleCode, userId); return userRoleService.listRoleByUserId(moduleCode, userId).stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList();
} }
return userRoleService.listRoleByUserId(userId); return userRoleService.listRoleByUserId(userId).stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList();
} }
@AOPLog @AOPLog
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(value = {CorePermissionCode.USER_ROLE_CREATE, CorePermissionCode.USER_ROLE_DELETE}) @RequireCorePermission(value = {CorePermissionCode.USER_ROLE_CREATE, CorePermissionCode.USER_ROLE_DELETE})
@PostMapping("/authorized/create") @PostMapping("/authorized/create")
public void authorizeUserRole(@RequestBody UserRoleAuthorizeRequest req) { public void authorizeUserRole(@RequestBody UserRoleAuthorizeRequest req) {
userRoleService.authorizeUserRole(req); userRoleService.authorizeUserRole(req);
} }
@RequireCoreRole(value = {CoreRoleCode.ADMIN, CoreRoleCode.USER_ROLE_ADMIN}, mode = RoleMatchMode.ANY)
@RequireCorePermission(CorePermissionCode.USER_ROLE_READ) @RequireCorePermission(CorePermissionCode.USER_ROLE_READ)
@PostMapping("/authorized/permission") @PostMapping("/authorized/permission")
public List<Permission> listAuthorizedPermission(@RequestParam String userId) { public List<Permission> listAuthorizedPermission(@RequestParam String userId) {
return userRoleService.listAllPermissionByUserId(userId); Set<String> manageableRoleIdSet = listManageableRoleId();
List<String> roleIdList = userRoleService.listRoleByUserId(userId)
.stream()
.filter(role -> manageableRoleIdSet.contains(role.getId()))
.map(Role::getId)
.toList();
return roleIdList.isEmpty() ? List.of() : permissionService.listByRoleId(service.listAllRoleIdIncludingChildren(roleIdList));
} }
private void fillRoleDetail(Role role) { private void fillRoleDetail(Role role) {
role.setPermissionList(permissionService.listByRoleId(role.getId())); role.setPermissionList(permissionService.listByRoleId(role.getId()));
role.setAllPermissionList(service.listPermissionByRoleId(role.getId())); role.setAllPermissionList(service.listPermissionByRoleId(role.getId()));
role.setChildRoleList(service.listChildByParentRoleId(role.getId())); role.setChildRoleList(service.listChildByParentRoleId(role.getId()));
if (role.getParentRoleId() != null) {
role.setParentRole(service.get(role.getParentRoleId()));
}
role.setDelegationPermissionList(service.listDelegationPermissionByRoleId(role.getId()));
}
private Set<String> listManageableRoleId() {
return Arrays.stream(ModuleCode.values())
.flatMap(moduleCode -> service.listManageableRole(moduleCode).stream())
.map(Role::getId)
.collect(Collectors.toSet());
}
private void requireManageableRole(String roleId) {
if (!listManageableRoleId().contains(roleId)) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
} }
} }
@@ -2,9 +2,7 @@ package com.imyeyu.api.modules.user.controller;
import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission; import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.annotation.RequireCoreRole;
import com.imyeyu.api.bean.CorePermissionCode; import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.User;
@@ -56,7 +54,6 @@ public class UserController {
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequestRateLimit @RequestRateLimit
@RequireCorePermission(CorePermissionCode.USER_READ) @RequireCorePermission(CorePermissionCode.USER_READ)
@PostMapping("/list") @PostMapping("/list")
@@ -65,7 +62,6 @@ public class UserController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequestRateLimit @RequestRateLimit
@RequireCorePermission(CorePermissionCode.USER_READ) @RequireCorePermission(CorePermissionCode.USER_READ)
@RequestMapping("/detail") @RequestMapping("/detail")
@@ -74,7 +70,6 @@ public class UserController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequestRateLimit @RequestRateLimit
@RequireCorePermission(CorePermissionCode.USER_CREATE) @RequireCorePermission(CorePermissionCode.USER_CREATE)
@PostMapping("/create") @PostMapping("/create")
@@ -83,7 +78,6 @@ public class UserController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequestRateLimit @RequestRateLimit
@RequireCorePermission(CorePermissionCode.USER_UPDATE) @RequireCorePermission(CorePermissionCode.USER_UPDATE)
@PostMapping("/update") @PostMapping("/update")
@@ -92,7 +86,6 @@ public class UserController {
} }
@AOPLog @AOPLog
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequestRateLimit @RequestRateLimit
@RequestMapping("/delete") @RequestMapping("/delete")
public void delete(@RequestParam String id) { public void delete(@RequestParam String id) {
@@ -125,6 +118,7 @@ public class UserController {
@AOPLog @AOPLog
@JsonView(ResponseView.Public.class) @JsonView(ResponseView.Public.class)
@RequestRateLimit @RequestRateLimit
@CaptchaValid
@PostMapping("/login") @PostMapping("/login")
public LoginResponse login(@RequestBody @Valid CaptchaData<LoginRequest> req) { public LoginResponse login(@RequestBody @Valid CaptchaData<LoginRequest> req) {
return userLoginService.login(req.getData()); return userLoginService.login(req.getData());
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.user.entity;
import com.imyeyu.api.annotation.MultilingualField; import com.imyeyu.api.annotation.MultilingualField;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.bean.AuthOwnerType;
import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
@@ -29,6 +30,21 @@ public class Permission extends UUIDEntity {
/** 说明 */ /** 说明 */
private String description; private String description;
/// true 为系统内置权限
private Boolean builtin;
/// true 为受保护权限
private Boolean protectedPermission;
/// 归属类型
private AuthOwnerType ownerType;
/// 归属 ID
private String ownerId;
/// 创建人用户 ID
private String createdBy;
@Transient @Transient
@MultilingualField("nameLangId") @MultilingualField("nameLangId")
private String name; private String name;
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.user.entity;
import com.imyeyu.api.annotation.MultilingualField; import com.imyeyu.api.annotation.MultilingualField;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.bean.AuthOwnerType;
import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity; import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
@@ -32,6 +33,24 @@ public class Role extends UUIDEntity {
/** 说明 */ /** 说明 */
private String description; private String description;
/// true 为系统内置角色
private Boolean builtin;
/// true 为受保护角色
private Boolean protectedRole;
/// 管理父角色 ID
private String parentRoleId;
/// 归属类型
private AuthOwnerType ownerType;
/// 归属 ID
private String ownerId;
/// 创建人用户 ID
private String createdBy;
@Transient @Transient
@MultilingualField("nameLangId") @MultilingualField("nameLangId")
private String name; private String name;
@@ -48,6 +67,14 @@ public class Role extends UUIDEntity {
@Transient @Transient
protected List<Role> childRoleList; protected List<Role> childRoleList;
/// 管理父角色
@Transient
protected Role parentRole;
/// 角色允许继续转授的权限列表
@Transient
protected List<Permission> delegationPermissionList;
/** 权限 ID 列表 */ /** 权限 ID 列表 */
@Transient @Transient
protected Set<String> permissionIdList; protected Set<String> permissionIdList;
@@ -55,4 +82,8 @@ public class Role extends UUIDEntity {
/** 子角色 ID 列表 */ /** 子角色 ID 列表 */
@Transient @Transient
protected Set<String> childRoleIdList; protected Set<String> childRoleIdList;
/// 转授权限 ID 列表
@Transient
protected Set<String> delegationPermissionIdList;
} }
@@ -0,0 +1,45 @@
package com.imyeyu.api.modules.user.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.DeleteColumn;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.IDEntity;
import lombok.Data;
/// 角色转授权限
///
/// @author Codex
/// @since 2026-08-08
@Data
public class RolePermissionDelegation implements IDEntity<String>, Creatable, Deletable {
/// 主键
@Id
@AutoUUID
protected String id;
/// 角色 ID
protected String roleId;
/// 权限 ID
protected String permissionId;
/// 创建人用户 ID
protected String createdBy;
/// 创建时间
protected Long createdAt;
/// 删除时间
@DeleteColumn
protected Long deletedAt;
@Transient
protected Role role;
@Transient
protected Permission permission;
}
@@ -1,5 +1,6 @@
package com.imyeyu.api.modules.user.entity; package com.imyeyu.api.modules.user.entity;
import com.imyeyu.api.modules.user.bean.AuthOwnerType;
import com.imyeyu.spring.annotation.table.AutoUUID; import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.DeleteColumn; import com.imyeyu.spring.annotation.table.DeleteColumn;
import com.imyeyu.spring.annotation.table.Id; import com.imyeyu.spring.annotation.table.Id;
@@ -8,30 +9,37 @@ import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.IDEntity; import com.imyeyu.spring.entity.IDEntity;
import lombok.Data; import lombok.Data;
/** /// 角色权限继承
* 角色关联 ///
* /// @author 夜雨
* @author 夜雨 /// @since 2026-05-13 14:40
* @since 2026-05-13 14:40
*/
@Data @Data
public class RoleRelation implements IDEntity<String>, Creatable, Deletable { public class RolePermissionInherit implements IDEntity<String>, Creatable, Deletable {
/** 主键 */ /// 主键
@Id @Id
@AutoUUID @AutoUUID
private String id; private String id;
/** 上级角色 ID */ /// 继承方角色 ID
private String parentRoleId; private String parentRoleId;
/** 子级角色 ID */ /// 被继承角色 ID
private String childRoleId; private String childRoleId;
/** 创建时间 */ /// 归属类型
private AuthOwnerType ownerType;
/// 归属 ID
private String ownerId;
/// 创建人用户 ID
private String createdBy;
/// 创建时间
private Long createdAt; private Long createdAt;
/** 删除时间 */ /// 删除时间
@DeleteColumn @DeleteColumn
private Long deletedAt; private Long deletedAt;
} }
@@ -2,6 +2,8 @@ package com.imyeyu.api.modules.user.mapper;
import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.api.bean.ModuleCode;
import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
@@ -13,5 +15,7 @@ import java.util.List;
*/ */
public interface PermissionMapper extends BaseMapper<Permission, String> { public interface PermissionMapper extends BaseMapper<Permission, String> {
List<Permission> selectByIdList(List<String> list); List<Permission> selectByIdList(@Param("idList") List<String> idList);
List<Permission> selectByModuleCode(@Param("moduleCode") ModuleCode moduleCode);
} }
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.user.mapper;
import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -15,7 +16,13 @@ import java.util.List;
*/ */
public interface RoleMapper extends BaseMapper<Role, String> { public interface RoleMapper extends BaseMapper<Role, String> {
List<Role> selectByIdList(Collection<String> idList); Role selectById(String id);
List<Role> selectByModuleCode(ModuleCode moduleCode); List<Role> selectByIdList(@Param("idList") Collection<String> idList);
List<Role> selectByModuleCode(@Param("moduleCode") ModuleCode moduleCode);
List<Role> selectByParentRoleId(@Param("parentRoleId") String parentRoleId);
List<Role> selectAll();
} }
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.user.mapper;
import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.entity.RolePermissionDelegation;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// 角色转授权限 Mapper
///
/// @author Codex
/// @since 2026-08-08
public interface RolePermissionDelegationMapper extends BaseMapper<RolePermissionDelegation, String> {
default List<RolePermissionDelegation> selectByRoleId(String roleId) {
return selectByRoleIdList(List.of(roleId));
}
List<RolePermissionDelegation> selectByRoleIdList(@Param("roleIdList") List<String> roleIdList);
List<Permission> selectPermissionByRoleIdList(@Param("roleIdList") List<String> roleIdList);
void createBatch(@Param("rolePermissionDelegationList") List<RolePermissionDelegation> rolePermissionDelegationList);
void deleteByPermissionIdList(@Param("roleId") String roleId, @Param("permissionIdList") List<String> permissionIdList);
}
@@ -0,0 +1,32 @@
package com.imyeyu.api.modules.user.mapper;
import com.imyeyu.api.modules.user.bean.AuthOwnerType;
import com.imyeyu.api.modules.user.entity.RolePermissionInherit;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/// 角色权限继承 Mapper
///
/// @author 夜雨
/// @since 2026-05-13 14:46
public interface RolePermissionInheritMapper extends BaseMapper<RolePermissionInherit, String> {
List<RolePermissionInherit> selectByParentRoleId(@Param("parentRoleId") String parentRoleId);
List<String> selectChildRoleIdListByParentRoleId(@Param("parentRoleId") String parentRoleId);
List<RolePermissionInherit> selectByParentRoleIdList(@Param("parentRoleIdList") List<String> parentRoleIdList);
List<String> selectChildRoleIdListByParentRoleIdList(@Param("parentRoleIdList") List<String> parentRoleIdList);
void createBatch(@Param("rolePermissionInheritList") List<RolePermissionInherit> rolePermissionInheritList);
void deleteByChildRoleIdList(
@Param("parentRoleId") String parentRoleId,
@Param("childRoleIdList") List<String> childRoleIdList,
@Param("ownerType") AuthOwnerType ownerType,
@Param("ownerId") String ownerId
);
}
@@ -3,6 +3,7 @@ package com.imyeyu.api.modules.user.mapper;
import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.entity.RolePermission; import com.imyeyu.api.modules.user.entity.RolePermission;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
@@ -18,11 +19,11 @@ public interface RolePermissionMapper extends BaseMapper<RolePermission, String>
return selectByRoleIdList(List.of(roleId)); return selectByRoleIdList(List.of(roleId));
} }
List<RolePermission> selectByRoleIdList(List<String> roleIdList); List<RolePermission> selectByRoleIdList(@Param("roleIdList") List<String> roleIdList);
List<Permission> selectPermissionByRoleIdList(List<String> roleIdList); List<Permission> selectPermissionByRoleIdList(@Param("roleIdList") List<String> roleIdList);
void createBatch(List<RolePermission> rolePermissionList); void createBatch(@Param("rolePermissionList") List<RolePermission> rolePermissionList);
void deleteByPermissionIdList(String roleId, List<String> permissionIdList); void deleteByPermissionIdList(@Param("roleId") String roleId, @Param("permissionIdList") List<String> permissionIdList);
} }
@@ -1,25 +0,0 @@
package com.imyeyu.api.modules.user.mapper;
import com.imyeyu.api.modules.user.entity.RoleRelation;
import com.imyeyu.spring.mapper.BaseMapper;
import java.util.List;
/**
* 角色关联 Mapper
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface RoleRelationMapper extends BaseMapper<RoleRelation, String> {
List<RoleRelation> selectByParentRoleId(String parentRoleId);
List<String> selectChildRoleIdListByParentRoleId(String parentRoleId);
List<String> selectChildRoleIdListByParentRoleIdList(List<String> parentRoleIdList);
void createBatch(List<RoleRelation> roleRelationList);
void deleteByChildRoleIdList(String parentRoleId, List<String> childRoleIdList);
}
@@ -19,5 +19,5 @@ public interface UserRoleMapper extends BaseMapper<UserRole, String> {
List<UserRole> selectByUserIdList(@Param("userIdList") Collection<String> userIdList); List<UserRole> selectByUserIdList(@Param("userIdList") Collection<String> userIdList);
void deleteByRoleIdList(String userId, List<String> roleIdList); void deleteByRoleIdList(@Param("userId") String userId, @Param("roleIdList") List<String> roleIdList);
} }
@@ -0,0 +1,79 @@
package com.imyeyu.api.modules.user.service;
import com.imyeyu.api.modules.user.entity.Role;
import java.util.Collection;
/// 授权边界服务
///
/// @author Codex
/// @since 2026-08-07
public interface AuthorizationBoundaryService {
/// 校验是否可以授予用户角色
///
/// @param operatorUserId 操作者用户 ID
/// @param targetUserId 目标用户 ID
/// @param roleIdList 角色 ID 列表
void checkCanGrantRoles(String operatorUserId, String targetUserId, Collection<String> roleIdList);
/// 校验是否可以移除用户角色
///
/// @param operatorUserId 操作者用户 ID
/// @param targetUserId 目标用户 ID
/// @param roleIdList 角色 ID 列表
void checkCanRevokeRoles(String operatorUserId, String targetUserId, Collection<String> roleIdList);
/// 校验是否可以创建角色
///
/// @param operatorUserId 操作者用户 ID
/// @param role 角色
void checkCanCreateRole(String operatorUserId, Role role);
/// 校验是否可以修改角色基础信息
///
/// @param operatorUserId 操作者用户 ID
/// @param role 角色
void checkCanUpdateRole(String operatorUserId, Role role);
/// 校验是否可以删除角色
///
/// @param operatorUserId 操作者用户 ID
/// @param roleId 角色 ID
void checkCanDeleteRole(String operatorUserId, String roleId);
/// 校验是否可以授予角色权限
///
/// @param operatorUserId 操作者用户 ID
/// @param roleId 角色 ID
/// @param permissionIdList 权限 ID 列表
void checkCanGrantPermissions(String operatorUserId, String roleId, Collection<String> permissionIdList);
/// 校验是否可以移除角色权限
///
/// @param operatorUserId 操作者用户 ID
/// @param roleId 角色 ID
/// @param permissionIdList 权限 ID 列表
void checkCanRevokePermissions(String operatorUserId, String roleId, Collection<String> permissionIdList);
/// 校验是否可以修改角色转授权限
///
/// @param operatorUserId 操作者用户 ID
/// @param roleId 角色 ID
/// @param permissionIdList 转授权限 ID 列表
void checkCanUpdateRoleDelegation(String operatorUserId, String roleId, Collection<String> permissionIdList);
/// 校验是否可以更新角色管理父级
///
/// @param operatorUserId 操作者用户 ID
/// @param roleId 角色 ID
/// @param parentRoleId 管理父角色 ID
void checkCanUpdateRoleParent(String operatorUserId, String roleId, String parentRoleId);
/// 校验是否可以更新角色继承关系
///
/// @param operatorUserId 操作者用户 ID
/// @param parentRoleId 父角色 ID
/// @param childRoleIdList 子角色 ID 列表
void checkCanUpdateRolePermissionInherits(String operatorUserId, String parentRoleId, Collection<String> childRoleIdList);
}
@@ -0,0 +1,42 @@
package com.imyeyu.api.modules.user.service;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.entity.Role;
import java.util.List;
/// 授权范围服务
///
/// @author Codex
/// @since 2026-08-07
public interface AuthorizationScopeService {
/// 查询可授权角色
///
/// @param operatorUserId 操作者用户 ID
/// @param moduleCode 模块代码
/// @return 角色列表
List<Role> listGrantableRole(String operatorUserId, ModuleCode moduleCode);
/// 查询可授权权限
///
/// @param operatorUserId 操作者用户 ID
/// @param moduleCode 模块代码
/// @return 权限列表
List<Permission> listGrantablePermission(String operatorUserId, ModuleCode moduleCode);
/// 查询可管理角色
///
/// @param operatorUserId 操作者用户 ID
/// @param moduleCode 模块代码
/// @return 角色列表
List<Role> listManageableRole(String operatorUserId, ModuleCode moduleCode);
/// 判断是否为系统管理员
///
/// @param operatorUserId 操作者用户 ID
/// @return true 为系统管理员
boolean isSystemAdmin(String operatorUserId);
}
@@ -0,0 +1,39 @@
package com.imyeyu.api.modules.user.service;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.bean.AuthOwnerType;
import com.imyeyu.api.modules.user.entity.Role;
/// 模块资源范围解析器
///
/// @author Codex
/// @since 2026-08-07
public interface ModuleResourceScopeResolver {
/// 模块代码
///
/// @return 模块代码
ModuleCode moduleCode();
/// 判断是否可以管理目标用户
///
/// @param operatorUserId 操作者用户 ID
/// @param targetUserId 目标用户 ID
/// @return true 为可以管理
boolean canManageUser(String operatorUserId, String targetUserId);
/// 判断是否可以管理角色
///
/// @param operatorUserId 操作者用户 ID
/// @param role 角色
/// @return true 为可以管理
boolean canManageRole(String operatorUserId, Role role);
/// 判断是否可以使用指定归属范围
///
/// @param operatorUserId 操作者用户 ID
/// @param ownerType 归属类型
/// @param ownerId 归属 ID
/// @return true 为可以使用
boolean canUseOwnerScope(String operatorUserId, AuthOwnerType ownerType, String ownerId);
}
@@ -2,6 +2,7 @@ package com.imyeyu.api.modules.user.service;
import com.imyeyu.api.modules.user.entity.Permission; import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.vo.permission.BatchCreateReq; import com.imyeyu.api.modules.user.vo.permission.BatchCreateReq;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
import java.util.List; import java.util.List;
@@ -30,4 +31,10 @@ public interface PermissionService extends BaseService<Permission, String> {
/// @param roleIdList 角色 ID 列表 /// @param roleIdList 角色 ID 列表
/// @return 权限列表 /// @return 权限列表
List<Permission> listByRoleId(List<String> roleIdList); List<Permission> listByRoleId(List<String> roleIdList);
/// 查询模块权限
///
/// @param moduleCode 模块代码
/// @return 权限列表
List<Permission> listByModuleCode(ModuleCode moduleCode);
} }
@@ -0,0 +1,39 @@
package com.imyeyu.api.modules.user.service;
import java.util.Collection;
import java.util.List;
/// 角色继承服务
///
/// @author Codex
/// @since 2026-08-07
public interface RoleHierarchyService {
/// 递归查询角色及其系统、模块作用域内的所有子角色 ID
///
/// 此方法不依赖用户资源范围,不会展开门店、用户等需要操作者上下文的作用域关系
///
/// @param roleIdList 角色 ID 列表
/// @return 角色 ID 列表
List<String> listAllRoleIdIncludingChildren(Collection<String> roleIdList);
/// 递归查询用户作用域内的角色及其所有子角色 ID
///
/// @param userId 用户 ID
/// @param roleIdList 角色 ID 列表
/// @return 角色 ID 列表
List<String> listAllRoleIdIncludingChildren(String userId, Collection<String> roleIdList);
/// 校验角色继承关系不存在环
///
/// @param parentRoleId 父角色 ID
/// @param childRoleIdList 子角色 ID 列表
void checkNoCycle(String parentRoleId, Collection<String> childRoleIdList);
/// 判断目标角色是否为子孙角色
///
/// @param parentRoleId 父角色 ID
/// @param childRoleId 子角色 ID
/// @return true 为子孙角色
boolean isDescendant(String parentRoleId, String childRoleId);
}
@@ -0,0 +1,31 @@
package com.imyeyu.api.modules.user.service;
import com.imyeyu.api.modules.user.entity.RolePermissionInherit;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// 角色权限继承服务
///
/// @author 夜雨
/// @since 2026-05-13 14:46
public interface RolePermissionInheritService extends BaseService<RolePermissionInherit, String> {
/// 查询直属权限继承
///
/// @param parentRoleId 继承方角色 ID
/// @return 权限继承列表
List<RolePermissionInherit> listByParentRoleId(String parentRoleId);
/// 查询直属被继承角色 ID 列表
///
/// @param parentRoleId 继承方角色 ID
/// @return 被继承角色 ID 列表
List<String> listChildRoleIdByParentRoleId(String parentRoleId);
/// 递归展开全部被继承角色 ID
///
/// @param parentRoleIdList 继承方角色 ID 列表
/// @return 全部被继承角色 ID 列表
List<String> listAllChildRoleIdByParentRoleIdList(List<String> parentRoleIdList);
}
@@ -1,31 +0,0 @@
package com.imyeyu.api.modules.user.service;
import com.imyeyu.api.modules.user.entity.RoleRelation;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/// 角色继承关系服务
///
/// @author 夜雨
/// @since 2026-05-13 14:46
public interface RoleRelationService extends BaseService<RoleRelation, String> {
/// 查询直属子角色关联
///
/// @param parentRoleId 父角色 ID
/// @return 角色关联列表
List<RoleRelation> listByParentRoleId(String parentRoleId);
/// 查询直属子角色 ID 列表
///
/// @param parentRoleId 父角色 ID
/// @return 子角色 ID 列表
List<String> listChildRoleIdByParentRoleId(String parentRoleId);
/// 递归展开全部子角色 ID
///
/// @param parentRoleIdList 父角色 ID 列表
/// @return 全部子角色 ID 列表
List<String> listAllChildRoleIdByParentRoleIdList(List<String> parentRoleIdList);
}
@@ -22,6 +22,18 @@ public interface RoleService extends BaseService<Role, String> {
List<Role> listByIdList(Collection<String> idList); List<Role> listByIdList(Collection<String> idList);
/// 查询当前用户可管理的模块角色
///
/// @param moduleCode 模块代码
/// @return 角色列表
List<Role> listManageableRole(ModuleCode moduleCode);
/// 查询当前用户可授权给账号的模块角色
///
/// @param moduleCode 模块代码
/// @return 角色列表
List<Role> listGrantableRole(ModuleCode moduleCode);
/// 查询直属子角色 /// 查询直属子角色
/// ///
/// @param parentRoleId 父角色 ID /// @param parentRoleId 父角色 ID
@@ -34,6 +46,40 @@ public interface RoleService extends BaseService<Role, String> {
/// @return 权限列表 /// @return 权限列表
List<Permission> listPermissionByRoleId(String roleId); List<Permission> listPermissionByRoleId(String roleId);
/// 查询可配置给目标角色的直接权限
///
/// @param roleId 角色 ID
/// @return 权限列表
List<Permission> listGrantableRolePermission(String roleId);
/// 查询角色转授权限
///
/// @param roleId 角色 ID
/// @return 权限列表
List<Permission> listDelegationPermissionByRoleId(String roleId);
/// 查询可配置给目标角色的转授权限
///
/// @param roleId 角色 ID
/// @return 权限列表
List<Permission> listGrantableDelegationPermission(String roleId);
/// 修改角色转授权限
///
/// @param role 角色
void updateRoleDelegation(Role role);
/// 查询可配置给目标角色的管理父级角色
///
/// @param roleId 角色 ID
/// @return 角色列表
List<Role> listGrantableParentRole(String roleId);
/// 修改角色管理父级
///
/// @param role 角色
void updateRoleParent(Role role);
/// 递归获取角色及其所有子角色的 ID 列表 /// 递归获取角色及其所有子角色的 ID 列表
/// ///
/// @param idList 角色 ID 列表 /// @param idList 角色 ID 列表
@@ -0,0 +1,376 @@
package com.imyeyu.api.modules.user.service.implement;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.bean.ModuleCode;
import com.imyeyu.api.modules.user.bean.AuthOwnerType;
import com.imyeyu.api.modules.user.entity.Permission;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.entity.UserRole;
import com.imyeyu.api.modules.user.mapper.PermissionMapper;
import com.imyeyu.api.modules.user.mapper.RoleMapper;
import com.imyeyu.api.modules.user.mapper.RolePermissionDelegationMapper;
import com.imyeyu.api.modules.user.mapper.RolePermissionMapper;
import com.imyeyu.api.modules.user.mapper.UserRoleMapper;
import com.imyeyu.api.modules.user.service.AuthorizationBoundaryService;
import com.imyeyu.api.modules.user.service.AuthorizationScopeService;
import com.imyeyu.api.modules.user.service.ModuleResourceScopeResolver;
import com.imyeyu.api.modules.user.service.RoleHierarchyService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/// 授权边界服务实现
///
/// @author Codex
/// @since 2026-08-07
@Service
@RequiredArgsConstructor
public class AuthorizationBoundaryServiceImplement implements AuthorizationBoundaryService {
private final RoleMapper roleMapper;
private final UserRoleMapper userRoleMapper;
private final PermissionMapper permissionMapper;
private final RolePermissionMapper rolePermissionMapper;
private final RolePermissionDelegationMapper rolePermissionDelegationMapper;
private final AuthorizationScopeService authorizationScopeService;
private final RoleHierarchyService roleHierarchyService;
private final List<ModuleResourceScopeResolver> moduleResourceScopeResolverList;
@Override
public void checkCanGrantRoles(String operatorUserId, String targetUserId, Collection<String> roleIdList) {
if (TimiJava.isEmpty(roleIdList)) {
return;
}
checkCorePermission(operatorUserId, CorePermissionCode.USER_ROLE_CREATE);
if (authorizationScopeService.isSystemAdmin(operatorUserId)) {
return;
}
List<Role> roleList = roleMapper.selectByIdList(roleIdList);
checkRoleIdListComplete(roleIdList, roleList);
for (Role role : roleList) {
checkCanManageUserInModule(operatorUserId, targetUserId, role.getModuleCode());
checkGrantableRole(operatorUserId, role);
}
}
@Override
public void checkCanRevokeRoles(String operatorUserId, String targetUserId, Collection<String> roleIdList) {
if (TimiJava.isEmpty(roleIdList)) {
return;
}
checkCorePermission(operatorUserId, CorePermissionCode.USER_ROLE_DELETE);
if (authorizationScopeService.isSystemAdmin(operatorUserId)) {
return;
}
List<Role> roleList = roleMapper.selectByIdList(roleIdList);
checkRoleIdListComplete(roleIdList, roleList);
for (Role role : roleList) {
checkCanManageUserInModule(operatorUserId, targetUserId, role.getModuleCode());
checkManageableRole(operatorUserId, role);
}
}
@Override
public void checkCanCreateRole(String operatorUserId, Role role) {
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_CREATE);
if (authorizationScopeService.isSystemAdmin(operatorUserId)) {
return;
}
TimiException.required(role.getModuleCode(), "not found role.moduleCode");
TimiException.required(role.getOwnerType(), "not found role.ownerType");
TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能创建内置角色");
TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能创建受保护角色");
checkOwnerScope(operatorUserId, role.getModuleCode(), role.getOwnerType(), role.getOwnerId());
}
@Override
public void checkCanUpdateRole(String operatorUserId, Role role) {
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_UPDATE);
Role dbRole = roleMapper.selectById(role.getId());
TimiException.required(dbRole, "not found role");
if (authorizationScopeService.isSystemAdmin(operatorUserId)) {
return;
}
checkManageableRole(operatorUserId, dbRole);
TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能设置内置角色");
TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能设置受保护角色");
if (role.getModuleCode() != null) {
TimiException.requiredTrue(role.getModuleCode() == dbRole.getModuleCode(), "不能修改角色模块");
}
if (role.getOwnerType() != null) {
checkOwnerScope(operatorUserId, dbRole.getModuleCode(), role.getOwnerType(), role.getOwnerId());
}
}
@Override
public void checkCanDeleteRole(String operatorUserId, String roleId) {
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_DELETE);
Role role = roleMapper.selectById(roleId);
TimiException.required(role, "not found role");
if (authorizationScopeService.isSystemAdmin(operatorUserId)) {
return;
}
checkManageableRole(operatorUserId, role);
TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能删除内置角色");
TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能删除受保护角色");
}
@Override
public void checkCanGrantPermissions(String operatorUserId, String roleId, Collection<String> permissionIdList) {
if (TimiJava.isEmpty(permissionIdList)) {
return;
}
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_CREATE);
Role role = roleMapper.selectById(roleId);
TimiException.required(role, "not found role");
boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId);
if (!systemAdmin) {
checkManageableRole(operatorUserId, role);
}
List<Permission> permissionList = permissionMapper.selectByIdList(permissionIdList.stream().toList());
checkPermissionIdListComplete(permissionIdList, permissionList);
Set<String> grantablePermissionIdSet = listRoleDirectGrantablePermissionId(operatorUserId, role);
for (Permission permission : permissionList) {
TimiException.requiredTrue(grantablePermissionIdSet.contains(permission.getId()), "权限不在上级下放边界内:%s".formatted(permission.getCode()));
}
}
@Override
public void checkCanRevokePermissions(String operatorUserId, String roleId, Collection<String> permissionIdList) {
if (TimiJava.isEmpty(permissionIdList)) {
return;
}
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_DELETE);
Role role = roleMapper.selectById(roleId);
TimiException.required(role, "not found role");
if (authorizationScopeService.isSystemAdmin(operatorUserId)) {
return;
}
checkManageableRole(operatorUserId, role);
}
@Override
public void checkCanUpdateRoleDelegation(String operatorUserId, String roleId, Collection<String> permissionIdList) {
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_CREATE);
Role role = roleMapper.selectById(roleId);
TimiException.required(role, "not found role");
boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId);
if (!systemAdmin) {
checkManageableRole(operatorUserId, role);
}
if (TimiJava.isEmpty(permissionIdList)) {
return;
}
List<Permission> permissionList = permissionMapper.selectByIdList(permissionIdList.stream().toList());
checkPermissionIdListComplete(permissionIdList, permissionList);
boolean hasParentRole = TimiJava.isNotEmpty(role.getParentRoleId());
Set<String> parentDelegationPermissionIdSet = listParentDelegationPermissionId(roleId);
if (!isCoreDelegationRoot(role)) {
TimiException.requiredTrue(hasParentRole, "目标角色缺少管理父级");
}
Set<String> operatorGrantablePermissionIdSet = listGrantablePermissionId(operatorUserId, permissionList);
for (Permission permission : permissionList) {
TimiException.requiredTrue(role.getModuleCode() == permission.getModuleCode() || isCoreDelegationRoot(role) || isCoreAuthorizationPermission(permission), "不能配置其他模块转授权限");
if (hasParentRole) {
TimiException.requiredTrue(parentDelegationPermissionIdSet.contains(permission.getId()), "转授权限超过上级边界:%s".formatted(permission.getCode()));
}
if (!systemAdmin) {
TimiException.requiredTrue(operatorGrantablePermissionIdSet.contains(permission.getId()), "权限不在可转授范围内:%s".formatted(permission.getCode()));
}
}
}
@Override
public void checkCanUpdateRoleParent(String operatorUserId, String roleId, String parentRoleId) {
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_UPDATE);
Role targetRole = roleMapper.selectById(roleId);
TimiException.required(targetRole, "not found role");
boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId);
if (!systemAdmin) {
checkManageableRole(operatorUserId, targetRole);
}
if (TimiJava.isEmpty(parentRoleId)) {
TimiException.requiredTrue(isCoreDelegationRoot(targetRole), "非根角色必须设置管理父级");
return;
}
Role parentRole = roleMapper.selectById(parentRoleId);
TimiException.required(parentRole, "not found parent role");
TimiException.requiredTrue(!roleId.equals(parentRoleId), "管理父级不能指向自身");
TimiException.requiredTrue(!isManagementDescendant(roleId, parentRoleId), "管理父级不能指向自己的下级");
if (!systemAdmin && !isOperatorRole(operatorUserId, parentRole)) {
checkManageableRole(operatorUserId, parentRole);
}
}
@Override
public void checkCanUpdateRolePermissionInherits(String operatorUserId, String parentRoleId, Collection<String> childRoleIdList) {
if (TimiJava.isEmpty(childRoleIdList)) {
return;
}
checkCorePermission(operatorUserId, CorePermissionCode.ROLE_PERMISSION_INHERIT_CREATE);
Role parentRole = roleMapper.selectById(parentRoleId);
TimiException.required(parentRole, "not found parent role");
roleHierarchyService.checkNoCycle(parentRoleId, childRoleIdList);
if (authorizationScopeService.isSystemAdmin(operatorUserId)) {
return;
}
if (!isOperatorRole(operatorUserId, parentRole)) {
checkManageableRole(operatorUserId, parentRole);
}
List<Role> childRoleList = roleMapper.selectByIdList(childRoleIdList);
checkRoleIdListComplete(childRoleIdList, childRoleList);
for (Role childRole : childRoleList) {
TimiException.requiredTrue(parentRole.getModuleCode() == childRole.getModuleCode(), "不能继承其他模块角色");
checkManageableRole(operatorUserId, childRole);
}
}
private void checkGrantableRole(String operatorUserId, Role role) {
Set<String> grantableRoleIdSet = authorizationScopeService.listGrantableRole(operatorUserId, role.getModuleCode())
.stream()
.map(Role::getId)
.collect(Collectors.toSet());
TimiException.requiredTrue(grantableRoleIdSet.contains(role.getId()), "角色不在可授权范围内:%s".formatted(role.getCode()));
}
private void checkManageableRole(String operatorUserId, Role role) {
Set<String> manageableRoleIdSet = authorizationScopeService.listManageableRole(operatorUserId, role.getModuleCode())
.stream()
.map(Role::getId)
.collect(Collectors.toSet());
TimiException.requiredTrue(manageableRoleIdSet.contains(role.getId()), "角色不在可管理范围内:%s".formatted(role.getCode()));
}
private Set<String> listRoleAllPermissionId(String operatorUserId, String roleId) {
List<String> allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(operatorUserId, List.of(roleId));
return rolePermissionMapper.selectPermissionByRoleIdList(allRoleIdList)
.stream()
.map(Permission::getId)
.collect(Collectors.toSet());
}
private Set<String> listGrantablePermissionId(String operatorUserId, List<Permission> permissionList) {
if (TimiJava.isEmpty(permissionList)) {
return Set.of();
}
return permissionList.stream()
.map(Permission::getModuleCode)
.distinct()
.flatMap(moduleCode -> authorizationScopeService.listGrantablePermission(operatorUserId, moduleCode).stream())
.map(Permission::getId)
.collect(Collectors.toSet());
}
private Set<String> listRoleDirectGrantablePermissionId(String operatorUserId, Role role) {
boolean systemAdmin = authorizationScopeService.isSystemAdmin(operatorUserId);
if (systemAdmin && isCoreDelegationRoot(role) && TimiJava.isEmpty(role.getParentRoleId())) {
return permissionMapper.selectAll().stream().map(Permission::getId).collect(Collectors.toSet());
}
TimiException.requiredTrue(TimiJava.isNotEmpty(role.getParentRoleId()), "目标角色缺少管理父级");
Set<String> parentDelegationPermissionIdSet = listParentDelegationPermissionId(role.getId());
if (systemAdmin) {
return parentDelegationPermissionIdSet;
}
List<Permission> parentDelegationPermissionList = TimiJava.isEmpty(parentDelegationPermissionIdSet)
? List.of()
: permissionMapper.selectByIdList(parentDelegationPermissionIdSet.stream().toList());
Set<String> operatorGrantablePermissionIdSet = listGrantablePermissionId(operatorUserId, parentDelegationPermissionList);
return parentDelegationPermissionIdSet.stream()
.filter(operatorGrantablePermissionIdSet::contains)
.collect(Collectors.toSet());
}
private Set<String> listParentDelegationPermissionId(String roleId) {
Role role = roleMapper.selectById(roleId);
if (role == null || TimiJava.isEmpty(role.getParentRoleId())) {
return Set.of();
}
return rolePermissionDelegationMapper.selectPermissionByRoleIdList(List.of(role.getParentRoleId()))
.stream()
.map(Permission::getId)
.collect(Collectors.toSet());
}
private boolean isOperatorRole(String operatorUserId, Role role) {
List<String> roleIdList = userRoleMapper.selectByUserIdList(List.of(operatorUserId)).stream().map(UserRole::getRoleId).toList();
if (TimiJava.isEmpty(roleIdList)) {
return false;
}
return roleHierarchyService.listAllRoleIdIncludingChildren(operatorUserId, roleIdList).contains(role.getId());
}
private boolean isCoreDelegationRoot(Role role) {
return role != null
&& ModuleCode.CORE == role.getModuleCode()
&& (CoreRoleCode.SYSTEM.name().equals(role.getCode()) || CoreRoleCode.ADMIN.name().equals(role.getCode()));
}
private boolean isManagementDescendant(String parentRoleId, String childRoleId) {
List<Role> childRoleList = roleMapper.selectByParentRoleId(parentRoleId);
for (Role childRole : childRoleList) {
if (childRoleId.equals(childRole.getId()) || isManagementDescendant(childRole.getId(), childRoleId)) {
return true;
}
}
return false;
}
private boolean isCoreAuthorizationPermission(Permission permission) {
return permission != null && ModuleCode.CORE == permission.getModuleCode();
}
private void checkCanManageUserInModule(String operatorUserId, String targetUserId, ModuleCode moduleCode) {
ModuleResourceScopeResolver resolver = getResolver(moduleCode);
TimiException.requiredTrue(resolver == null || resolver.canManageUser(operatorUserId, targetUserId), "目标用户不在可管理范围内");
}
private void checkOwnerScope(String operatorUserId, ModuleCode moduleCode, AuthOwnerType ownerType, String ownerId) {
ModuleResourceScopeResolver resolver = getResolver(moduleCode);
TimiException.requiredTrue(resolver == null || resolver.canUseOwnerScope(operatorUserId, ownerType, ownerId), "归属范围不在可管理范围内");
}
private void checkCorePermission(String operatorUserId, CorePermissionCode permissionCode) {
TimiException.requiredTrue(hasPermission(operatorUserId, ModuleCode.CORE, permissionCode.getValue()), "权限不足:%s".formatted(permissionCode.getValue()));
}
private boolean hasPermission(String userId, ModuleCode moduleCode, String permissionCode) {
List<String> roleIdList = userRoleMapper.selectByUserIdList(List.of(userId)).stream().map(UserRole::getRoleId).toList();
if (TimiJava.isEmpty(roleIdList)) {
return false;
}
List<String> allRoleIdList = roleHierarchyService.listAllRoleIdIncludingChildren(userId, roleIdList);
return rolePermissionMapper.selectPermissionByRoleIdList(allRoleIdList)
.stream()
.anyMatch(permission -> moduleCode == permission.getModuleCode() && permissionCode.equals(permission.getCode()));
}
private ModuleResourceScopeResolver getResolver(ModuleCode moduleCode) {
return moduleResourceScopeResolverList.stream()
.filter(resolver -> moduleCode == resolver.moduleCode())
.findFirst()
.orElse(null);
}
private void checkRoleIdListComplete(Collection<String> idList, List<Role> roleList) {
Set<String> foundIdSet = roleList.stream().map(Role::getId).collect(Collectors.toSet());
Set<String> idSet = new HashSet<>(idList);
TimiException.requiredTrue(foundIdSet.containsAll(idSet), "存在无效角色 ID");
}
private void checkPermissionIdListComplete(Collection<String> idList, List<Permission> permissionList) {
Set<String> foundIdSet = permissionList.stream().map(Permission::getId).collect(Collectors.toSet());
Set<String> idSet = new HashSet<>(idList);
TimiException.requiredTrue(foundIdSet.containsAll(idSet), "存在无效权限 ID");
}
}

Some files were not shown because too many files have changed in this diff Show More