diff --git a/pom.xml b/pom.xml
index b21b79f..c6b5a74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
com.imyeyu.timiserverapi
TimiServerAPI
- 1.0.13
+ 1.0.14
jar
TimiServerAPI
imyeyu.com API
diff --git a/src/main/java/com/imyeyu/api/config/RedisConfig.java b/src/main/java/com/imyeyu/api/config/RedisConfig.java
index 2ae799f..c381e7b 100644
--- a/src/main/java/com/imyeyu/api/config/RedisConfig.java
+++ b/src/main/java/com/imyeyu/api/config/RedisConfig.java
@@ -2,6 +2,7 @@ package com.imyeyu.api.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.common.entity.Multilingual;
+import com.imyeyu.api.modules.gao.bean.GaoUserInviteCache;
import com.imyeyu.spring.bean.RedisConfigParams;
import com.imyeyu.spring.config.AbstractRedisConfig;
import com.imyeyu.spring.util.Redis;
@@ -202,4 +203,10 @@ public class RedisConfig extends AbstractRedisConfig {
public Redis getClipboardRedisTemplate() {
return getRedis(database, RedisSerializers.STRING, RedisSerializers.STRING);
}
+
+ /// @return GAO 用户邀请,code: 邀请缓存
+ @Bean("redisGaoUserInvite")
+ public Redis getGaoUserInviteRedisTemplate() {
+ return getRedis(database, RedisSerializers.STRING, RedisSerializers.jacksonSerializer(jackson, GaoUserInviteCache.class));
+ }
}
diff --git a/src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java b/src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java
index 2086eeb..6af5d0d 100644
--- a/src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java
+++ b/src/main/java/com/imyeyu/api/modules/common/bean/SettingModule.java
@@ -32,6 +32,8 @@ public enum SettingModule {
TEMP_FILE(Setting.Module.TempFile.class),
+ USER(Setting.Module.User.class),
+
SYSTEM(Setting.Module.System.class),
SYSTEM_STATUS(Setting.Module.SystemStatus.class);
diff --git a/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java b/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java
index 1ce808b..6333371 100644
--- a/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java
+++ b/src/main/java/com/imyeyu/api/modules/common/service/SettingService.java
@@ -1,10 +1,10 @@
package com.imyeyu.api.modules.common.service;
+import com.imyeyu.api.modules.common.bean.SettingModule;
import com.imyeyu.api.modules.common.bean.attachment.SettingSupport;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.service.BaseService;
-import com.imyeyu.utils.Text;
import java.util.List;
@@ -19,7 +19,7 @@ public interface SettingService extends BaseService {
Setting getByCode(String bizType, String bizId, String code);
default Setting getSystem(Enum> bizType) {
- return getByCode(Text.bizClassName(bizType.getClass()), TimiJava.ZERO_UUID, bizType.toString());
+ return getByCode(SettingModule.fromClass(bizType.getClass()).name(), TimiJava.ZERO_UUID, bizType.toString());
}
List listByBizId(String bizType, String bizId);
diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
index 9015b38..7b46191 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java
@@ -84,7 +84,7 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
@Getter
final boolean grantToStoreManager;
- /// `true` 为初始化时授权给全局管理员角色
+ /// `true` 为初始化时授权给门店管理角色
@Getter
final boolean grantToGlobalManager;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java
index 3bd2282..b3101f1 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoRoleCode.java
@@ -12,8 +12,8 @@ import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum GaoRoleCode implements BuiltinRoleCode {
- /// 全局管理员
- GLOBAL_MANAGER("全局管理员", null),
+ /// 门店管理
+ GLOBAL_MANAGER("门店管理", null),
/// 店长
STORE_MANAGER("店长", GLOBAL_MANAGER.name());
diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoUserInviteCache.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoUserInviteCache.java
new file mode 100644
index 0000000..0505b40
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoUserInviteCache.java
@@ -0,0 +1,29 @@
+package com.imyeyu.api.modules.gao.bean;
+
+import lombok.Data;
+
+/// GAO 用户邀请缓存
+///
+/// @author Codex
+/// @since 2026-08-13
+@Data
+public class GaoUserInviteCache {
+
+ /// 发起邀请的用户 ID
+ private String inviterUserId;
+
+ /// 门店 ID
+ private String storeId;
+
+ /// 角色 ID
+ private String roleId;
+
+ /// 邀请来源或场景
+ private String scene;
+
+ /// 邀请创建时间,毫秒时间戳
+ private Long createdAt;
+
+ /// 邀请过期时间,毫秒时间戳
+ private Long expireAt;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java
index 2a97e6c..fbcb2e3 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventRecordController.java
@@ -16,6 +16,7 @@ import com.imyeyu.api.modules.gao.service.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage;
+import com.imyeyu.api.modules.gao.vo.GaoEventRecordPageResult;
import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage;
import com.imyeyu.api.modules.user.service.RoleChecker;
import com.imyeyu.api.modules.user.service.UserLoginService;
@@ -64,8 +65,12 @@ public class GaoEventRecordController {
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_READ)
@PostMapping("/list")
@JsonView(ResponseView.Public.class)
- public PageResult list(@RequestBody GaoEventRecordPage page) {
- PageResult result = service.pageByRange(page);
+ public GaoEventRecordPageResult list(@RequestBody GaoEventRecordPage page) {
+ PageResult pageResult = service.pageByRange(page);
+ GaoEventRecordPageResult result = new GaoEventRecordPageResult();
+ result.setTotal(pageResult.getTotal());
+ result.setList(pageResult.getList());
+ result.setNumberValueTotal(resolveNumberValueTotal(page));
{
List customerIdList = result.getList().stream().map(GaoEventRecord::getCustomerId).toList();
Map eventMap = eventService.mapByIdList(result.getList().stream().map(GaoEventRecord::getEventId).toList());
@@ -80,6 +85,22 @@ public class GaoEventRecordController {
return result;
}
+ /// 根据列表筛选条件返回单个 NUMBER 事件的数值合计
+ ///
+ /// @param page 登记记录范围查询请求
+ /// @return 数值合计,不满足单个 NUMBER 事件条件时为空
+ private Long resolveNumberValueTotal(GaoEventRecordPage page) {
+ List eventIdList = page.getEventIdList();
+ if (eventIdList == null || eventIdList.size() != 1) {
+ return null;
+ }
+ GaoEvent event = eventService.mapByIdList(eventIdList).get(eventIdList.get(0));
+ if (event == null || event.getValueType() != GaoEvent.ValueType.NUMBER) {
+ return null;
+ }
+ return service.sumNumberValueByRange(page);
+ }
+
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.EVENT_RECORD_READ)
diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java
index f045ea8..65570ba 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java
@@ -9,7 +9,14 @@ import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoUser;
import com.imyeyu.api.modules.gao.service.GaoStoreService;
+import com.imyeyu.api.modules.gao.service.GaoUserInviteService;
import com.imyeyu.api.modules.gao.service.GaoUserService;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateRequest;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateResponse;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterData;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterRequest;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateRequest;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateResponse;
import com.imyeyu.api.modules.user.entity.Role;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.service.RoleChecker;
@@ -17,12 +24,15 @@ import com.imyeyu.api.modules.user.service.RoleService;
import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.api.modules.user.service.UserRoleService;
import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.api.modules.user.vo.LoginResponse;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.annotation.AOPLog;
+import com.imyeyu.spring.annotation.CaptchaValid;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -32,6 +42,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
/// GAO 用户接口
///
@@ -46,11 +57,38 @@ public class GaoUserController {
private final RoleService roleService;
private final RoleChecker roleChecker;
private final GaoUserService service;
+ private final GaoUserInviteService inviteService;
private final UserRoleService userRoleService;
private final GaoStoreService gaoStoreService;
private final UserLoginService userLoginService;
private final AttachmentService attachmentService;
+ @AOPLog
+ @RequestRateLimit
+ @RequireGaoPermission(GaoPermissionCode.USER_CREATE)
+ @PostMapping("/invite")
+ public GaoUserInviteCreateResponse invite(@RequestBody @Valid GaoUserInviteCreateRequest req) {
+ return inviteService.create(req);
+ }
+
+ @AOPLog
+ @RequestRateLimit(20)
+ @PostMapping("/invite/validate")
+ public GaoUserInviteValidateResponse validateInvite(@RequestBody @Valid GaoUserInviteValidateRequest req) {
+ return inviteService.validate(req.getCode());
+ }
+
+ @AOPLog
+ @JsonView(ResponseView.Public.class)
+ @CaptchaValid
+ @RequestRateLimit(value = 5, inSeconds = 60)
+ @PostMapping("/invite/register")
+ public LoginResponse registerByInvite(@RequestBody @Valid GaoUserInviteRegisterRequest req) {
+ GaoUserInviteRegisterData data = req.getData();
+ String userId = inviteService.register(data);
+ return userLoginService.login(UUID.randomUUID().toString(), userId);
+ }
+
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@@ -137,6 +175,7 @@ public class GaoUserController {
@RequireGaoPermission(GaoPermissionCode.USER_READ)
@PostMapping("/role/list")
public List roleList() {
+ // TODO 此接口获取本模块所有角色,需要限制角色
return roleService.listByModuleCode(ModuleCode.GAO);
}
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java
index 0a354b1..df33b56 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java
@@ -38,6 +38,26 @@ public class GaoEvent extends UUIDEntity {
DELETED
}
+ /// 事件数值类型
+ public enum ValueType {
+
+ /// 不携带数值
+ NONE,
+
+ /// 携带整数数值
+ NUMBER
+ }
+
+ /// 事件数值单位
+ public enum ValueUnit {
+
+ /// 金额分
+ CENT,
+
+ /// 次数
+ COUNT
+ }
+
/// 登记限制类型
///
/// @author 夜雨
@@ -70,6 +90,9 @@ public class GaoEvent extends UUIDEntity {
final Long min;
}
+ /// 统计图表颜色
+ private String color;
+
/// 登记事件名称
private String name;
@@ -79,8 +102,14 @@ public class GaoEvent extends UUIDEntity {
/// 说明
private String description;
- /// 统计图表颜色
- private String color;
+ /// 数值类型
+ private ValueType valueType;
+
+ /// 数值单位
+ private ValueUnit valueUnit;
+
+ /// true 为登记时必须提交 numberValue
+ private Boolean requireValue;
/// 状态
private Status status;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java
index 39f4739..757a4cd 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEventRecord.java
@@ -40,6 +40,15 @@ public class GaoEventRecord extends UUIDEntity {
/// 操作管理员用户 ID
private String operatorUserId;
+ /// 本次登记提交的积分值
+ private Long pointValue;
+
+ /// 本次登记通过积分规则获得的积分总和
+ private Long earnedPointValue;
+
+ /// 本次登记提交的整数业务数值
+ private Long numberValue;
+
/// 备注
private String remark;
@@ -52,10 +61,6 @@ public class GaoEventRecord extends UUIDEntity {
/// 营业日递增序号
private Long businessDayNo;
- /// 本次登记提交的积分值,只参与积分触发,不落库
- @Transient
- private Long pointValue;
-
@Transient
private GaoCustomer customer;
diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java
index b37d46c..a17a27c 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java
@@ -42,7 +42,10 @@ public class GaoPointRule extends UUIDEntity {
AFTER_COUNT_EVERY_TIME,
/// 使用登记接口提交的积分值触发
- SOURCE_VALUE
+ SOURCE_VALUE,
+
+ /// 按来源事件数值累计分档触发
+ CUSTOMER_EVENT_VALUE_TIER
}
/// 积分值模式
@@ -52,7 +55,10 @@ public class GaoPointRule extends UUIDEntity {
FIXED,
/// 使用登记接口提交的积分值
- SOURCE_VALUE
+ SOURCE_VALUE,
+
+ /// 使用条件配置中的分档积分值
+ TIER
}
/// 门店 ID
diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java
index cbd2f00..07d79d4 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java
@@ -21,6 +21,28 @@ public interface GaoEventRecordMapper extends BaseMapper
Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt);
+ /// 查询事件中已携带数值的登记记录数量
+ ///
+ /// @param eventId 事件 ID
+ /// @return 携带数值的登记记录数量
+ Long countByEventIdAndNumberValue(String eventId);
+
+ /// 汇总客户指定来源事件在时间范围内的数值
+ ///
+ /// @param storeId 门店 ID
+ /// @param customerId 客户 ID
+ /// @param eventId 来源事件 ID
+ /// @param beginAt 起始时间,包含
+ /// @param endAt 结束时间,不包含
+ /// @return 数值总和
+ Long sumNumberValue(@Param("storeId") String storeId, @Param("customerId") String customerId, @Param("eventId") String eventId, @Param("beginAt") Long beginAt, @Param("endAt") Long endAt);
+
+ /// 回写登记记录最终获得的积分快照
+ ///
+ /// @param id 登记记录 ID
+ /// @param earnedPointValue 获得积分总和
+ void updateEarnedPointValue(@Param("id") String id, @Param("earnedPointValue") Long earnedPointValue);
+
/// 按门店和登记日期统计登记记录数
///
/// @param storeId 门店 ID
@@ -42,6 +64,12 @@ public interface GaoEventRecordMapper extends BaseMapper
/// @return 登记事件记录列表
List selectByRange(GaoEventRecordPage page);
+ /// 按登记记录列表条件汇总数值
+ ///
+ /// @param page 登记记录范围查询请求
+ /// @return 登记数值合计
+ Long sumNumberValueByRange(GaoEventRecordPage page);
+
/// 统计客户登记排行榜总数
///
/// @param page 排行榜分页请求
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java
index 3fb4fb8..7859567 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java
@@ -32,6 +32,12 @@ public interface GaoEventRecordService extends BaseService pageByRange(GaoEventRecordPage req);
+ /// 按登记记录列表条件汇总数值
+ ///
+ /// @param req 登记记录范围查询请求
+ /// @return 登记数值合计
+ Long sumNumberValueByRange(GaoEventRecordPage req);
+
/// 按时间范围查询客户登记排行榜
///
/// @param page 范围查询请求,不传时间为全部时间
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserInviteService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserInviteService.java
new file mode 100644
index 0000000..51b466b
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserInviteService.java
@@ -0,0 +1,31 @@
+package com.imyeyu.api.modules.gao.service;
+
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateRequest;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateResponse;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterData;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateResponse;
+
+/// GAO 用户邀请注册服务
+///
+/// @author Codex
+/// @since 2026-08-13
+public interface GaoUserInviteService {
+
+ /// 创建 GAO 用户邀请
+ ///
+ /// @param req 邀请创建请求
+ /// @return 邀请 code 和过期时间
+ GaoUserInviteCreateResponse create(GaoUserInviteCreateRequest req);
+
+ /// 校验 GAO 用户邀请是否可用
+ ///
+ /// @param code 邀请 code
+ /// @return 校验结果
+ GaoUserInviteValidateResponse validate(String code);
+
+ /// 使用邀请注册 GAO 用户
+ ///
+ /// @param data 注册数据
+ /// @return 注册成功的用户 ID
+ String register(GaoUserInviteRegisterData data);
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java
index 3c703d3..14afa78 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java
@@ -87,7 +87,9 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService pageRankByRange(GaoEventRecordRankPage page) {
TimiException.required(page.getStoreId(), "not found page.storeId");
- page.setTodayDateValue(dateValue(Time.now()));
+ validateRankPage(page);
+ if (page.getRankType() == GaoEventRecordRankPage.RankType.CONTINUOUS_DAYS) {
+ page.setTodayDateValue(dateValue(Time.now()));
+ }
PageResult result = new PageResult<>();
result.setTotal(mapper.countRankByRange(page));
result.setList(mapper.selectRankByRange(page));
return result;
}
+ /// 校验登记数值排行的事件范围
+ ///
+ /// @param page 排行榜查询请求
+ private void validateRankPage(GaoEventRecordRankPage page) {
+ if (page.getRankType() != GaoEventRecordRankPage.RankType.TOTAL_NUMBER_VALUE) {
+ return;
+ }
+ List eventIdList = page.getEventIdList();
+ TimiException.requiredTrue(eventIdList != null && eventIdList.size() == 1, "按登记数值排行必须选择单个事件");
+ GaoEvent event = eventService.mapByIdList(eventIdList).get(eventIdList.get(0));
+ TimiException.requiredTrue(event != null && event.getValueType() == GaoEvent.ValueType.NUMBER, "按登记数值排行只能选择 NUMBER 事件");
+ }
+
private void checkEventPermission(GaoEvent event) {
List roleCodeList = eventService.listRoleCode(event.getId());
TimiException.requiredTrue(roleCodeList != null && !roleCodeList.isEmpty(), "登记事件未配置可登记角色");
@@ -343,6 +362,26 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService roleCodeList) {
List dbRoleRelationList = roleRelationMapper.selectAllByEventIdList(List.of(eventId));
Set roleCodeSet = TimiJava.defaultIfEmpty(roleCodeList, List.of()).stream().filter(TimiJava::isNotEmpty).collect(Collectors.toSet());
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRuleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRuleServiceImplement.java
index f8c93b6..a8d15e0 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRuleServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointRuleServiceImplement.java
@@ -25,6 +25,8 @@ import java.util.List;
@RequiredArgsConstructor
public class GaoPointRuleServiceImplement extends AbstractEntityService implements GaoPointRuleService {
+ private static final int MAX_TIER_COUNT = 100;
+
private final GaoPointRuleMapper mapper;
private final GaoEventService eventService;
@@ -116,6 +118,13 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService ruleList = ruleService.listActive(record.getStoreId(), record.getEventId(), Time.now());
Long count = null;
+ long earnedPointValue = 0L;
for (GaoPointRule rule : ruleList) {
- Long pointDelta = resolvePointDelta(record, rule, count);
- if (pointDelta == null) {
+ RuleResolution resolution = resolveRule(record, rule, count);
+ if (resolution == null) {
continue;
}
- if (count == null) {
- count = eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null);
+ if (rule.getTriggerType() == GaoPointRule.TriggerType.FIRST_TIME || rule.getTriggerType() == GaoPointRule.TriggerType.AFTER_COUNT_EVERY_TIME) {
+ count = count == null ? count(record) : count;
+ }
+ long pointDelta = resolution.pointDelta();
+ if (0 < pointDelta) {
+ ledgerService.earn(record, rule, pointDelta, resolution.sourcePayload());
+ earnedPointValue = Math.addExact(earnedPointValue, pointDelta);
}
- JsonNode sourcePayload = sourcePayload(record, rule, pointDelta);
- ledgerService.earn(record, rule, pointDelta, sourcePayload);
if (Boolean.TRUE.equals(rule.getExclusive())) {
break;
}
}
+ record.setEarnedPointValue(earnedPointValue);
+ eventRecordMapper.updateEarnedPointValue(record.getId(), earnedPointValue);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@@ -104,25 +115,26 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
return ledgerService.adjust(request, userLoginService.getRequireLoginUserId());
}
- private Long resolvePointDelta(GaoEventRecord record, GaoPointRule rule, Long count) {
+ private RuleResolution resolveRule(GaoEventRecord record, GaoPointRule rule, Long count) {
return switch (rule.getTriggerType()) {
- case EVERY_TIME -> rule.getPointValue();
+ case EVERY_TIME -> new RuleResolution(rule.getPointValue(), null);
case FIRST_TIME -> {
long currentCount = count == null ? count(record) : count;
- yield currentCount == 1 ? rule.getPointValue() : null;
+ yield currentCount == 1 ? new RuleResolution(rule.getPointValue(), null) : 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;
+ yield includeCurrent ? threshold <= currentCount ? new RuleResolution(rule.getPointValue(), null) : null : threshold < currentCount ? new RuleResolution(rule.getPointValue(), null) : null;
}
case SOURCE_VALUE -> resolveSourceValue(record, rule);
+ case CUSTOMER_EVENT_VALUE_TIER -> resolveTier(record, rule);
};
}
- private Long resolveSourceValue(GaoEventRecord record, GaoPointRule rule) {
+ private RuleResolution resolveSourceValue(GaoEventRecord record, GaoPointRule rule) {
if (record.getPointValue() == null) {
return null;
}
@@ -136,17 +148,49 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
TimiException.requiredTrue(min == null || min.isNull() || min.longValue() <= pointValue, "pointValue 小于规则最小值");
TimiException.requiredTrue(max == null || max.isNull() || pointValue <= max.longValue(), "pointValue 大于规则最大值");
}
- return pointValue;
+ ObjectNode payload = jackson.createObjectNode();
+ payload.put("submittedPointValue", pointValue);
+ payload.put("validatedPointValue", pointValue);
+ return new RuleResolution(pointValue, payload);
}
- private JsonNode sourcePayload(GaoEventRecord record, GaoPointRule rule, long pointDelta) {
- if (rule.getTriggerType() != GaoPointRule.TriggerType.SOURCE_VALUE) {
- return null;
+ private RuleResolution resolveTier(GaoEventRecord record, GaoPointRule rule) {
+ JsonNode condition = rule.getConditionJson();
+ String sourceEventId = condition.get("sourceEventId").asText();
+ long amount = condition.get("window").get("amount").longValue();
+ ZoneId zone = ZoneId.systemDefault();
+ LocalDate currentDate = Instant.ofEpochMilli(record.getRegisteredAt()).atZone(zone).toLocalDate();
+ long windowBeginAt = currentDate.minusYears(amount).atStartOfDay(zone).toInstant().toEpochMilli();
+ long windowEndAt = Math.addExact(record.getRegisteredAt(), 1L);
+ long sumValue = TimiJava.defaultIfNull(eventRecordMapper.sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt), 0L);
+
+ JsonNode matchedTier = null;
+ for (JsonNode tier : condition.get("tiers")) {
+ long min = tier.get("min").longValue();
+ JsonNode maxNode = tier.get("max");
+ if (min <= sumValue && (maxNode == null || maxNode.isNull() || sumValue <= maxNode.longValue())) {
+ matchedTier = tier;
+ break;
+ }
}
+ TimiException.required(matchedTier, "TIER 没有匹配的分档");
+ long pointDelta = matchedTier.get("pointValue").longValue();
ObjectNode payload = jackson.createObjectNode();
- payload.put("submittedPointValue", record.getPointValue());
- payload.put("validatedPointValue", pointDelta);
- return payload;
+ payload.put("triggerType", GaoPointRule.TriggerType.CUSTOMER_EVENT_VALUE_TIER.name());
+ payload.put("sourceEventId", sourceEventId);
+ payload.put("sourceValueField", "numberValue");
+ payload.put("windowBeginAt", windowBeginAt);
+ payload.put("windowEndAt", windowEndAt);
+ payload.put("sumValue", sumValue);
+ ObjectNode tierPayload = payload.putObject("tier");
+ tierPayload.put("min", matchedTier.get("min").longValue());
+ JsonNode maxNode = matchedTier.get("max");
+ if (maxNode != null && !maxNode.isNull()) {
+ tierPayload.put("max", maxNode.longValue());
+ }
+ tierPayload.put("pointValue", pointDelta);
+ payload.put("pointDelta", pointDelta);
+ return new RuleResolution(pointDelta, payload);
}
private long count(GaoEventRecord record) {
@@ -156,4 +200,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) {
return "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId());
}
+
+ private record RuleResolution(Long pointDelta, JsonNode sourcePayload) {
+ }
}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserInviteServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserInviteServiceImplement.java
new file mode 100644
index 0000000..69c39e2
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserInviteServiceImplement.java
@@ -0,0 +1,325 @@
+package com.imyeyu.api.modules.gao.service.implement;
+
+import com.imyeyu.api.bean.ModuleCode;
+import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
+import com.imyeyu.api.modules.gao.bean.GaoUserInviteCache;
+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.GaoUser;
+import com.imyeyu.api.modules.gao.service.GaoStoreService;
+import com.imyeyu.api.modules.gao.service.GaoUserInviteService;
+import com.imyeyu.api.modules.gao.service.GaoUserService;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateRequest;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateResponse;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterData;
+import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateResponse;
+import com.imyeyu.api.modules.system.entity.Logger;
+import com.imyeyu.api.modules.system.service.LoggerService;
+import com.imyeyu.api.modules.user.entity.Role;
+import com.imyeyu.api.modules.user.entity.User;
+import com.imyeyu.api.modules.user.entity.UserRole;
+import com.imyeyu.api.modules.user.service.AuthorizationScopeService;
+import com.imyeyu.api.modules.user.service.PermissionChecker;
+import com.imyeyu.api.modules.user.service.RoleChecker;
+import com.imyeyu.api.modules.user.service.RoleService;
+import com.imyeyu.api.modules.user.service.UserLoginService;
+import com.imyeyu.api.modules.user.service.UserRoleService;
+import com.imyeyu.api.modules.user.service.UserService;
+import com.imyeyu.api.modules.user.bean.AuthOwnerType;
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.timi.TimiCode;
+import com.imyeyu.java.bean.timi.TimiException;
+import com.imyeyu.spring.util.Redis;
+import com.imyeyu.utils.Digest;
+import com.imyeyu.utils.Time;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.UUID;
+
+/// GAO 用户邀请注册服务实现
+///
+/// @author Codex
+/// @since 2026-08-13
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class GaoUserInviteServiceImplement implements GaoUserInviteService {
+
+ private static final String INVITE_KEY_PREFIX = "GAO:USER:INVITE:";
+ private static final String CONSUME_LOCK_KEY_PREFIX = "GAO:USER:INVITE:CONSUME:";
+ private static final String CREATE_CODE_LOCK_KEY = "GAO:USER:INVITE:CREATE:LOCK";
+ private static final String CODE_CHARACTERS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+ private static final int CODE_LENGTH = 10;
+ private static final long INVITE_TTL = Time.D * 7;
+ private static final long CODE_LOCK_TTL = Time.S * 5;
+ private static final long CONSUME_LOCK_TTL = Time.M * 2;
+ private static final long REGISTER_RATE_LIMIT_TTL = Time.M * 10;
+ private static final int CODE_RATE_LIMIT = 30;
+ private static final int REGISTER_VALUE_RATE_LIMIT = 10;
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ private final GaoStoreService gaoStoreService;
+ private final GaoUserService gaoUserService;
+ private final UserLoginService userLoginService;
+ private final UserRoleService userRoleService;
+ private final UserService userService;
+ private final RoleService roleService;
+ private final PermissionChecker permissionChecker;
+ private final RoleChecker roleChecker;
+ private final AuthorizationScopeService authorizationScopeService;
+ private final LoggerService loggerService;
+
+ @Qualifier("redisGaoUserInvite")
+ private final Redis redisInvite;
+
+ @Qualifier("redisLocker")
+ private final Redis redisLocker;
+
+ @Qualifier("redisRateLimit")
+ private final Redis redisRateLimit;
+
+ @Override
+ public GaoUserInviteCreateResponse create(GaoUserInviteCreateRequest req) {
+ Logger logger = new Logger(Logger.Module.GAO, Logger.Level.INFO, "GAO_USER_INVITE_CREATE");
+ try {
+ TimiException.required(req, "not found invite request");
+ TimiException.required(req.getRoleId(), "not found roleId");
+
+ String inviterUserId = userLoginService.getRequireLoginUserId();
+ logger.setUserId(inviterUserId);
+ permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.USER_CREATE.getValue());
+ boolean globalManager = authorizationScopeService.isSystemAdmin(inviterUserId)
+ || roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name());
+ boolean storeManager = !globalManager;
+ GaoStore loginStore = storeManager ? gaoStoreService.getByUserId(inviterUserId) : null;
+ String storeId = storeManager ? loginStore == null ? null : loginStore.getId() : req.getStoreId();
+ TimiException.required(storeId, "not found storeId");
+ checkStoreScope(inviterUserId, storeId);
+
+ GaoStore store = requireActiveStore(storeId);
+ Role role = requireInvitableRole(inviterUserId, req.getRoleId(), storeId);
+ long createdAt = Time.now();
+ long expireAt = createdAt + INVITE_TTL;
+
+ GaoUserInviteCache cache = new GaoUserInviteCache();
+ cache.setInviterUserId(inviterUserId);
+ cache.setStoreId(store.getId());
+ cache.setRoleId(role.getId());
+ cache.setScene(storeManager ? "STORE_MANAGER" : "STORE_MANAGEMENT");
+ cache.setCreatedAt(createdAt);
+ cache.setExpireAt(expireAt);
+ String code = saveInvite(cache);
+
+ logger.setContent("storeId=%s;roleId=%s;scene=%s;createdAt=%d;expireAt=%d".formatted(
+ store.getId(), role.getId(), cache.getScene(), createdAt, expireAt));
+ logger.setResult("created");
+ return new GaoUserInviteCreateResponse(code, expireAt);
+ } 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, "邀请创建失败", e);
+ } finally {
+ loggerService.create(logger);
+ }
+ }
+
+ @Override
+ public GaoUserInviteValidateResponse validate(String code) {
+ TimiException.required(code, "not found code");
+ checkCodeRateLimit(code);
+ GaoUserInviteCache cache = requireValidInvite(code);
+ requireActiveStore(cache.getStoreId());
+ requireValidRole(cache.getRoleId(), cache.getStoreId());
+ return new GaoUserInviteValidateResponse(true, cache.getExpireAt());
+ }
+
+ @Transactional(TimiServerDBConfig.ROLLBACKER)
+ @Override
+ public String register(GaoUserInviteRegisterData data) {
+ TimiException.required(data, "not found register data");
+ String code = data.getCode();
+ TimiException.required(code, "not found code");
+ checkCodeRateLimit(code);
+ checkRegisterValueRateLimit("NAME", data.getName());
+ checkRegisterValueRateLimit("PHONE", data.getPhoneNo());
+ String lockKey = getConsumeLockKey(code);
+ TimiException.requiredTrue(redisLocker.lock(lockKey, 1, CONSUME_LOCK_TTL), "邀请码无效或正在使用");
+
+ GaoUserInviteCache cache = null;
+ Logger logger = new Logger(Logger.Module.GAO, Logger.Level.WARN, "GAO_USER_INVITE_REGISTER");
+ try {
+ cache = requireValidInvite(code);
+ logger.setContent(buildLogContent(cache));
+ requireActiveStore(cache.getStoreId());
+ requireValidRole(cache.getRoleId(), cache.getStoreId());
+ TimiException.requiredNull(userService.getByName(data.getName()), "existed user name");
+ TimiException.requiredNull(userService.getByPhoneNo(data.getPhoneNo()), "existed phone number");
+ if (!redisInvite.destroy(getInviteKey(code))) {
+ throw invalidInvite();
+ }
+
+ User user = new User();
+ user.setName(data.getName());
+ user.setPhoneNo(data.getPhoneNo());
+ user.setPassword(data.getPassword());
+
+ GaoUser gaoUser = new GaoUser();
+ gaoUser.setStoreId(cache.getStoreId());
+ gaoUser.setUser(user);
+ gaoUserService.create(gaoUser);
+
+ UserRole userRole = new UserRole();
+ userRole.setId(UUID.randomUUID().toString());
+ userRole.setUserId(user.getId());
+ userRole.setRoleId(cache.getRoleId());
+ userRole.setCreatedAt(Time.now());
+ userRoleService.create(userRole);
+
+ logger.setLevel(Logger.Level.INFO);
+ logger.setUserId(user.getId());
+ logger.setResult(user.getId());
+ return user.getId();
+ } catch (TimiException e) {
+ 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, "邀请注册失败", e);
+ } finally {
+ loggerService.create(logger);
+ redisLocker.releaseLock(lockKey);
+ }
+ }
+
+ private String saveInvite(GaoUserInviteCache cache) {
+ TimiException.requiredTrue(redisLocker.lock(CREATE_CODE_LOCK_KEY, 1, CODE_LOCK_TTL), "服务繁忙,请稍后重试");
+ try {
+ for (int attempt = 0; attempt < 3; attempt++) {
+ String code = randomCode();
+ if (!redisInvite.has(getInviteKey(code))) {
+ redisInvite.set(getInviteKey(code), cache, INVITE_TTL);
+ return code;
+ }
+ }
+ throw new TimiException(TimiCode.ERROR_SERVICE_BUSY, "邀请码生成失败");
+ } finally {
+ redisLocker.releaseLock(CREATE_CODE_LOCK_KEY);
+ }
+ }
+
+ private String randomCode() {
+ StringBuilder code = new StringBuilder(CODE_LENGTH);
+ for (int i = 0; i < CODE_LENGTH; i++) {
+ code.append(CODE_CHARACTERS.charAt(SECURE_RANDOM.nextInt(CODE_CHARACTERS.length())));
+ }
+ return code.toString();
+ }
+
+ private GaoUserInviteCache requireValidInvite(String code) {
+ TimiException.required(code, "not found code");
+ GaoUserInviteCache cache = redisInvite.get(getInviteKey(code));
+ if (cache == null || cache.getExpireAt() == null || cache.getExpireAt() <= Time.now()) {
+ throw invalidInvite();
+ }
+ return cache;
+ }
+
+ private GaoStore requireActiveStore(String storeId) {
+ GaoStore store = gaoStoreService.get(storeId);
+ if (store == null || !store.isActivated()) {
+ throw invalidInvite();
+ }
+ return store;
+ }
+
+ private Role requireInvitableRole(String inviterUserId, String roleId, String storeId) {
+ Role role = requireValidRole(roleId, storeId);
+ List directRoleIdList = userRoleService.listRoleByUserId(ModuleCode.GAO, inviterUserId)
+ .stream()
+ .map(Role::getId)
+ .toList();
+ boolean descendant = authorizationScopeService.listManageableRole(inviterUserId, ModuleCode.GAO)
+ .stream()
+ .anyMatch(item -> role.getId().equals(item.getId()))
+ && !directRoleIdList.contains(role.getId());
+ if (!descendant) {
+ throw new TimiException(TimiCode.PERMISSION_ERROR, "角色不在可邀请范围内");
+ }
+ return role;
+ }
+
+ private Role requireValidRole(String roleId, String storeId) {
+ Role role = roleService.get(roleId);
+ if (role == null || role.getModuleCode() != ModuleCode.GAO || !isRoleUsableForStore(role, storeId)) {
+ throw invalidInvite();
+ }
+ return role;
+ }
+
+ private boolean isRoleUsableForStore(Role role, String storeId) {
+ return role.getOwnerType() == null
+ || role.getOwnerType() == AuthOwnerType.MODULE && (TimiJava.isEmpty(role.getOwnerId()) || ModuleCode.GAO.name().equals(role.getOwnerId()))
+ || role.getOwnerType() == AuthOwnerType.STORE && storeId.equals(role.getOwnerId());
+ }
+
+ private void checkStoreScope(String userId, String storeId) {
+ if (authorizationScopeService.isSystemAdmin(userId) || roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
+ return;
+ }
+ GaoStore userStore = gaoStoreService.getByUserId(userId);
+ TimiException.requiredTrue(userStore != null && storeId.equals(userStore.getId()), "无权操作其他门店");
+ }
+
+ private String buildLogContent(GaoUserInviteCache cache) {
+ return "inviterUserId=%s;storeId=%s;roleId=%s;scene=%s".formatted(cache.getInviterUserId(), cache.getStoreId(), cache.getRoleId(), cache.getScene());
+ }
+
+ private void checkCodeRateLimit(String code) {
+ checkRateLimit("GAO:USER:INVITE:LIMIT:CODE:" + Digest.sha256(code), CODE_RATE_LIMIT, REGISTER_RATE_LIMIT_TTL);
+ }
+
+ private void checkRegisterValueRateLimit(String type, String value) {
+ checkRateLimit("GAO:USER:INVITE:LIMIT:" + type + ":" + Digest.sha256(value), REGISTER_VALUE_RATE_LIMIT, REGISTER_RATE_LIMIT_TTL);
+ }
+
+ private void checkRateLimit(String key, int limit, long ttl) {
+ String lockKey = key + ":LOCK";
+ TimiException.requiredTrue(redisLocker.lock(lockKey, 1, Time.S * 5), "服务繁忙,请稍后重试");
+ try {
+ Integer count = redisRateLimit.get(key);
+ if (count != null && limit <= count) {
+ throw new TimiException(TimiCode.REQUEST_BAD, "request_rate_limit");
+ }
+ redisRateLimit.set(key, count == null ? 1 : count + 1, ttl);
+ } finally {
+ redisLocker.releaseLock(lockKey);
+ }
+ }
+
+ private String getInviteKey(String code) {
+ return INVITE_KEY_PREFIX + code;
+ }
+
+ private String getConsumeLockKey(String code) {
+ return CONSUME_LOCK_KEY_PREFIX + code;
+ }
+
+ private TimiException invalidInvite() {
+ return new TimiException(TimiCode.ARG_BAD, "邀请码无效或已过期");
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java
index 66891e4..44b42bd 100644
--- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java
@@ -1,6 +1,5 @@
package com.imyeyu.api.modules.gao.service.implement;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoStore;
import com.imyeyu.api.modules.gao.entity.GaoUser;
@@ -30,7 +29,6 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class GaoUserServiceImplement extends AbstractEntityService implements GaoUserService {
- private final ObjectMapper jackson;
private final UserService userService;
private final LoggerService loggerService;
private final GaoStoreService gaoStoreService;
@@ -52,7 +50,8 @@ public class GaoUserServiceImplement extends AbstractEntityService {
+
+ /// 级联校验注册数据
+ @Override
+ @NotNull
+ @Valid
+ public GaoUserInviteRegisterData getData() {
+ return super.getData();
+ }
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateRequest.java
new file mode 100644
index 0000000..b601a72
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateRequest.java
@@ -0,0 +1,18 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+
+/// GAO 用户邀请校验请求
+///
+/// @author Codex
+/// @since 2026-08-13
+@Data
+public class GaoUserInviteValidateRequest {
+
+ /// 邀请 code
+ @NotBlank
+ @Pattern(regexp = "[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{10}")
+ private String code;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateResponse.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateResponse.java
new file mode 100644
index 0000000..e6197ae
--- /dev/null
+++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateResponse.java
@@ -0,0 +1,19 @@
+package com.imyeyu.api.modules.gao.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/// GAO 用户邀请校验响应
+///
+/// @author Codex
+/// @since 2026-08-13
+@Data
+@AllArgsConstructor
+public class GaoUserInviteValidateResponse {
+
+ /// true 为邀请码可用
+ private boolean valid;
+
+ /// 邀请过期时间,毫秒时间戳
+ private Long expireAt;
+}
diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java
index 5972d9a..d365d71 100644
--- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java
@@ -18,6 +18,7 @@ import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Digest;
+import com.imyeyu.utils.Regex;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -38,7 +39,6 @@ import java.util.concurrent.ThreadLocalRandom;
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class UserLoginServiceImplement implements UserLoginService, TimiJava {
- private static final String REGEX_PHONE_NO = "^1\\d{10}$";
private static final String USER_TOKEN_KEY_PREFIX = "USER:TOKEN:";
private static final String PHONE_VERIFY_KEY_PREFIX = "USER:PHONE:VERIFY:";
private static final long PHONE_VERIFY_CODE_TTL = Time.M * 5;
@@ -61,7 +61,7 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
User dbUser = userService.get(req.getUser());
if (dbUser == null && req.getUser().contains("@")) {
dbUser = userService.getByVerifiedEmail(req.getUser());
- } else if (dbUser == null && isPhoneNo(req.getUser())) {
+ } else if (dbUser == null && Regex.isMatch(Regex.MOBILE_PHONE, req.getUser())) {
dbUser = userService.getByVerifiedPhoneNo(req.getUser());
} else if (dbUser == null) {
dbUser = userService.getByName(req.getUser());
@@ -143,8 +143,8 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void sendPhoneVerifyCode(String phoneNo) {
- TimiException.requiredTrue(isPhoneNo(phoneNo), "invalid phone number");
- User verifiedUser = userService.getByVerifiedPhoneNo(phoneNo);
+ TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, phoneNo), "invalid phone number");
+ User verifiedUser = userService.getByPhoneNo(phoneNo);
User loginUser = getRequireLoginUser();
TimiException.requiredTrue(verifiedUser == null || verifiedUser.getId().equals(loginUser.getId()), "existed phone number");
@@ -156,11 +156,11 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void verifyPhoneNo(String phoneNo, String verifyCode) {
- TimiException.requiredTrue(isPhoneNo(phoneNo), "invalid phone number");
+ TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, phoneNo), "invalid phone number");
TimiException.required(verifyCode, "not found verify code");
User user = getRequireLoginUser();
- User verifiedUser = userService.getByVerifiedPhoneNo(phoneNo);
+ User verifiedUser = userService.getByPhoneNo(phoneNo);
TimiException.requiredTrue(verifiedUser == null || verifiedUser.getId().equals(user.getId()), "existed phone number");
String cacheCode = redisUserPhoneVerify.get(getPhoneVerifyKey(phoneNo));
@@ -188,10 +188,6 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
userService.delete(user.getId());
}
- private boolean isPhoneNo(String value) {
- return TimiJava.isNotEmpty(value) && value.matches(REGEX_PHONE_NO);
- }
-
private String getPhoneVerifyKey(String phoneNo) {
return "%s%s".formatted(PHONE_VERIFY_KEY_PREFIX, phoneNo);
}
diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java
index 0f0ba8f..9274af4 100644
--- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java
+++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java
@@ -57,7 +57,7 @@ public class UserServiceImplement extends AbstractEntityService im
{
if (TimiJava.isNotEmpty(user.getPhoneNo())) {
TimiException.requiredTrue(user.isValidPhone(), "invalid phone");
- User byPhoneNo = getByVerifiedPhoneNo(user.getPhoneNo());
+ User byPhoneNo = getByPhoneNo(user.getPhoneNo());
TimiException.requiredNull(byPhoneNo, "existed phone number");
}
}
@@ -94,7 +94,7 @@ public class UserServiceImplement extends AbstractEntityService im
if (TimiJava.isNotEmpty(phoneNo)) {
TimiException.requiredTrue(user.isValidPhone(), "invalid phone");
if (!phoneNo.equals(dbUser.getPhoneNo())) {
- User byPhoneNo = getByVerifiedPhoneNo(phoneNo);
+ User byPhoneNo = getByPhoneNo(phoneNo);
TimiException.requiredTrue(byPhoneNo == null || byPhoneNo.getId().equals(dbUser.getId()), "existed phone number");
}
}
diff --git a/src/main/resources/db/migration/timiserver/V32__extend_gao_point_event_value.sql b/src/main/resources/db/migration/timiserver/V32__extend_gao_point_event_value.sql
new file mode 100644
index 0000000..8285ecc
--- /dev/null
+++ b/src/main/resources/db/migration/timiserver/V32__extend_gao_point_event_value.sql
@@ -0,0 +1,10 @@
+ALTER TABLE `gao_event`
+ ADD COLUMN `value_type` VARCHAR(16) NOT NULL DEFAULT 'NONE' COMMENT '数值类型,NONE 不携带数值,NUMBER 携带整数数值' AFTER `description`,
+ ADD COLUMN `value_unit` VARCHAR(16) NULL COMMENT '数值单位,CENT 金额分,COUNT 次数' AFTER `value_type`,
+ ADD COLUMN `require_value` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为登记时必须提交 numberValue' AFTER `value_unit`;
+
+ALTER TABLE `gao_event_record`
+ ADD COLUMN `point_value` BIGINT NULL COMMENT '本次登记提交的积分值' AFTER `operator_user_id`,
+ ADD COLUMN `earned_point_value` BIGINT NOT NULL DEFAULT 0 COMMENT '本次登记通过积分规则获得的积分总和' AFTER `point_value`,
+ ADD COLUMN `number_value` BIGINT NULL COMMENT '本次登记提交的整数业务数值' AFTER `earned_point_value`,
+ ADD KEY `idx_gao_event_record_value_sum` (`store_id`, `customer_id`, `event_id`, `deleted_at`, `registered_at`);
diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml
index 194d81b..2b9cbdc 100644
--- a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml
+++ b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml
@@ -26,6 +26,39 @@
AND `deleted_at` IS NULL
+
+
+
+
+
+ UPDATE `gao_event_record`
+ SET
+ `earned_point_value` = #{earnedPointValue},
+ `updated_at` = ROUND(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000)
+ WHERE
+ `id` = #{id}
+ AND `deleted_at` IS NULL
+
+
+
r.`deleted_at` IS NULL
@@ -119,6 +160,9 @@
#{eventId}
+
+ AND r.`number_value` IS NOT NULL
+
diff --git a/src/main/resources/mapper/timi-server/gao/GaoPointRuleMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPointRuleMapper.xml
index c5434d5..1277f83 100644
--- a/src/main/resources/mapper/timi-server/gao/GaoPointRuleMapper.xml
+++ b/src/main/resources/mapper/timi-server/gao/GaoPointRuleMapper.xml
@@ -10,6 +10,6 @@
AND (`begin_at` IS NULL OR `begin_at` <= #{now})
AND (`end_at` IS NULL OR #{now} <= `end_at`)
AND `deleted_at` IS NULL
- ORDER BY `priority` ASC, `created_at` ASC, `id` ASC
+ ORDER BY `priority`, `created_at`, `id`