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),
|
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),
|
STAT_READ("STAT:READ", "统计读取", false, false),
|
||||||
|
|
||||||
QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建", false, false),
|
QR_CODE_CREATE("QR_CODE:CREATE", "二维码创建", false, false),
|
||||||
@@ -103,5 +119,4 @@ public enum GaoPermissionCode implements BuiltinPermissionCode {
|
|||||||
public boolean isGrantToAdmin() {
|
public boolean isGrantToAdmin() {
|
||||||
return false;
|
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;
|
private Long businessDayNo;
|
||||||
|
|
||||||
|
/// 本次登记提交的积分值,只参与积分触发,不落库
|
||||||
|
@Transient
|
||||||
|
private Long pointValue;
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
private GaoCustomer customer;
|
private GaoCustomer customer;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.entity;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.entity.UUIDEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/// GAO 客户积分账户
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-08-11
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class GaoPointAccount extends UUIDEntity {
|
||||||
|
|
||||||
|
/// 门店 ID
|
||||||
|
private String storeId;
|
||||||
|
|
||||||
|
/// 客户 ID
|
||||||
|
private String customerId;
|
||||||
|
|
||||||
|
/// 当前可用积分余额
|
||||||
|
private Long balance;
|
||||||
|
|
||||||
|
/// 累计获得积分
|
||||||
|
private Long totalEarned;
|
||||||
|
|
||||||
|
/// 累计撤销积分
|
||||||
|
private Long totalRevoked;
|
||||||
|
|
||||||
|
/// 累计人工调整积分,可正可负
|
||||||
|
private Long totalAdjusted;
|
||||||
|
|
||||||
|
/// 乐观锁版本
|
||||||
|
private Long version;
|
||||||
|
}
|
||||||
@@ -0,0 +1,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.entity.GaoEventRecord;
|
||||||
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
|
import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoEventService;
|
import com.imyeyu.api.modules.gao.service.GaoEventService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
|
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
|
||||||
@@ -63,6 +64,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
private final AttachmentService attachmentService;
|
private final AttachmentService attachmentService;
|
||||||
private final GaoCustomerService customerService;
|
private final GaoCustomerService customerService;
|
||||||
private final GaoStoreBusinessDayService businessDayService;
|
private final GaoStoreBusinessDayService businessDayService;
|
||||||
|
private final GaoPointTriggerService pointTriggerService;
|
||||||
|
|
||||||
private final GaoEventRecordMapper mapper;
|
private final GaoEventRecordMapper mapper;
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
dbRecord.setRegisteredAt(registeredAt);
|
dbRecord.setRegisteredAt(registeredAt);
|
||||||
dbRecord.setRegisteredDateValue(registeredDateValue);
|
dbRecord.setRegisteredDateValue(registeredDateValue);
|
||||||
dbRecord.setBusinessDayNo(businessDayNo);
|
dbRecord.setBusinessDayNo(businessDayNo);
|
||||||
|
dbRecord.setPointValue(record.getPointValue());
|
||||||
super.create(dbRecord);
|
super.create(dbRecord);
|
||||||
// 附件
|
// 附件
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
@@ -131,6 +134,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
attachmentService.update(clone);
|
attachmentService.update(clone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pointTriggerService.onEventRecordCreated(dbRecord);
|
||||||
logIdList.add(dbRecord.getId());
|
logIdList.add(dbRecord.getId());
|
||||||
}
|
}
|
||||||
logger.setLevel(Logger.Level.INFO);
|
logger.setLevel(Logger.Level.INFO);
|
||||||
@@ -149,6 +153,14 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void createBatch(List<GaoEventRecord> recordList) {
|
||||||
|
for (GaoEventRecord record : TimiJava.safeIterable(recordList)) {
|
||||||
|
create(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void update(GaoEventRecord record) {
|
public void update(GaoEventRecord record) {
|
||||||
@@ -193,6 +205,33 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void delete(String id) {
|
||||||
|
TimiException.required(id, "not found record.id");
|
||||||
|
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_DELETE");
|
||||||
|
try {
|
||||||
|
logger.setContent(id);
|
||||||
|
GaoEventRecord record = get(id);
|
||||||
|
TimiException.required(record, "not found event record");
|
||||||
|
pointTriggerService.revokeByEventRecord(record);
|
||||||
|
super.delete(id);
|
||||||
|
logger.setLevel(Logger.Level.INFO);
|
||||||
|
logger.setResult(id);
|
||||||
|
} catch (TimiException e) {
|
||||||
|
logger.setLevel(Logger.Level.WARN);
|
||||||
|
logger.setException(e.getMessage());
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.setLevel(Logger.Level.ERROR);
|
||||||
|
logger.setException(TimiJava.serializeThrowable(e));
|
||||||
|
log.error("删除 GAO 登记记录失败", e);
|
||||||
|
throw new TimiException(TimiCode.ERROR, "删除 GAO 登记记录失败", e);
|
||||||
|
} finally {
|
||||||
|
loggerService.create(logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void createWithCustomer(GaoCustomer customer) {
|
public void createWithCustomer(GaoCustomer customer) {
|
||||||
|
|||||||
+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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `gao_point_account` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
|
||||||
|
`customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID',
|
||||||
|
`active_customer_id` VARCHAR(36) GENERATED ALWAYS AS (
|
||||||
|
CASE WHEN `deleted_at` IS NULL THEN `customer_id` END
|
||||||
|
) STORED COMMENT '未删除客户 ID' INVISIBLE,
|
||||||
|
`balance` BIGINT NOT NULL DEFAULT 0 COMMENT '当前可用积分余额',
|
||||||
|
`total_earned` BIGINT NOT NULL DEFAULT 0 COMMENT '累计获得积分',
|
||||||
|
`total_revoked` BIGINT NOT NULL DEFAULT 0 COMMENT '累计撤销积分',
|
||||||
|
`total_adjusted` BIGINT NOT NULL DEFAULT 0 COMMENT '累计人工调整积分',
|
||||||
|
`version` BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||||
|
`created_at` BIGINT NOT NULL,
|
||||||
|
`updated_at` BIGINT NULL,
|
||||||
|
`deleted_at` BIGINT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_gao_point_account_customer` (`store_id`, `active_customer_id`),
|
||||||
|
KEY `idx_gao_point_account_customer_id` (`customer_id`),
|
||||||
|
KEY `idx_gao_point_account_store_balance` (`store_id`, `deleted_at`, `balance`)
|
||||||
|
) COMMENT='GAO 客户积分账户';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `gao_point_rule` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
|
||||||
|
`event_id` VARCHAR(36) NOT NULL COMMENT '登记事件 ID',
|
||||||
|
`name` VARCHAR(64) NOT NULL COMMENT '规则名称',
|
||||||
|
`status` VARCHAR(32) NOT NULL COMMENT '规则状态',
|
||||||
|
`trigger_type` VARCHAR(32) NOT NULL COMMENT '触发类型',
|
||||||
|
`point_mode` VARCHAR(32) NOT NULL COMMENT '积分值模式',
|
||||||
|
`point_value` BIGINT NULL COMMENT '固定积分值',
|
||||||
|
`condition_json` JSON NULL COMMENT '条件配置',
|
||||||
|
`begin_at` BIGINT NULL COMMENT '生效开始时间戳,毫秒',
|
||||||
|
`end_at` BIGINT NULL COMMENT '生效结束时间戳,毫秒',
|
||||||
|
`priority` INT NOT NULL DEFAULT 0 COMMENT '优先级,数值小优先',
|
||||||
|
`exclusive` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'true 为命中后停止继续匹配后续规则',
|
||||||
|
`remark` TEXT NULL COMMENT '备注',
|
||||||
|
`created_at` BIGINT NOT NULL,
|
||||||
|
`updated_at` BIGINT NULL,
|
||||||
|
`deleted_at` BIGINT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_gao_point_rule_match` (`store_id`, `event_id`, `status`, `deleted_at`, `priority`),
|
||||||
|
KEY `idx_gao_point_rule_type` (`trigger_type`, `point_mode`)
|
||||||
|
) COMMENT='GAO 客户积分规则';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `gao_point_ledger` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`account_id` VARCHAR(36) NOT NULL COMMENT '客户积分账户 ID',
|
||||||
|
`store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
|
||||||
|
`customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID',
|
||||||
|
`change_type` VARCHAR(32) NOT NULL COMMENT '变化类型',
|
||||||
|
`direction` VARCHAR(16) NOT NULL COMMENT '方向',
|
||||||
|
`point_delta` BIGINT NOT NULL COMMENT '本次变化积分,增加为正,减少为负',
|
||||||
|
`balance_before` BIGINT NOT NULL COMMENT '变化前余额',
|
||||||
|
`balance_after` BIGINT NOT NULL COMMENT '变化后余额',
|
||||||
|
`rule_id` VARCHAR(36) NULL COMMENT '积分规则 ID',
|
||||||
|
`event_id` VARCHAR(36) NULL COMMENT '登记事件 ID',
|
||||||
|
`event_record_id` VARCHAR(36) NULL COMMENT '登记记录 ID',
|
||||||
|
`source_payload` JSON NULL COMMENT '来源快照',
|
||||||
|
`idempotency_key` VARCHAR(128) NOT NULL COMMENT '幂等键',
|
||||||
|
`remark` TEXT NULL COMMENT '备注',
|
||||||
|
`operator_user_id` VARCHAR(36) NULL COMMENT '操作用户 ID',
|
||||||
|
`occurred_at` BIGINT NOT NULL COMMENT '发生时间',
|
||||||
|
`created_at` BIGINT NOT NULL,
|
||||||
|
`updated_at` BIGINT NULL,
|
||||||
|
`deleted_at` BIGINT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_gao_point_ledger_idempotency` (`idempotency_key`),
|
||||||
|
KEY `idx_gao_point_ledger_account_time` (`account_id`, `deleted_at`, `occurred_at`),
|
||||||
|
KEY `idx_gao_point_ledger_customer_time` (`store_id`, `customer_id`, `deleted_at`, `occurred_at`),
|
||||||
|
KEY `idx_gao_point_ledger_event_record` (`event_record_id`, `change_type`),
|
||||||
|
KEY `idx_gao_point_ledger_rule_time` (`rule_id`, `occurred_at`)
|
||||||
|
) COMMENT='GAO 客户积分流水';
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
|
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper">
|
||||||
|
<select id="selectByStoreIdAndCustomerId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointAccount">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_account`
|
||||||
|
WHERE `store_id` = #{storeId}
|
||||||
|
AND `customer_id` = #{customerId}
|
||||||
|
AND `deleted_at` IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertIfAbsent">
|
||||||
|
INSERT INTO `gao_point_account` (
|
||||||
|
`id`, `store_id`, `customer_id`, `balance`, `total_earned`, `total_revoked`, `total_adjusted`, `version`, `created_at`, `updated_at`, `deleted_at`
|
||||||
|
) VALUES (
|
||||||
|
#{account.id}, #{account.storeId}, #{account.customerId}, #{account.balance}, #{account.totalEarned}, #{account.totalRevoked}, #{account.totalAdjusted}, #{account.version}, #{account.createdAt}, #{account.updatedAt}, #{account.deletedAt}
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE `id` = `id`
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoPointAccount">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_account`
|
||||||
|
WHERE `id` = #{id}
|
||||||
|
AND `deleted_at` IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="updateBalance">
|
||||||
|
UPDATE `gao_point_account`
|
||||||
|
SET
|
||||||
|
`balance` = `balance` + #{delta},
|
||||||
|
`total_earned` = `total_earned` + #{totalEarnedDelta},
|
||||||
|
`total_revoked` = `total_revoked` + #{totalRevokedDelta},
|
||||||
|
`total_adjusted` = `total_adjusted` + #{totalAdjustedDelta},
|
||||||
|
`version` = `version` + 1,
|
||||||
|
`updated_at` = #{updatedAt}
|
||||||
|
WHERE `id` = #{id}
|
||||||
|
AND `balance` + #{delta} >= 0
|
||||||
|
AND `deleted_at` IS NULL
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
|
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper">
|
||||||
|
<select id="selectByIdempotencyKey" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_ledger`
|
||||||
|
WHERE `idempotency_key` = #{idempotencyKey}
|
||||||
|
AND `deleted_at` IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectEarnByEventRecordId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_ledger`
|
||||||
|
WHERE `event_record_id` = #{eventRecordId}
|
||||||
|
AND `change_type` = 'EARN'
|
||||||
|
AND `deleted_at` IS NULL
|
||||||
|
ORDER BY `created_at` ASC, `id` ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByIdempotencyKeyList" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_ledger`
|
||||||
|
WHERE `deleted_at` IS NULL
|
||||||
|
AND `idempotency_key` IN
|
||||||
|
<foreach collection="idempotencyKeyList" item="idempotencyKey" open="(" separator="," close=")">
|
||||||
|
#{idempotencyKey}
|
||||||
|
</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<sql id="queryWhere">
|
||||||
|
`deleted_at` IS NULL
|
||||||
|
<if test="storeId != null and storeId != ''">
|
||||||
|
AND `store_id` = #{storeId}
|
||||||
|
</if>
|
||||||
|
<if test="customerId != null and customerId != ''">
|
||||||
|
AND `customer_id` = #{customerId}
|
||||||
|
</if>
|
||||||
|
<if test="accountId != null and accountId != ''">
|
||||||
|
AND `account_id` = #{accountId}
|
||||||
|
</if>
|
||||||
|
<if test="changeType != null">
|
||||||
|
AND `change_type` = #{changeType}
|
||||||
|
</if>
|
||||||
|
<if test="beginAt != null">
|
||||||
|
AND `occurred_at` >= #{beginAt}
|
||||||
|
</if>
|
||||||
|
<if test="endAt != null">
|
||||||
|
AND `occurred_at` <= #{endAt}
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="countByQuery" resultType="long">
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM `gao_point_ledger`
|
||||||
|
WHERE
|
||||||
|
<include refid="queryWhere"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_ledger`
|
||||||
|
WHERE
|
||||||
|
<include refid="queryWhere"/>
|
||||||
|
ORDER BY `occurred_at` DESC, `created_at` DESC
|
||||||
|
LIMIT #{offset}, #{limit}
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
|
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoPointRuleMapper">
|
||||||
|
<select id="selectActiveByStoreIdAndEventId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointRule">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_rule`
|
||||||
|
WHERE `store_id` = #{storeId}
|
||||||
|
AND `event_id` = #{eventId}
|
||||||
|
AND `status` = 'ACTIVE'
|
||||||
|
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
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
Reference in New Issue
Block a user