add gao point
This commit is contained in:
@@ -49,6 +49,22 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
|
||||
|
||||
EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false),
|
||||
|
||||
POINT_ACCOUNT_READ("POINT_ACCOUNT:READ", "客户积分账户读取", true, true),
|
||||
|
||||
POINT_LEDGER_READ("POINT_LEDGER:READ", "客户积分流水读取", true, true),
|
||||
|
||||
POINT_RULE_CREATE("POINT_RULE:CREATE", "客户积分规则创建", true, true),
|
||||
|
||||
POINT_RULE_READ("POINT_RULE:READ", "客户积分规则读取", true, true),
|
||||
|
||||
POINT_RULE_UPDATE("POINT_RULE:UPDATE", "客户积分规则修改", true, true),
|
||||
|
||||
POINT_RULE_DELETE("POINT_RULE:DELETE", "客户积分规则删除", true, true),
|
||||
|
||||
POINT_ADJUST("POINT:ADJUST", "客户积分人工调整", true, true),
|
||||
|
||||
POINT_SOURCE_VALUE("POINT:SOURCE_VALUE", "登记提交自定义积分值", true, true),
|
||||
|
||||
STAT_READ("STAT:READ", "统计读取", false, false),
|
||||
|
||||
QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建", false, false),
|
||||
@@ -103,5 +119,4 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
|
||||
public boolean isGrantToAdmin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.imyeyu.api.modules.gao.controller;
|
||||
|
||||
import com.imyeyu.api.bean.ModuleCode;
|
||||
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
|
||||
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
||||
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoStoreService;
|
||||
import com.imyeyu.api.modules.user.service.RoleChecker;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.annotation.AOPLog;
|
||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// GAO 客户积分账户接口
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/gao/customer/point")
|
||||
public class GaoPointAccountController {
|
||||
|
||||
private final RoleChecker roleChecker;
|
||||
private final GaoStoreService storeService;
|
||||
private final GaoPointAccountService accountService;
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
|
||||
@PostMapping("/account/detail")
|
||||
public GaoPointAccount detail(@RequestParam String customerId, @RequestParam(required = false) String storeId) {
|
||||
storeId = resolveStoreId(storeId);
|
||||
GaoPointAccount account = accountService.getByCustomerId(storeId, customerId);
|
||||
TimiException.required(account, "not found customer point account");
|
||||
return account;
|
||||
}
|
||||
|
||||
private String resolveStoreId(String requestedStoreId) {
|
||||
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
||||
return requestedStoreId;
|
||||
}
|
||||
return storeService.getBelongIdByRequiredLoginUserId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.imyeyu.api.modules.gao.controller;
|
||||
|
||||
import com.imyeyu.api.bean.ModuleCode;
|
||||
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
|
||||
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
||||
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoStoreService;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
|
||||
import com.imyeyu.api.modules.user.service.RoleChecker;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.annotation.AOPLog;
|
||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// GAO 客户积分流水接口
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/gao/customer/point")
|
||||
public class GaoPointLedgerController {
|
||||
|
||||
private final RoleChecker roleChecker;
|
||||
private final GaoStoreService storeService;
|
||||
private final GaoPointLedgerService ledgerService;
|
||||
private final GaoPointTriggerService triggerService;
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_LEDGER_READ)
|
||||
@PostMapping("/ledger/list")
|
||||
public PageResult<GaoPointLedger> list(@RequestBody GaoPointLedgerPage page) {
|
||||
TimiException.required(page, "not found page");
|
||||
page.setStoreId(resolveStoreId(page.getStoreId()));
|
||||
return ledgerService.pageByQuery(page);
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_ADJUST)
|
||||
@PostMapping("/adjust")
|
||||
public void adjust(@RequestBody GaoPointAdjustRequest request) {
|
||||
request.setStoreId(resolveStoreId(request.getStoreId()));
|
||||
triggerService.adjust(request);
|
||||
}
|
||||
|
||||
private String resolveStoreId(String requestedStoreId) {
|
||||
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
||||
return requestedStoreId;
|
||||
}
|
||||
return storeService.getBelongIdByRequiredLoginUserId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.imyeyu.api.modules.gao.controller;
|
||||
|
||||
import com.imyeyu.api.bean.ModuleCode;
|
||||
import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission;
|
||||
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
||||
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoStoreService;
|
||||
import com.imyeyu.api.modules.user.service.RoleChecker;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.annotation.AOPLog;
|
||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// GAO 客户积分规则接口
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/gao/customer/point")
|
||||
public class GaoPointRuleController {
|
||||
|
||||
private final RoleChecker roleChecker;
|
||||
private final GaoStoreService storeService;
|
||||
private final GaoPointRuleService ruleService;
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ)
|
||||
@PostMapping("/rule/list")
|
||||
public PageResult<GaoPointRule> list(@RequestBody Page<GaoPointRule> page) {
|
||||
TimiException.required(page, "not found page");
|
||||
if (!isGlobalManager()) {
|
||||
GaoPointRule example = TimiJava.defaultIfNull(page.getEqualsExample(), new GaoPointRule());
|
||||
example.setStoreId(resolveStoreId(null));
|
||||
page.setEqualsExample(example);
|
||||
}
|
||||
return ruleService.page(page);
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_READ)
|
||||
@PostMapping("/rule/detail")
|
||||
public GaoPointRule detail(@RequestParam String id) {
|
||||
GaoPointRule rule = ruleService.get(id);
|
||||
TimiException.required(rule, "not found point rule");
|
||||
checkStore(rule.getStoreId());
|
||||
return rule;
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_CREATE)
|
||||
@PostMapping("/rule/create")
|
||||
public String create(@RequestBody GaoPointRule rule) {
|
||||
rule.setStoreId(resolveStoreId(rule.getStoreId()));
|
||||
ruleService.create(rule);
|
||||
return rule.getId();
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_UPDATE)
|
||||
@PostMapping("/rule/update")
|
||||
public void update(@RequestBody GaoPointRule rule) {
|
||||
rule.setStoreId(resolveStoreId(rule.getStoreId()));
|
||||
ruleService.update(rule);
|
||||
}
|
||||
|
||||
@AOPLog
|
||||
@RequestRateLimit
|
||||
@RequireGaoPermission(GaoPermissionCode.POINT_RULE_DELETE)
|
||||
@PostMapping("/rule/delete")
|
||||
public void delete(@RequestParam String id) {
|
||||
GaoPointRule rule = ruleService.get(id);
|
||||
TimiException.required(rule, "not found point rule");
|
||||
checkStore(rule.getStoreId());
|
||||
ruleService.delete(id);
|
||||
}
|
||||
|
||||
private String resolveStoreId(String requestedStoreId) {
|
||||
if (isGlobalManager()) {
|
||||
return requestedStoreId;
|
||||
}
|
||||
return storeService.getBelongIdByRequiredLoginUserId();
|
||||
}
|
||||
|
||||
private void checkStore(String storeId) {
|
||||
if (!isGlobalManager()) {
|
||||
TimiException.requiredTrue(resolveStoreId(null).equals(storeId), "无权操作其他门店数据");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isGlobalManager() {
|
||||
return roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name());
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ public class GaoEventRecord extends UUIDEntity {
|
||||
/// 营业日递增序号
|
||||
private Long businessDayNo;
|
||||
|
||||
/// 本次登记提交的积分值,只参与积分触发,不落库
|
||||
@Transient
|
||||
private Long pointValue;
|
||||
|
||||
@Transient
|
||||
private GaoCustomer customer;
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.imyeyu.api.modules.gao.entity;
|
||||
|
||||
import com.imyeyu.spring.entity.UUIDEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/// GAO 客户积分账户
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GaoPointAccount extends UUIDEntity {
|
||||
|
||||
/// 门店 ID
|
||||
private String storeId;
|
||||
|
||||
/// 客户 ID
|
||||
private String customerId;
|
||||
|
||||
/// 当前可用积分余额
|
||||
private Long balance;
|
||||
|
||||
/// 累计获得积分
|
||||
private Long totalEarned;
|
||||
|
||||
/// 累计撤销积分
|
||||
private Long totalRevoked;
|
||||
|
||||
/// 累计人工调整积分,可正可负
|
||||
private Long totalAdjusted;
|
||||
|
||||
/// 乐观锁版本
|
||||
private Long version;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.imyeyu.api.modules.gao.entity;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.imyeyu.spring.entity.UUIDEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/// GAO 客户积分流水
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GaoPointLedger extends UUIDEntity {
|
||||
|
||||
/// 流水变化类型
|
||||
public enum ChangeType {
|
||||
|
||||
/// 登记事件获得积分
|
||||
EARN,
|
||||
|
||||
/// 删除登记记录撤销积分
|
||||
REVOKE,
|
||||
|
||||
/// 人工调整积分
|
||||
ADJUST
|
||||
}
|
||||
|
||||
/// 流水方向
|
||||
public enum Direction {
|
||||
|
||||
/// 增加积分
|
||||
INCREASE,
|
||||
|
||||
/// 减少积分
|
||||
DECREASE
|
||||
}
|
||||
|
||||
/// 客户积分账户 ID
|
||||
private String accountId;
|
||||
|
||||
/// 门店 ID
|
||||
private String storeId;
|
||||
|
||||
/// 客户 ID
|
||||
private String customerId;
|
||||
|
||||
/// 变化类型
|
||||
private ChangeType changeType;
|
||||
|
||||
/// 方向
|
||||
private Direction direction;
|
||||
|
||||
/// 本次变化积分,增加为正,减少为负
|
||||
private Long pointDelta;
|
||||
|
||||
/// 变化前余额
|
||||
private Long balanceBefore;
|
||||
|
||||
/// 变化后余额
|
||||
private Long balanceAfter;
|
||||
|
||||
/// 命中的积分规则 ID
|
||||
private String ruleId;
|
||||
|
||||
/// 登记事件 ID
|
||||
private String eventId;
|
||||
|
||||
/// 登记记录 ID
|
||||
private String eventRecordId;
|
||||
|
||||
/// 来源快照
|
||||
private JsonNode sourcePayload;
|
||||
|
||||
/// 幂等键
|
||||
private String idempotencyKey;
|
||||
|
||||
/// 备注
|
||||
private String remark;
|
||||
|
||||
/// 操作用户 ID
|
||||
private String operatorUserId;
|
||||
|
||||
/// 发生时间
|
||||
private Long occurredAt;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.imyeyu.api.modules.gao.entity;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.imyeyu.spring.entity.UUIDEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/// GAO 客户积分规则
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GaoPointRule extends UUIDEntity {
|
||||
|
||||
/// 规则状态
|
||||
public enum Status {
|
||||
|
||||
/// 草稿,不参与触发
|
||||
DRAFT,
|
||||
|
||||
/// 启用,参与触发
|
||||
ACTIVE,
|
||||
|
||||
/// 停用,不参与触发
|
||||
INACTIVE,
|
||||
|
||||
/// 已删除,不参与触发
|
||||
DELETED
|
||||
}
|
||||
|
||||
/// 触发类型
|
||||
public enum TriggerType {
|
||||
|
||||
/// 每登记一次触发一次
|
||||
EVERY_TIME,
|
||||
|
||||
/// 客户首次登记该事件触发一次
|
||||
FIRST_TIME,
|
||||
|
||||
/// 客户累计登记达到 N 次后每次触发
|
||||
AFTER_COUNT_EVERY_TIME,
|
||||
|
||||
/// 使用登记接口提交的积分值触发
|
||||
SOURCE_VALUE
|
||||
}
|
||||
|
||||
/// 积分值模式
|
||||
public enum PointMode {
|
||||
|
||||
/// 使用规则固定积分值
|
||||
FIXED,
|
||||
|
||||
/// 使用登记接口提交的积分值
|
||||
SOURCE_VALUE
|
||||
}
|
||||
|
||||
/// 门店 ID
|
||||
private String storeId;
|
||||
|
||||
/// 登记事件 ID
|
||||
private String eventId;
|
||||
|
||||
/// 规则名称
|
||||
private String name;
|
||||
|
||||
/// 规则状态
|
||||
private Status status;
|
||||
|
||||
/// 触发类型
|
||||
private TriggerType triggerType;
|
||||
|
||||
/// 积分值模式
|
||||
private PointMode pointMode;
|
||||
|
||||
/// 固定积分值
|
||||
private Long pointValue;
|
||||
|
||||
/// 条件配置
|
||||
private JsonNode conditionJson;
|
||||
|
||||
/// 生效开始时间戳,毫秒,空为不限制
|
||||
private Long beginAt;
|
||||
|
||||
/// 生效结束时间戳,毫秒,空为不限制
|
||||
private Long endAt;
|
||||
|
||||
/// 优先级,数值小优先
|
||||
private Integer priority;
|
||||
|
||||
/// true 为命中后停止继续匹配后续规则
|
||||
private Boolean exclusive;
|
||||
|
||||
/// 备注
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.imyeyu.api.modules.gao.mapper;
|
||||
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/// GAO 客户积分账户 Mapper
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
public interface GaoPointAccountMapper extends BaseMapper<GaoPointAccount, String> {
|
||||
|
||||
/// 查询客户当前有效积分账户
|
||||
///
|
||||
/// @param storeId 门店 ID
|
||||
/// @param customerId 客户 ID
|
||||
/// @return 积分账户
|
||||
GaoPointAccount selectByStoreIdAndCustomerId(@Param("storeId") String storeId, @Param("customerId") String customerId);
|
||||
|
||||
/// 创建客户积分账户,已存在时保持原账户不变
|
||||
///
|
||||
/// @param account 积分账户
|
||||
void insertIfAbsent(@Param("account") GaoPointAccount account);
|
||||
|
||||
/// 锁定积分账户
|
||||
///
|
||||
/// @param id 账户 ID
|
||||
/// @return 积分账户
|
||||
GaoPointAccount selectForUpdate(@Param("id") String id);
|
||||
|
||||
/// 原子更新积分账户余额和统计值
|
||||
///
|
||||
/// @param id 账户 ID
|
||||
/// @param delta 本次余额变化
|
||||
/// @param totalEarnedDelta 累计获得积分变化
|
||||
/// @param totalRevokedDelta 累计撤销积分变化
|
||||
/// @param totalAdjustedDelta 累计人工调整积分变化
|
||||
/// @param updatedAt 更新时间
|
||||
/// @return 更新行数
|
||||
int updateBalance(@Param("id") String id, @Param("delta") long delta, @Param("totalEarnedDelta") long totalEarnedDelta, @Param("totalRevokedDelta") long totalRevokedDelta, @Param("totalAdjustedDelta") long totalAdjustedDelta, @Param("updatedAt") long updatedAt);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.imyeyu.api.modules.gao.mapper;
|
||||
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// GAO 客户积分流水 Mapper
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
public interface GaoPointLedgerMapper extends BaseMapper<GaoPointLedger, String> {
|
||||
|
||||
/// 按幂等键查询流水
|
||||
///
|
||||
/// @param idempotencyKey 幂等键
|
||||
/// @return 流水
|
||||
GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey);
|
||||
|
||||
/// 查询登记记录产生的原始获得积分流水
|
||||
///
|
||||
/// @param eventRecordId 登记记录 ID
|
||||
/// @return 获得积分流水
|
||||
List<GaoPointLedger> selectEarnByEventRecordId(@Param("eventRecordId") String eventRecordId);
|
||||
|
||||
/// 批量查询幂等键对应的流水
|
||||
///
|
||||
/// @param idempotencyKeyList 幂等键列表
|
||||
/// @return 已存在流水
|
||||
List<GaoPointLedger> selectByIdempotencyKeyList(@Param("idempotencyKeyList") List<String> idempotencyKeyList);
|
||||
|
||||
/// 查询积分流水总数
|
||||
///
|
||||
/// @param page 查询条件
|
||||
/// @return 流水总数
|
||||
long countByQuery(GaoPointLedgerPage page);
|
||||
|
||||
/// 分页查询积分流水
|
||||
///
|
||||
/// @param page 查询条件
|
||||
/// @return 流水列表
|
||||
List<GaoPointLedger> selectByQuery(GaoPointLedgerPage page);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.imyeyu.api.modules.gao.mapper;
|
||||
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// GAO 客户积分规则 Mapper
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
public interface GaoPointRuleMapper extends BaseMapper<GaoPointRule, String> {
|
||||
|
||||
/// 查询当前时间可匹配的启用规则
|
||||
///
|
||||
/// @param storeId 门店 ID
|
||||
/// @param eventId 登记事件 ID
|
||||
/// @param now 当前时间戳
|
||||
/// @return 按优先级排序的规则列表
|
||||
List<GaoPointRule> selectActiveByStoreIdAndEventId(@Param("storeId") String storeId, @Param("eventId") String eventId, @Param("now") long now);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.imyeyu.api.modules.gao.service;
|
||||
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||
import com.imyeyu.spring.service.BaseService;
|
||||
|
||||
/// GAO 客户积分账户服务
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
public interface GaoPointAccountService extends BaseService<GaoPointAccount, String> {
|
||||
|
||||
/// 查询客户当前有效积分账户
|
||||
///
|
||||
/// @param storeId 门店 ID
|
||||
/// @param customerId 客户 ID
|
||||
/// @return 积分账户,不存在时返回 null
|
||||
GaoPointAccount getByCustomerId(String storeId, String customerId);
|
||||
|
||||
/// 获取并锁定客户积分账户,不存在时创建
|
||||
///
|
||||
/// @param storeId 门店 ID
|
||||
/// @param customerId 客户 ID
|
||||
/// @return 已锁定的积分账户
|
||||
GaoPointAccount getOrCreateForUpdate(String storeId, String customerId);
|
||||
|
||||
/// 原子更新账户余额
|
||||
///
|
||||
/// @param accountId 账户 ID
|
||||
/// @param delta 本次余额变化
|
||||
/// @param totalEarnedDelta 累计获得积分变化
|
||||
/// @param totalRevokedDelta 累计撤销积分变化
|
||||
/// @param totalAdjustedDelta 累计人工调整积分变化
|
||||
/// @return 更新行数
|
||||
int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.imyeyu.api.modules.gao.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.service.BaseService;
|
||||
|
||||
/// GAO 客户积分流水服务
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
public interface GaoPointLedgerService extends BaseService<GaoPointLedger, String> {
|
||||
|
||||
/// 写入登记获得积分流水
|
||||
///
|
||||
/// @param record 登记记录
|
||||
/// @param rule 命中规则
|
||||
/// @param pointDelta 积分变化
|
||||
/// @param sourcePayload 来源快照
|
||||
/// @return 积分流水
|
||||
GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload);
|
||||
|
||||
/// 写入删除登记记录的撤销流水
|
||||
///
|
||||
/// @param originLedger 原始获得积分流水
|
||||
/// @param record 登记记录
|
||||
/// @return 撤销流水
|
||||
GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record);
|
||||
|
||||
/// 写入人工调整流水
|
||||
///
|
||||
/// @param request 人工调整请求
|
||||
/// @param operatorUserId 操作用户 ID
|
||||
/// @return 调整流水
|
||||
GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId);
|
||||
|
||||
/// 分页查询积分流水
|
||||
///
|
||||
/// @param page 查询条件
|
||||
/// @return 分页结果
|
||||
PageResult<GaoPointLedger> pageByQuery(GaoPointLedgerPage page);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.imyeyu.api.modules.gao.service;
|
||||
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||
import com.imyeyu.spring.service.BaseService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// GAO 客户积分规则服务
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
public interface GaoPointRuleService extends BaseService<GaoPointRule, String> {
|
||||
|
||||
/// 查询当前时间可匹配的启用规则
|
||||
///
|
||||
/// @param storeId 门店 ID
|
||||
/// @param eventId 登记事件 ID
|
||||
/// @param now 当前时间戳
|
||||
/// @return 规则列表
|
||||
List<GaoPointRule> listActive(String storeId, String eventId, long now);
|
||||
|
||||
/// 创建积分规则
|
||||
///
|
||||
/// @param rule 积分规则
|
||||
void create(GaoPointRule rule);
|
||||
|
||||
/// 修改积分规则
|
||||
///
|
||||
/// @param rule 积分规则
|
||||
void update(GaoPointRule rule);
|
||||
|
||||
/// 删除积分规则
|
||||
///
|
||||
/// @param id 规则 ID
|
||||
void delete(String id);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.imyeyu.api.modules.gao.service;
|
||||
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
||||
|
||||
/// GAO 客户积分触发服务
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
public interface GaoPointTriggerService {
|
||||
|
||||
/// 在登记记录创建成功后触发积分
|
||||
///
|
||||
/// @param record 登记记录
|
||||
void onEventRecordCreated(GaoEventRecord record);
|
||||
|
||||
/// 撤销登记记录产生的全部获得积分
|
||||
///
|
||||
/// @param record 登记记录
|
||||
void revokeByEventRecord(GaoEventRecord record);
|
||||
|
||||
/// 执行人工调整
|
||||
///
|
||||
/// @param request 人工调整请求
|
||||
/// @return 调整流水
|
||||
GaoPointLedger adjust(GaoPointAdjustRequest request);
|
||||
}
|
||||
+39
@@ -10,6 +10,7 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
||||
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
|
||||
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoEventService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
|
||||
@@ -63,6 +64,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
||||
private final AttachmentService attachmentService;
|
||||
private final GaoCustomerService customerService;
|
||||
private final GaoStoreBusinessDayService businessDayService;
|
||||
private final GaoPointTriggerService pointTriggerService;
|
||||
|
||||
private final GaoEventRecordMapper mapper;
|
||||
|
||||
@@ -116,6 +118,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
||||
dbRecord.setRegisteredAt(registeredAt);
|
||||
dbRecord.setRegisteredDateValue(registeredDateValue);
|
||||
dbRecord.setBusinessDayNo(businessDayNo);
|
||||
dbRecord.setPointValue(record.getPointValue());
|
||||
super.create(dbRecord);
|
||||
// 附件
|
||||
if (i == 0) {
|
||||
@@ -131,6 +134,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
||||
attachmentService.update(clone);
|
||||
}
|
||||
}
|
||||
pointTriggerService.onEventRecordCreated(dbRecord);
|
||||
logIdList.add(dbRecord.getId());
|
||||
}
|
||||
logger.setLevel(Logger.Level.INFO);
|
||||
@@ -149,6 +153,14 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void createBatch(List<GaoEventRecord> recordList) {
|
||||
for (GaoEventRecord record : TimiJava.safeIterable(recordList)) {
|
||||
create(record);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void update(GaoEventRecord record) {
|
||||
@@ -193,6 +205,33 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
TimiException.required(id, "not found record.id");
|
||||
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_DELETE");
|
||||
try {
|
||||
logger.setContent(id);
|
||||
GaoEventRecord record = get(id);
|
||||
TimiException.required(record, "not found event record");
|
||||
pointTriggerService.revokeByEventRecord(record);
|
||||
super.delete(id);
|
||||
logger.setLevel(Logger.Level.INFO);
|
||||
logger.setResult(id);
|
||||
} catch (TimiException e) {
|
||||
logger.setLevel(Logger.Level.WARN);
|
||||
logger.setException(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.setLevel(Logger.Level.ERROR);
|
||||
logger.setException(TimiJava.serializeThrowable(e));
|
||||
log.error("删除 GAO 登记记录失败", e);
|
||||
throw new TimiException(TimiCode.ERROR, "删除 GAO 登记记录失败", e);
|
||||
} finally {
|
||||
loggerService.create(logger);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void createWithCustomer(GaoCustomer customer) {
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.imyeyu.api.modules.gao.service.implement;
|
||||
|
||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||
import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import com.imyeyu.spring.service.AbstractEntityService;
|
||||
import com.imyeyu.utils.Time;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/// GAO 客户积分账户服务实现
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPointAccount, String> implements GaoPointAccountService {
|
||||
|
||||
private final GaoPointAccountMapper mapper;
|
||||
|
||||
@Override
|
||||
protected BaseMapper<GaoPointAccount, String> mapper() {
|
||||
return mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GaoPointAccount getByCustomerId(String storeId, String customerId) {
|
||||
TimiException.required(storeId, "not found storeId");
|
||||
TimiException.required(customerId, "not found customerId");
|
||||
return mapper.selectByStoreIdAndCustomerId(storeId, customerId);
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public GaoPointAccount getOrCreateForUpdate(String storeId, String customerId) {
|
||||
TimiException.required(storeId, "not found storeId");
|
||||
TimiException.required(customerId, "not found customerId");
|
||||
|
||||
GaoPointAccount account = mapper.selectByStoreIdAndCustomerId(storeId, customerId);
|
||||
if (account == null) {
|
||||
account = new GaoPointAccount();
|
||||
account.setId(UUID.randomUUID().toString());
|
||||
account.setStoreId(storeId);
|
||||
account.setCustomerId(customerId);
|
||||
account.setBalance(0L);
|
||||
account.setTotalEarned(0L);
|
||||
account.setTotalRevoked(0L);
|
||||
account.setTotalAdjusted(0L);
|
||||
account.setVersion(0L);
|
||||
account.setCreatedAt(Time.now());
|
||||
mapper.insertIfAbsent(account);
|
||||
account = mapper.selectByStoreIdAndCustomerId(storeId, customerId);
|
||||
}
|
||||
TimiException.required(account, "not found customer point account");
|
||||
GaoPointAccount lockedAccount = mapper.selectForUpdate(account.getId());
|
||||
TimiException.required(lockedAccount, "not found customer point account");
|
||||
return lockedAccount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta) {
|
||||
TimiException.required(accountId, "not found accountId");
|
||||
return mapper.updateBalance(accountId, delta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta, Time.now());
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package com.imyeyu.api.modules.gao.service.implement;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
||||
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
|
||||
import com.imyeyu.api.modules.user.service.UserLoginService;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import com.imyeyu.spring.service.AbstractEntityService;
|
||||
import com.imyeyu.utils.Time;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/// GAO 客户积分流水服务实现
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPointLedger, String> implements GaoPointLedgerService {
|
||||
|
||||
private final GaoPointLedgerMapper mapper;
|
||||
private final GaoPointAccountService accountService;
|
||||
private final UserLoginService userLoginService;
|
||||
|
||||
@Override
|
||||
protected BaseMapper<GaoPointLedger, String> mapper() {
|
||||
return mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(GaoPointLedger ledger) {
|
||||
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止修改");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止删除");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy(String id) {
|
||||
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "积分流水只允许追加,禁止物理删除");
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload) {
|
||||
TimiException.required(record, "not found event record");
|
||||
TimiException.required(rule, "not found point rule");
|
||||
TimiException.requiredTrue(0 < pointDelta, "pointDelta invalid");
|
||||
String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId());
|
||||
return apply(record.getStoreId(), record.getCustomerId(), GaoPointLedger.ChangeType.EARN, pointDelta, rule.getId(), record.getEventId(), record.getId(), sourcePayload, idempotencyKey, rule.getRemark(), record.getOperatorUserId(), record.getRegisteredAt());
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record) {
|
||||
TimiException.required(originLedger, "not found origin ledger");
|
||||
TimiException.required(record, "not found event record");
|
||||
TimiException.requiredTrue(0 < originLedger.getPointDelta(), "origin ledger pointDelta invalid");
|
||||
String idempotencyKey = "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId());
|
||||
return apply(originLedger.getStoreId(), originLedger.getCustomerId(), GaoPointLedger.ChangeType.REVOKE, -originLedger.getPointDelta(), originLedger.getRuleId(), originLedger.getEventId(), originLedger.getEventRecordId(), originLedger.getSourcePayload(), idempotencyKey, "撤销登记记录积分", userLoginService.getRequireLoginUserId(), Time.now());
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId) {
|
||||
TimiException.required(request, "not found adjust request");
|
||||
TimiException.requiredTrue(TimiJava.isNotEmpty(request.getAdjustRequestId()), "not found adjustRequestId");
|
||||
TimiException.required(request.getStoreId(), "not found storeId");
|
||||
TimiException.required(request.getCustomerId(), "not found customerId");
|
||||
TimiException.required(request.getPointDelta(), "not found pointDelta");
|
||||
TimiException.requiredTrue(request.getPointDelta() != 0, "pointDelta invalid");
|
||||
TimiException.requiredTrue(TimiJava.isNotEmpty(request.getRemark()), "not found adjust remark");
|
||||
TimiException.required(operatorUserId, "not found operatorUserId");
|
||||
String idempotencyKey = "MANUAL_ADJUST:%s".formatted(request.getAdjustRequestId());
|
||||
return apply(request.getStoreId(), request.getCustomerId(), GaoPointLedger.ChangeType.ADJUST, request.getPointDelta(), null, null, null, null, idempotencyKey, request.getRemark(), operatorUserId, Time.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<GaoPointLedger> pageByQuery(GaoPointLedgerPage page) {
|
||||
TimiException.required(page, "not found page");
|
||||
PageResult<GaoPointLedger> result = new PageResult<>();
|
||||
result.setTotal(mapper.countByQuery(page));
|
||||
result.setList(mapper.selectByQuery(page));
|
||||
return result;
|
||||
}
|
||||
|
||||
private GaoPointLedger apply(String storeId, String customerId, GaoPointLedger.ChangeType changeType, long pointDelta, String ruleId, String eventId, String eventRecordId, JsonNode sourcePayload, String idempotencyKey, String remark, String operatorUserId, Long occurredAt) {
|
||||
GaoPointLedger existing = mapper.selectByIdempotencyKey(idempotencyKey);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
GaoPointAccount account = accountService.getOrCreateForUpdate(storeId, customerId);
|
||||
existing = mapper.selectByIdempotencyKey(idempotencyKey);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
long balanceBefore = account.getBalance();
|
||||
long balanceAfter = Math.addExact(balanceBefore, pointDelta);
|
||||
long totalEarnedDelta = changeType == GaoPointLedger.ChangeType.EARN ? pointDelta : 0L;
|
||||
long totalRevokedDelta = changeType == GaoPointLedger.ChangeType.REVOKE ? -pointDelta : 0L;
|
||||
long totalAdjustedDelta = changeType == GaoPointLedger.ChangeType.ADJUST ? pointDelta : 0L;
|
||||
|
||||
GaoPointLedger ledger = new GaoPointLedger();
|
||||
ledger.setAccountId(account.getId());
|
||||
ledger.setStoreId(storeId);
|
||||
ledger.setCustomerId(customerId);
|
||||
ledger.setChangeType(changeType);
|
||||
ledger.setDirection(0 < pointDelta ? GaoPointLedger.Direction.INCREASE : GaoPointLedger.Direction.DECREASE);
|
||||
ledger.setPointDelta(pointDelta);
|
||||
ledger.setBalanceBefore(balanceBefore);
|
||||
ledger.setBalanceAfter(balanceAfter);
|
||||
ledger.setRuleId(ruleId);
|
||||
ledger.setEventId(eventId);
|
||||
ledger.setEventRecordId(eventRecordId);
|
||||
ledger.setSourcePayload(sourcePayload);
|
||||
ledger.setIdempotencyKey(idempotencyKey);
|
||||
ledger.setRemark(remark);
|
||||
ledger.setOperatorUserId(operatorUserId);
|
||||
ledger.setOccurredAt(occurredAt);
|
||||
create(ledger);
|
||||
int updated = accountService.updateBalance(account.getId(), pointDelta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta);
|
||||
TimiException.requiredTrue(0 < updated, "积分余额不足");
|
||||
return ledger;
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package com.imyeyu.api.modules.gao.service.implement;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoEvent;
|
||||
import com.imyeyu.api.modules.gao.mapper.GaoPointRuleMapper;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoEventService;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import com.imyeyu.spring.service.AbstractEntityService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// GAO 客户积分规则服务实现
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPointRule, String> implements GaoPointRuleService {
|
||||
|
||||
private final GaoPointRuleMapper mapper;
|
||||
private final GaoEventService eventService;
|
||||
|
||||
@Override
|
||||
protected BaseMapper<GaoPointRule, String> mapper() {
|
||||
return mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GaoPointRule> listActive(String storeId, String eventId, long now) {
|
||||
TimiException.required(storeId, "not found storeId");
|
||||
TimiException.required(eventId, "not found eventId");
|
||||
return mapper.selectActiveByStoreIdAndEventId(storeId, eventId, now);
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void create(GaoPointRule rule) {
|
||||
TimiException.required(rule, "not found rule");
|
||||
TimiException.required(rule.getStoreId(), "not found rule.storeId");
|
||||
TimiException.required(rule.getEventId(), "not found rule.eventId");
|
||||
TimiException.required(rule.getName(), "not found rule.name");
|
||||
checkEvent(rule.getStoreId(), rule.getEventId());
|
||||
rule.setStatus(TimiJava.defaultIfNull(rule.getStatus(), GaoPointRule.Status.DRAFT));
|
||||
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
|
||||
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
|
||||
verifyRule(rule);
|
||||
super.create(rule);
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void update(GaoPointRule rule) {
|
||||
TimiException.required(rule, "not found rule");
|
||||
TimiException.required(rule.getId(), "not found rule.id");
|
||||
TimiException.required(rule.getStoreId(), "not found rule.storeId");
|
||||
TimiException.required(rule.getEventId(), "not found rule.eventId");
|
||||
TimiException.required(rule.getName(), "not found rule.name");
|
||||
GaoPointRule dbRule = get(rule.getId());
|
||||
TimiException.required(dbRule, "not found rule");
|
||||
TimiException.requiredTrue(dbRule.getStoreId().equals(rule.getStoreId()), "rule.storeId invalid");
|
||||
checkEvent(rule.getStoreId(), rule.getEventId());
|
||||
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
|
||||
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
|
||||
verifyRule(rule);
|
||||
dbRule.setEventId(rule.getEventId());
|
||||
dbRule.setName(rule.getName());
|
||||
dbRule.setStatus(rule.getStatus());
|
||||
dbRule.setTriggerType(rule.getTriggerType());
|
||||
dbRule.setPointMode(rule.getPointMode());
|
||||
dbRule.setPointValue(rule.getPointValue());
|
||||
dbRule.setConditionJson(rule.getConditionJson());
|
||||
dbRule.setBeginAt(rule.getBeginAt());
|
||||
dbRule.setEndAt(rule.getEndAt());
|
||||
dbRule.setPriority(rule.getPriority());
|
||||
dbRule.setExclusive(rule.getExclusive());
|
||||
dbRule.setRemark(rule.getRemark());
|
||||
mapper.update(dbRule);
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
TimiException.required(id, "not found rule.id");
|
||||
GaoPointRule rule = get(id);
|
||||
TimiException.required(rule, "not found rule");
|
||||
rule.setStatus(GaoPointRule.Status.DELETED);
|
||||
super.update(rule);
|
||||
super.delete(id);
|
||||
}
|
||||
|
||||
private void checkEvent(String storeId, String eventId) {
|
||||
GaoEvent event = eventService.get(eventId);
|
||||
TimiException.required(event, "not found event");
|
||||
TimiException.requiredTrue(storeId.equals(event.getStoreId()), "规则和登记事件不属于同一门店");
|
||||
}
|
||||
|
||||
private void verifyRule(GaoPointRule rule) {
|
||||
TimiException.required(rule.getStatus(), "not found rule.status");
|
||||
TimiException.required(rule.getTriggerType(), "not found rule.triggerType");
|
||||
TimiException.required(rule.getPointMode(), "not found rule.pointMode");
|
||||
TimiException.requiredTrue(rule.getPriority() != null && 0 <= rule.getPriority(), "rule.priority invalid");
|
||||
TimiException.requiredTrue(rule.getBeginAt() == null || rule.getEndAt() == null || rule.getBeginAt() <= rule.getEndAt(), "rule.beginAt or rule.endAt invalid");
|
||||
|
||||
if (rule.getTriggerType() == GaoPointRule.TriggerType.SOURCE_VALUE) {
|
||||
TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.SOURCE_VALUE, "SOURCE_VALUE 规则必须使用 SOURCE_VALUE 积分值模式");
|
||||
TimiException.requiredNull(rule.getPointValue(), "SOURCE_VALUE 规则不能填写固定积分值");
|
||||
verifySourceCondition(rule.getConditionJson());
|
||||
return;
|
||||
}
|
||||
|
||||
TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.FIXED, "固定触发规则必须使用 FIXED 积分值模式");
|
||||
TimiException.required(rule.getPointValue(), "not found rule.pointValue");
|
||||
TimiException.requiredTrue(0 < rule.getPointValue(), "rule.pointValue invalid");
|
||||
if (rule.getTriggerType() == GaoPointRule.TriggerType.AFTER_COUNT_EVERY_TIME) {
|
||||
JsonNode condition = requireObjectCondition(rule.getConditionJson());
|
||||
JsonNode threshold = condition.get("threshold");
|
||||
TimiException.requiredTrue(threshold != null && threshold.isIntegralNumber() && 0 < threshold.longValue(), "AFTER_COUNT_EVERY_TIME.threshold invalid");
|
||||
JsonNode includeCurrent = condition.get("includeCurrent");
|
||||
TimiException.requiredTrue(includeCurrent == null || includeCurrent.isNull() || includeCurrent.isBoolean(), "AFTER_COUNT_EVERY_TIME.includeCurrent invalid");
|
||||
verifyScope(condition);
|
||||
} else if (rule.getTriggerType() == GaoPointRule.TriggerType.FIRST_TIME) {
|
||||
verifyScope(requireObjectCondition(rule.getConditionJson()));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifySourceCondition(JsonNode conditionJson) {
|
||||
JsonNode condition = conditionJson == null ? null : requireObjectCondition(conditionJson);
|
||||
Long min = condition == null || condition.get("min") == null || condition.get("min").isNull() ? null : condition.get("min").isIntegralNumber() ? condition.get("min").longValue() : null;
|
||||
Long max = condition == null || condition.get("max") == null || condition.get("max").isNull() ? null : condition.get("max").isIntegralNumber() ? condition.get("max").longValue() : null;
|
||||
TimiException.requiredTrue(condition == null || min != null || condition.get("min") == null || condition.get("min").isNull(), "SOURCE_VALUE.min invalid");
|
||||
TimiException.requiredTrue(condition == null || max != null || condition.get("max") == null || condition.get("max").isNull(), "SOURCE_VALUE.max invalid");
|
||||
TimiException.requiredTrue(min == null || 0 < min, "SOURCE_VALUE.min invalid");
|
||||
TimiException.requiredTrue(max == null || 0 < max, "SOURCE_VALUE.max invalid");
|
||||
TimiException.requiredTrue(min == null || max == null || min <= max, "SOURCE_VALUE.min or SOURCE_VALUE.max invalid");
|
||||
}
|
||||
|
||||
private JsonNode requireObjectCondition(JsonNode conditionJson) {
|
||||
TimiException.requiredTrue(conditionJson != null && conditionJson.isObject(), "rule.conditionJson must be object");
|
||||
return conditionJson;
|
||||
}
|
||||
|
||||
private void verifyScope(JsonNode condition) {
|
||||
JsonNode scope = condition.get("scope");
|
||||
TimiException.requiredTrue(scope == null || scope.isNull() || "CUSTOMER_EVENT".equals(scope.asText()), "rule.conditionJson.scope invalid");
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package com.imyeyu.api.modules.gao.service.implement;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.imyeyu.api.bean.ModuleCode;
|
||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
||||
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper;
|
||||
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
||||
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
||||
import com.imyeyu.api.modules.user.service.PermissionChecker;
|
||||
import com.imyeyu.api.modules.user.service.UserLoginService;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.utils.Time;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/// GAO 客户积分触发服务实现
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
||||
|
||||
private final ObjectMapper jackson;
|
||||
private final PermissionChecker permissionChecker;
|
||||
private final UserLoginService userLoginService;
|
||||
private final GaoPointRuleService ruleService;
|
||||
private final GaoPointLedgerService ledgerService;
|
||||
private final GaoCustomerService customerService;
|
||||
private final GaoPointLedgerMapper ledgerMapper;
|
||||
private final GaoEventRecordMapper eventRecordMapper;
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void onEventRecordCreated(GaoEventRecord record) {
|
||||
TimiException.required(record, "not found event record");
|
||||
TimiException.required(record.getStoreId(), "not found record.storeId");
|
||||
TimiException.required(record.getCustomerId(), "not found record.customerId");
|
||||
TimiException.required(record.getEventId(), "not found record.eventId");
|
||||
|
||||
List<GaoPointRule> ruleList = ruleService.listActive(record.getStoreId(), record.getEventId(), Time.now());
|
||||
Long count = null;
|
||||
for (GaoPointRule rule : ruleList) {
|
||||
Long pointDelta = resolvePointDelta(record, rule, count);
|
||||
if (pointDelta == null) {
|
||||
continue;
|
||||
}
|
||||
if (count == null) {
|
||||
count = eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null);
|
||||
}
|
||||
JsonNode sourcePayload = sourcePayload(record, rule, pointDelta);
|
||||
ledgerService.earn(record, rule, pointDelta, sourcePayload);
|
||||
if (Boolean.TRUE.equals(rule.getExclusive())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||
@Override
|
||||
public void revokeByEventRecord(GaoEventRecord record) {
|
||||
TimiException.required(record, "not found event record");
|
||||
List<GaoPointLedger> originLedgerList = ledgerMapper.selectEarnByEventRecordId(record.getId());
|
||||
if (originLedgerList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> revokedKeySet = new HashSet<>();
|
||||
List<String> revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList();
|
||||
for (GaoPointLedger ledger : ledgerMapper.selectByIdempotencyKeyList(revokeKeyList)) {
|
||||
revokedKeySet.add(ledger.getIdempotencyKey());
|
||||
}
|
||||
for (GaoPointLedger originLedger : originLedgerList) {
|
||||
if (!revokedKeySet.contains(revokeKey(record, originLedger))) {
|
||||
ledgerService.revoke(originLedger, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GaoPointLedger adjust(GaoPointAdjustRequest request) {
|
||||
TimiException.required(request, "not found adjust request");
|
||||
TimiException.required(request.getStoreId(), "not found storeId");
|
||||
TimiException.required(request.getCustomerId(), "not found customerId");
|
||||
GaoCustomer customer = customerService.get(request.getCustomerId());
|
||||
TimiException.required(customer, "not found customer");
|
||||
TimiException.requiredTrue(request.getStoreId().equals(customer.getStoreId()), "客户和积分调整门店不一致");
|
||||
return ledgerService.adjust(request, userLoginService.getRequireLoginUserId());
|
||||
}
|
||||
|
||||
private Long resolvePointDelta(GaoEventRecord record, GaoPointRule rule, Long count) {
|
||||
return switch (rule.getTriggerType()) {
|
||||
case EVERY_TIME -> rule.getPointValue();
|
||||
case FIRST_TIME -> {
|
||||
long currentCount = count == null ? count(record) : count;
|
||||
yield currentCount == 1 ? rule.getPointValue() : null;
|
||||
}
|
||||
case AFTER_COUNT_EVERY_TIME -> {
|
||||
JsonNode condition = rule.getConditionJson();
|
||||
long threshold = condition.get("threshold").longValue();
|
||||
boolean includeCurrent = condition.get("includeCurrent") == null || condition.get("includeCurrent").isNull() || condition.get("includeCurrent").asBoolean();
|
||||
long currentCount = count == null ? count(record) : count;
|
||||
yield includeCurrent ? threshold <= currentCount ? rule.getPointValue() : null : threshold < currentCount ? rule.getPointValue() : null;
|
||||
}
|
||||
case SOURCE_VALUE -> resolveSourceValue(record, rule);
|
||||
};
|
||||
}
|
||||
|
||||
private Long resolveSourceValue(GaoEventRecord record, GaoPointRule rule) {
|
||||
if (record.getPointValue() == null) {
|
||||
return null;
|
||||
}
|
||||
permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.POINT_SOURCE_VALUE.getValue());
|
||||
long pointValue = record.getPointValue();
|
||||
TimiException.requiredTrue(0 < pointValue, "pointValue invalid");
|
||||
JsonNode condition = rule.getConditionJson();
|
||||
if (condition != null) {
|
||||
JsonNode min = condition.get("min");
|
||||
JsonNode max = condition.get("max");
|
||||
TimiException.requiredTrue(min == null || min.isNull() || min.longValue() <= pointValue, "pointValue 小于规则最小值");
|
||||
TimiException.requiredTrue(max == null || max.isNull() || pointValue <= max.longValue(), "pointValue 大于规则最大值");
|
||||
}
|
||||
return pointValue;
|
||||
}
|
||||
|
||||
private JsonNode sourcePayload(GaoEventRecord record, GaoPointRule rule, long pointDelta) {
|
||||
if (rule.getTriggerType() != GaoPointRule.TriggerType.SOURCE_VALUE) {
|
||||
return null;
|
||||
}
|
||||
ObjectNode payload = jackson.createObjectNode();
|
||||
payload.put("submittedPointValue", record.getPointValue());
|
||||
payload.put("validatedPointValue", pointDelta);
|
||||
return payload;
|
||||
}
|
||||
|
||||
private long count(GaoEventRecord record) {
|
||||
return TimiJava.defaultIfNull(eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null), 0L);
|
||||
}
|
||||
|
||||
private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) {
|
||||
return "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.imyeyu.api.modules.gao.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/// GAO 客户积分人工调整请求
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Data
|
||||
public class GaoPointAdjustRequest {
|
||||
|
||||
/// 人工调整请求 ID,用于生成幂等键
|
||||
private String adjustRequestId;
|
||||
|
||||
/// 门店 ID
|
||||
private String storeId;
|
||||
|
||||
/// 客户 ID
|
||||
private String customerId;
|
||||
|
||||
/// 调整积分,正数增加,负数减少
|
||||
private Long pointDelta;
|
||||
|
||||
/// 调整原因
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.imyeyu.api.modules.gao.vo;
|
||||
|
||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||
import com.imyeyu.java.bean.BasePage;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/// GAO 客户积分流水查询请求
|
||||
///
|
||||
/// @author Codex
|
||||
/// @since 2026-08-11
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GaoPointLedgerPage extends BasePage {
|
||||
|
||||
/// 门店 ID
|
||||
private String storeId;
|
||||
|
||||
/// 客户 ID
|
||||
private String customerId;
|
||||
|
||||
/// 客户积分账户 ID
|
||||
private String accountId;
|
||||
|
||||
/// 流水变化类型
|
||||
private GaoPointLedger.ChangeType changeType;
|
||||
|
||||
/// 开始发生时间
|
||||
private Long beginAt;
|
||||
|
||||
/// 结束发生时间
|
||||
private Long endAt;
|
||||
}
|
||||
Reference in New Issue
Block a user