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 7e94011..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 @@ -19,12 +19,12 @@ 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.vo.LoginResponse; 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.vo.LoginResponse; import com.imyeyu.java.TimiJava; import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.CaptchaValid; @@ -72,7 +72,7 @@ public class GaoUserController { } @AOPLog - @RequestRateLimit(value = 20, inSeconds = 10) + @RequestRateLimit(20) @PostMapping("/invite/validate") public GaoUserInviteValidateResponse validateInvite(@RequestBody @Valid GaoUserInviteValidateRequest req) { return inviteService.validate(req.getCode()); 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..3721e73 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 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..5c922fa 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 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/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..d461d8a 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 + +