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`