add groovy script
This commit is contained in:
@@ -180,6 +180,11 @@
|
|||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.groovy</groupId>
|
||||||
|
<artifactId>groovy</artifactId>
|
||||||
|
<version>4.0.29</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.imyeyu.spring</groupId>
|
<groupId>com.imyeyu.spring</groupId>
|
||||||
<artifactId>timi-spring</artifactId>
|
<artifactId>timi-spring</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.imyeyu.api.modules.common.bean;
|
||||||
|
|
||||||
|
/// Groovy 脚本执行结果
|
||||||
|
///
|
||||||
|
/// @param <T> 脚本返回值类型
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public record GroovyScriptExecutionResult<T>(String scriptId, String versionId, Integer version, String checksum, T result) {
|
||||||
|
}
|
||||||
@@ -48,6 +48,8 @@ public class Attachment extends UUIDEntity {
|
|||||||
|
|
||||||
GAO_ORDER_FULFILLMENT,
|
GAO_ORDER_FULFILLMENT,
|
||||||
|
|
||||||
|
GAO_SALES_ORDER,
|
||||||
|
|
||||||
TEMP_FILE,
|
TEMP_FILE,
|
||||||
|
|
||||||
MIRROR
|
MIRROR
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.imyeyu.api.modules.common.entity;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.entity.UUIDEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/// Groovy 脚本
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class GroovyScript extends UUIDEntity {
|
||||||
|
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// @author 夜雨
|
||||||
|
/// @since 2026/9/4 14:54
|
||||||
|
public enum BizType {
|
||||||
|
|
||||||
|
/// GAO 积分规则
|
||||||
|
GAO_POINT_RULE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 业务归属
|
||||||
|
private BizType bizType;
|
||||||
|
|
||||||
|
/// 业务内唯一代码
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
/// 名称
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/// 说明
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/// true 为允许执行已发布版本
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
/// 当前发布版本 ID
|
||||||
|
private String publishedVersionId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.imyeyu.api.modules.common.entity;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.entity.UUIDEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/// Groovy 脚本版本
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class GroovyScriptVersion extends UUIDEntity {
|
||||||
|
|
||||||
|
/// 脚本 ID
|
||||||
|
private String scriptId;
|
||||||
|
|
||||||
|
/// 版本号
|
||||||
|
private Integer version;
|
||||||
|
|
||||||
|
/// 脚本内容
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/// 脚本内容 SHA-256
|
||||||
|
private String checksum;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.imyeyu.api.modules.common.mapper;
|
||||||
|
|
||||||
|
import com.imyeyu.api.modules.common.entity.GroovyScript;
|
||||||
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
|
|
||||||
|
/// Groovy 脚本 Mapper
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public interface GroovyScriptMapper extends BaseMapper<GroovyScript, String> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.imyeyu.api.modules.common.mapper;
|
||||||
|
|
||||||
|
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
|
||||||
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
|
|
||||||
|
/// Groovy 脚本版本 Mapper
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public interface GroovyScriptVersionMapper extends BaseMapper<GroovyScriptVersion, String> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package com.imyeyu.api.modules.common.service;
|
||||||
|
|
||||||
|
import com.imyeyu.api.modules.common.entity.GroovyScript;
|
||||||
|
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
|
||||||
|
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
|
||||||
|
import com.imyeyu.spring.service.BaseService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/// Groovy 脚本服务
|
||||||
|
///
|
||||||
|
/// 脚本仅限可信管理员维护,不能作为不可信用户代码的执行环境
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public interface GroovyScriptService extends BaseService<GroovyScript, String> {
|
||||||
|
|
||||||
|
/// 按业务归属和代码查询脚本
|
||||||
|
///
|
||||||
|
/// @param bizType 业务归属
|
||||||
|
/// @param code 业务内唯一代码
|
||||||
|
/// @return 脚本,不存在时返回 null
|
||||||
|
GroovyScript getByCode(GroovyScript.BizType bizType, String code);
|
||||||
|
|
||||||
|
/// 创建脚本元数据
|
||||||
|
///
|
||||||
|
/// 新建脚本默认停用,且没有已发布版本
|
||||||
|
///
|
||||||
|
/// @param script 脚本元数据
|
||||||
|
void create(GroovyScript script);
|
||||||
|
|
||||||
|
/// 更新脚本元数据
|
||||||
|
///
|
||||||
|
/// 业务归属和代码是脚本稳定标识,创建后不可变更
|
||||||
|
///
|
||||||
|
/// @param script 脚本元数据
|
||||||
|
void update(GroovyScript script);
|
||||||
|
|
||||||
|
/// 设置脚本启用状态
|
||||||
|
///
|
||||||
|
/// @param id 脚本 ID
|
||||||
|
/// @param enabled true 为允许执行已发布版本
|
||||||
|
void setEnabled(String id, boolean enabled);
|
||||||
|
|
||||||
|
/// 创建不可变的脚本版本
|
||||||
|
///
|
||||||
|
/// @param scriptId 脚本 ID
|
||||||
|
/// @param content 脚本内容
|
||||||
|
/// @return 新版本
|
||||||
|
GroovyScriptVersion createVersion(String scriptId, String content);
|
||||||
|
|
||||||
|
/// 查询脚本的全部版本
|
||||||
|
///
|
||||||
|
/// @param scriptId 脚本 ID
|
||||||
|
/// @return 按版本号升序的版本列表
|
||||||
|
List<GroovyScriptVersion> listVersion(String scriptId);
|
||||||
|
|
||||||
|
/// 发布指定版本
|
||||||
|
///
|
||||||
|
/// @param scriptId 脚本 ID
|
||||||
|
/// @param versionId 脚本版本 ID
|
||||||
|
void publish(String scriptId, String versionId);
|
||||||
|
|
||||||
|
/// 取消当前发布版本
|
||||||
|
///
|
||||||
|
/// @param scriptId 脚本 ID
|
||||||
|
void unpublish(String scriptId);
|
||||||
|
|
||||||
|
/// 删除脚本
|
||||||
|
///
|
||||||
|
/// @param id 脚本 ID
|
||||||
|
void delete(String id);
|
||||||
|
|
||||||
|
/// 执行已发布且启用的脚本
|
||||||
|
///
|
||||||
|
/// @param bizType 业务归属
|
||||||
|
/// @param code 业务内唯一代码
|
||||||
|
/// @param bindings 传给脚本的变量
|
||||||
|
/// @return 脚本返回值
|
||||||
|
Object execute(GroovyScript.BizType bizType, String code, Map<String, Object> bindings);
|
||||||
|
|
||||||
|
/// 执行已发布且启用的脚本并返回实际执行版本
|
||||||
|
///
|
||||||
|
/// @param bizType 业务归属
|
||||||
|
/// @param code 业务内唯一代码
|
||||||
|
/// @param bindings 传给脚本的变量
|
||||||
|
/// @param resultType 返回值类型
|
||||||
|
/// @param <T> 返回值类型
|
||||||
|
/// @return 脚本执行结果
|
||||||
|
<T> GroovyScriptExecutionResult<T> executePublished(GroovyScript.BizType bizType, String code, Map<String, Object> bindings, Class<T> resultType);
|
||||||
|
|
||||||
|
/// 按指定历史版本执行脚本
|
||||||
|
///
|
||||||
|
/// 用于按积分发生时的脚本版本复算,脚本当前是否发布或启用不影响执行;脚本返回 null 表示未命中,属合法结果
|
||||||
|
///
|
||||||
|
/// @param versionId 脚本版本 ID
|
||||||
|
/// @param bindings 传给脚本的变量
|
||||||
|
/// @param resultType 返回值类型
|
||||||
|
/// @param <T> 返回值类型
|
||||||
|
/// @return 脚本执行结果
|
||||||
|
<T> GroovyScriptExecutionResult<T> executeVersion(String versionId, Map<String, Object> bindings, Class<T> resultType);
|
||||||
|
|
||||||
|
/// 执行已发布且启用的脚本并验证返回值类型
|
||||||
|
///
|
||||||
|
/// @param bizType 业务归属
|
||||||
|
/// @param code 业务内唯一代码
|
||||||
|
/// @param bindings 传给脚本的变量
|
||||||
|
/// @param resultType 返回值类型
|
||||||
|
/// @param <T> 返回值类型
|
||||||
|
/// @return 脚本返回值
|
||||||
|
<T> T execute(GroovyScript.BizType bizType, String code, Map<String, Object> bindings, Class<T> resultType);
|
||||||
|
|
||||||
|
/// 清除脚本的本地编译缓存
|
||||||
|
///
|
||||||
|
/// 多实例部署时,其他节点在收到脚本变更通知后调用此方法
|
||||||
|
///
|
||||||
|
/// @param scriptId 脚本 ID
|
||||||
|
void invalidate(String scriptId);
|
||||||
|
}
|
||||||
+293
@@ -0,0 +1,293 @@
|
|||||||
|
package com.imyeyu.api.modules.common.service.implement;
|
||||||
|
|
||||||
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
|
import com.imyeyu.api.modules.common.entity.GroovyScript;
|
||||||
|
import com.imyeyu.api.modules.common.entity.GroovyScriptVersion;
|
||||||
|
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
|
||||||
|
import com.imyeyu.api.modules.common.mapper.GroovyScriptMapper;
|
||||||
|
import com.imyeyu.api.modules.common.mapper.GroovyScriptVersionMapper;
|
||||||
|
import com.imyeyu.api.modules.common.service.GroovyScriptService;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
|
import com.imyeyu.spring.service.AbstractEntityService;
|
||||||
|
import groovy.lang.Binding;
|
||||||
|
import groovy.lang.GroovyClassLoader;
|
||||||
|
import groovy.lang.Script;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/// Groovy 脚本服务实现
|
||||||
|
///
|
||||||
|
/// 每个已编译版本使用独立类加载器,缓存失效后可释放对应类及其元数据
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GroovyScriptServiceImplement extends AbstractEntityService<GroovyScript, String> implements GroovyScriptService {
|
||||||
|
|
||||||
|
private final GroovyScriptMapper mapper;
|
||||||
|
private final GroovyScriptVersionMapper versionMapper;
|
||||||
|
private final Map<String, CompiledScript> compiledScriptMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BaseMapper<GroovyScript, String> mapper() {
|
||||||
|
return mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroovyScript getByCode(GroovyScript.BizType bizType, String code) {
|
||||||
|
GroovyScript example = new GroovyScript();
|
||||||
|
example.setBizType(bizType);
|
||||||
|
example.setCode(code);
|
||||||
|
return mapper.selectByExample(example);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void create(GroovyScript script) {
|
||||||
|
TimiException.required(script, "未找到脚本");
|
||||||
|
TimiException.required(script.getBizType(), "未找到脚本业务归属");
|
||||||
|
TimiException.required(script.getCode(), "未找到脚本代码");
|
||||||
|
TimiException.required(script.getName(), "未找到脚本名称");
|
||||||
|
TimiException.requiredNull(getByCode(script.getBizType(), script.getCode()), "Groovy 脚本已存在");
|
||||||
|
script.setEnabled(false);
|
||||||
|
script.setPublishedVersionId(null);
|
||||||
|
super.create(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void update(GroovyScript script) {
|
||||||
|
TimiException.required(script, "未找到脚本");
|
||||||
|
TimiException.required(script.getId(), "未找到脚本 ID");
|
||||||
|
TimiException.required(script.getName(), "未找到脚本名称");
|
||||||
|
GroovyScript dbScript = get(script.getId());
|
||||||
|
TimiException.required(dbScript, "未找到脚本");
|
||||||
|
dbScript.setName(script.getName());
|
||||||
|
dbScript.setDescription(script.getDescription());
|
||||||
|
mapper.update(dbScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void setEnabled(String id, boolean enabled) {
|
||||||
|
GroovyScript script = requireScript(id);
|
||||||
|
script.setEnabled(enabled);
|
||||||
|
mapper.update(script);
|
||||||
|
invalidateAfterCommit(script.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public GroovyScriptVersion createVersion(String scriptId, String content) {
|
||||||
|
requireScript(scriptId);
|
||||||
|
TimiException.required(content, "未找到脚本内容");
|
||||||
|
TimiException.requiredTrue(!content.isBlank(), "脚本内容不能为空");
|
||||||
|
|
||||||
|
GroovyScriptVersion version = new GroovyScriptVersion();
|
||||||
|
version.setScriptId(scriptId);
|
||||||
|
version.setVersion(nextVersion(scriptId));
|
||||||
|
version.setContent(content);
|
||||||
|
version.setChecksum(checksum(content));
|
||||||
|
versionMapper.insert(version);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GroovyScriptVersion> listVersion(String scriptId) {
|
||||||
|
TimiException.required(scriptId, "未找到脚本 ID");
|
||||||
|
GroovyScriptVersion example = new GroovyScriptVersion();
|
||||||
|
example.setScriptId(scriptId);
|
||||||
|
List<GroovyScriptVersion> versionList = new ArrayList<>(versionMapper.selectAllByExample(example));
|
||||||
|
versionList.sort(Comparator.comparing(GroovyScriptVersion::getVersion));
|
||||||
|
return versionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void publish(String scriptId, String versionId) {
|
||||||
|
GroovyScript script = requireScript(scriptId);
|
||||||
|
GroovyScriptVersion version = requireVersion(versionId);
|
||||||
|
TimiException.requiredTrue(script.getId().equals(version.getScriptId()), "脚本版本不属于当前脚本");
|
||||||
|
script.setPublishedVersionId(version.getId());
|
||||||
|
mapper.update(script);
|
||||||
|
invalidateAfterCommit(script.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void unpublish(String scriptId) {
|
||||||
|
GroovyScript script = requireScript(scriptId);
|
||||||
|
script.setPublishedVersionId(null);
|
||||||
|
mapper.update(script);
|
||||||
|
invalidateAfterCommit(script.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void delete(String id) {
|
||||||
|
requireScript(id);
|
||||||
|
super.delete(id);
|
||||||
|
invalidateAfterCommit(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(GroovyScript.BizType bizType, String code, Map<String, Object> bindings) {
|
||||||
|
return executePublished(bizType, code, bindings, Object.class).result();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> GroovyScriptExecutionResult<T> executePublished(GroovyScript.BizType bizType, String code, Map<String, Object> bindings, Class<T> resultType) {
|
||||||
|
TimiException.required(bizType, "未找到脚本业务归属");
|
||||||
|
TimiException.required(code, "未找到脚本代码");
|
||||||
|
TimiException.required(resultType, "未找到返回值类型");
|
||||||
|
GroovyScript script = getByCode(bizType, code);
|
||||||
|
TimiException.required(script, "未找到 Groovy 脚本");
|
||||||
|
TimiException.requiredTrue(Boolean.TRUE.equals(script.getEnabled()), "Groovy 脚本已停用");
|
||||||
|
TimiException.required(script.getPublishedVersionId(), "Groovy 脚本尚未发布");
|
||||||
|
GroovyScriptExecutionResult<T> result = executeVersion(script.getPublishedVersionId(), bindings, resultType);
|
||||||
|
TimiException.requiredTrue(script.getId().equals(result.scriptId()), "Groovy 脚本已发布版本无效");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T execute(GroovyScript.BizType bizType, String code, Map<String, Object> bindings, Class<T> resultType) {
|
||||||
|
return executePublished(bizType, code, bindings, resultType).result();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> GroovyScriptExecutionResult<T> executeVersion(String versionId, Map<String, Object> bindings, Class<T> resultType) {
|
||||||
|
TimiException.required(resultType, "未找到返回值类型");
|
||||||
|
GroovyScriptVersion version = requireVersion(versionId);
|
||||||
|
CompiledScript compiledScript = compiledScriptMap.computeIfAbsent(cacheKey(version), key -> compile(version.getScriptId(), version));
|
||||||
|
try {
|
||||||
|
Object result = compiledScript.execute(bindings);
|
||||||
|
TimiException.requiredTrue(result == null || resultType.isInstance(result), "Groovy 脚本返回值类型不匹配");
|
||||||
|
return new GroovyScriptExecutionResult<>(version.getScriptId(), version.getId(), version.getVersion(), version.getChecksum(), resultType.cast(result));
|
||||||
|
} catch (TimiException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.warn("Groovy 脚本执行失败,scriptId={}, versionId={}", version.getScriptId(), version.getId(), e);
|
||||||
|
throw new TimiException(TimiCode.ERROR, "Groovy 脚本执行失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidate(String scriptId) {
|
||||||
|
if (scriptId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
compiledScriptMap.entrySet().removeIf(entry -> {
|
||||||
|
if (!scriptId.equals(entry.getValue().scriptId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entry.getValue().close();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroovyScript requireScript(String id) {
|
||||||
|
TimiException.required(id, "未找到脚本 ID");
|
||||||
|
GroovyScript script = get(id);
|
||||||
|
TimiException.required(script, "未找到脚本");
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroovyScriptVersion requireVersion(String id) {
|
||||||
|
TimiException.required(id, "未找到脚本版本 ID");
|
||||||
|
GroovyScriptVersion version = versionMapper.select(id);
|
||||||
|
TimiException.required(version, "未找到脚本版本");
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nextVersion(String scriptId) {
|
||||||
|
return listVersion(scriptId).stream()
|
||||||
|
.map(GroovyScriptVersion::getVersion)
|
||||||
|
.max(Integer::compareTo)
|
||||||
|
.orElse(0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cacheKey(GroovyScriptVersion version) {
|
||||||
|
return "%s:%s".formatted(version.getId(), version.getChecksum());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompiledScript compile(String scriptId, GroovyScriptVersion version) {
|
||||||
|
GroovyClassLoader classLoader = new GroovyClassLoader(getClass().getClassLoader());
|
||||||
|
try {
|
||||||
|
Class<?> type = classLoader.parseClass(version.getContent(), "GroovyScript_%s_%d.groovy".formatted(version.getId().replace('-', '_'), version.getVersion()));
|
||||||
|
return new CompiledScript(scriptId, type.asSubclass(Script.class), classLoader);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
close(classLoader);
|
||||||
|
log.warn("Groovy 脚本编译失败,scriptId={}, versionId={}", scriptId, version.getId(), e);
|
||||||
|
throw new TimiException(TimiCode.ERROR, "Groovy 脚本编译失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String checksum(String content) {
|
||||||
|
try {
|
||||||
|
return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(content.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("JVM 缺少 SHA-256 算法", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateAfterCommit(String scriptId) {
|
||||||
|
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
invalidate(scriptId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
invalidate(scriptId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close(GroovyClassLoader classLoader) {
|
||||||
|
try {
|
||||||
|
classLoader.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("关闭 Groovy 脚本类加载器失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CompiledScript(String scriptId, Class<? extends Script> type, GroovyClassLoader classLoader) {
|
||||||
|
|
||||||
|
private Object execute(Map<String, Object> bindings) {
|
||||||
|
try {
|
||||||
|
Script script = type.getDeclaredConstructor().newInstance();
|
||||||
|
script.setBinding(new Binding(bindings == null ? Map.of() : new HashMap<>(bindings)));
|
||||||
|
return script.run();
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new IllegalStateException("Groovy 脚本实例化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() {
|
||||||
|
try {
|
||||||
|
classLoader.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,16 +65,6 @@ public class GaoStoreController {
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
@AOPLog
|
|
||||||
@RequestRateLimit
|
|
||||||
@RequireGaoPermission(GaoPermissionCode.STORE_UPDATE)
|
|
||||||
@PostMapping("/direct-product-sale-event/list")
|
|
||||||
public List<GaoEvent> listDirectProductSaleEvents(@RequestParam String storeId) {
|
|
||||||
GaoStore store = service.get(storeId);
|
|
||||||
storeScopeService.checkStore(store);
|
|
||||||
return service.listDirectProductSaleEvents(storeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AOPLog
|
@AOPLog
|
||||||
@RequestRateLimit
|
@RequestRateLimit
|
||||||
@RequireGaoPermission(GaoPermissionCode.STORE_CREATE)
|
@RequireGaoPermission(GaoPermissionCode.STORE_CREATE)
|
||||||
@@ -101,7 +91,6 @@ public class GaoStoreController {
|
|||||||
dbStore.setAddress(store.getAddress());
|
dbStore.setAddress(store.getAddress());
|
||||||
dbStore.setRemark(store.getRemark());
|
dbStore.setRemark(store.getRemark());
|
||||||
dbStore.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
|
dbStore.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
|
||||||
dbStore.setDirectProductSaleEventId(store.getDirectProductSaleEventId());
|
|
||||||
service.update(dbStore);
|
service.update(dbStore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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-09-04
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class GaoCustomerConsumptionRecord extends UUIDEntity {
|
||||||
|
|
||||||
|
/// 消费来源类型
|
||||||
|
public enum SourceType {
|
||||||
|
/// 迁移的旧消费事件
|
||||||
|
LEGACY_EVENT,
|
||||||
|
/// 销售订单全额实收
|
||||||
|
SALES_ORDER
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 消费事实状态
|
||||||
|
public enum Status {
|
||||||
|
/// 有效并参与累计
|
||||||
|
ACTIVE,
|
||||||
|
/// 已失效,不参与累计
|
||||||
|
REVOKED
|
||||||
|
}
|
||||||
|
|
||||||
|
private String storeId;
|
||||||
|
private String customerId;
|
||||||
|
private SourceType sourceType;
|
||||||
|
private String sourceId;
|
||||||
|
private String salesOrderId;
|
||||||
|
private GaoSalesOrder.OrderType orderType;
|
||||||
|
private Long amount;
|
||||||
|
private Long effectiveAt;
|
||||||
|
private Status status;
|
||||||
|
private Long revokedAt;
|
||||||
|
private String revokeReason;
|
||||||
|
private JsonNode sourcePayload;
|
||||||
|
}
|
||||||
@@ -25,7 +25,10 @@ public class GaoEvent extends UUIDEntity {
|
|||||||
GENERAL,
|
GENERAL,
|
||||||
|
|
||||||
/// 购买产品套餐
|
/// 购买产品套餐
|
||||||
PACKAGE_PURCHASE
|
PACKAGE_PURCHASE,
|
||||||
|
|
||||||
|
/// 已退役的历史消费事件,仅保留归档和溯源
|
||||||
|
LEGACY_CONSUMPTION
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ public class GaoPointLedger extends UUIDEntity {
|
|||||||
/// 流水变化类型
|
/// 流水变化类型
|
||||||
public enum ChangeType {
|
public enum ChangeType {
|
||||||
|
|
||||||
/// 登记事件发放积分
|
/// 规则发放积分
|
||||||
EARN,
|
EARN,
|
||||||
|
|
||||||
/// 登记事件消费积分
|
/// 规则扣减积分
|
||||||
CONSUME,
|
CONSUME,
|
||||||
|
|
||||||
/// 删除登记记录撤销积分
|
/// 撤销积分
|
||||||
REVOKE,
|
REVOKE,
|
||||||
|
|
||||||
/// 人工调整积分
|
/// 人工调整积分
|
||||||
@@ -40,6 +40,16 @@ public class GaoPointLedger extends UUIDEntity {
|
|||||||
DECREASE
|
DECREASE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 直接触发来源类型
|
||||||
|
public enum TriggerSourceType {
|
||||||
|
|
||||||
|
/// 事件登记
|
||||||
|
EVENT_RECORD,
|
||||||
|
|
||||||
|
/// 订单首次全额实收
|
||||||
|
SALES_ORDER_PAID
|
||||||
|
}
|
||||||
|
|
||||||
/// 客户积分账户 ID
|
/// 客户积分账户 ID
|
||||||
private String accountId;
|
private String accountId;
|
||||||
|
|
||||||
@@ -67,11 +77,20 @@ public class GaoPointLedger extends UUIDEntity {
|
|||||||
/// 命中的积分规则 ID
|
/// 命中的积分规则 ID
|
||||||
private String ruleId;
|
private String ruleId;
|
||||||
|
|
||||||
/// 登记事件 ID
|
/// 直接触发来源类型
|
||||||
private String eventId;
|
private TriggerSourceType triggerSourceType;
|
||||||
|
|
||||||
/// 登记记录 ID
|
/// 直接触发来源 ID
|
||||||
private String eventRecordId;
|
private String triggerSourceId;
|
||||||
|
|
||||||
|
/// 关联销售订单 ID
|
||||||
|
private String salesOrderId;
|
||||||
|
|
||||||
|
/// 实际执行 Groovy 脚本版本 ID,简单规则为空
|
||||||
|
private String groovyScriptVersionId;
|
||||||
|
|
||||||
|
/// 命中时的规则快照
|
||||||
|
private JsonNode ruleSnapshot;
|
||||||
|
|
||||||
/// 来源快照
|
/// 来源快照
|
||||||
private JsonNode sourcePayload;
|
private JsonNode sourcePayload;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.imyeyu.api.modules.gao.entity;
|
package com.imyeyu.api.modules.gao.entity;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.imyeyu.spring.entity.UUIDEntity;
|
import com.imyeyu.spring.entity.UUIDEntity;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@@ -29,43 +28,47 @@ public class GaoPointRule extends UUIDEntity {
|
|||||||
DELETED
|
DELETED
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 触发类型
|
/// 规则模式
|
||||||
public enum TriggerType {
|
public enum RuleMode {
|
||||||
|
|
||||||
/// 每登记一次触发一次
|
/// 简单规则,固定积分值
|
||||||
EVERY_TIME,
|
SIMPLE,
|
||||||
|
|
||||||
/// 客户首次登记该事件触发一次
|
/// Groovy 脚本规则
|
||||||
FIRST_TIME,
|
GROOVY
|
||||||
|
|
||||||
/// 客户累计登记达到 N 次后每次触发
|
|
||||||
AFTER_COUNT_EVERY_TIME,
|
|
||||||
|
|
||||||
/// 使用登记接口提交的积分值触发
|
|
||||||
SOURCE_VALUE,
|
|
||||||
|
|
||||||
/// 按来源事件数值累计分档触发
|
|
||||||
CUSTOMER_EVENT_VALUE_TIER
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 积分值模式
|
/// 直接触发来源类型
|
||||||
public enum PointMode {
|
public enum TriggerSourceType {
|
||||||
|
|
||||||
/// 使用规则固定积分值
|
/// 事件登记
|
||||||
FIXED,
|
EVENT_RECORD,
|
||||||
|
|
||||||
/// 使用登记接口提交的积分值
|
/// 订单首次全额实收
|
||||||
SOURCE_VALUE,
|
SALES_ORDER_PAID
|
||||||
|
}
|
||||||
|
|
||||||
/// 使用条件配置中的分档积分值
|
/// 触发对象筛选类型
|
||||||
TIER
|
public enum TriggerRefType {
|
||||||
|
|
||||||
|
/// GAO 登记事件
|
||||||
|
EVENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 门店 ID
|
/// 门店 ID
|
||||||
private String storeId;
|
private String storeId;
|
||||||
|
|
||||||
/// 登记事件 ID
|
/// 规则模式
|
||||||
private String eventId;
|
private RuleMode ruleMode;
|
||||||
|
|
||||||
|
/// 直接触发来源类型
|
||||||
|
private TriggerSourceType triggerSourceType;
|
||||||
|
|
||||||
|
/// 触发对象筛选类型
|
||||||
|
private TriggerRefType triggerRefType;
|
||||||
|
|
||||||
|
/// 触发对象筛选 ID
|
||||||
|
private String triggerRefId;
|
||||||
|
|
||||||
/// 规则名称
|
/// 规则名称
|
||||||
private String name;
|
private String name;
|
||||||
@@ -73,17 +76,11 @@ public class GaoPointRule extends UUIDEntity {
|
|||||||
/// 规则状态
|
/// 规则状态
|
||||||
private Status status;
|
private Status status;
|
||||||
|
|
||||||
/// 触发类型
|
/// 固定积分值,简单规则使用
|
||||||
private TriggerType triggerType;
|
|
||||||
|
|
||||||
/// 积分值模式
|
|
||||||
private PointMode pointMode;
|
|
||||||
|
|
||||||
/// 固定积分值
|
|
||||||
private Long pointValue;
|
private Long pointValue;
|
||||||
|
|
||||||
/// 条件配置
|
/// Groovy 脚本 ID,GROOVY 模式使用
|
||||||
private JsonNode conditionJson;
|
private String groovyScriptId;
|
||||||
|
|
||||||
/// 生效开始时间戳,毫秒,空为不限制
|
/// 生效开始时间戳,毫秒,空为不限制
|
||||||
private Long beginAt;
|
private Long beginAt;
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ public class GaoSalesOrder extends UUIDEntity {
|
|||||||
/// 有效收款,单位分
|
/// 有效收款,单位分
|
||||||
private Long paidAmount;
|
private Long paidAmount;
|
||||||
|
|
||||||
|
/// 首次全额实收时间
|
||||||
|
private Long paidAt;
|
||||||
|
|
||||||
/// 有效退款,单位分
|
/// 有效退款,单位分
|
||||||
private Long refundedAmount;
|
private Long refundedAmount;
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ public class GaoStore extends UUIDEntity {
|
|||||||
/// 排序值
|
/// 排序值
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
|
|
||||||
/// 直接产品订单使用的消费事件 ID
|
|
||||||
private String directProductSaleEventId;
|
|
||||||
|
|
||||||
public boolean isActivated() {
|
public boolean isActivated() {
|
||||||
return status == Status.ACTIVE;
|
return status == Status.ACTIVE;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.mapper;
|
||||||
|
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
|
||||||
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/// GAO 客户有效消费事实 Mapper
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public interface GaoCustomerConsumptionRecordMapper extends BaseMapper<GaoCustomerConsumptionRecord, String> {
|
||||||
|
|
||||||
|
GaoCustomerConsumptionRecord selectBySource(@Param("sourceType") GaoCustomerConsumptionRecord.SourceType sourceType, @Param("sourceId") String sourceId);
|
||||||
|
|
||||||
|
List<GaoCustomerConsumptionRecord> selectActiveByWindow(@Param("storeId") String storeId, @Param("customerId") String customerId, @Param("beginAt") long beginAt, @Param("endAt") long endAt);
|
||||||
|
|
||||||
|
int revokeBySource(@Param("sourceType") GaoCustomerConsumptionRecord.SourceType sourceType, @Param("sourceId") String sourceId, @Param("revokedAt") long revokedAt, @Param("revokeReason") String revokeReason);
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ public class GaoEventSQLProvider extends SQLProvider {
|
|||||||
private String appendWithoutDeleted(String sql) {
|
private String appendWithoutDeleted(String sql) {
|
||||||
return sql.replace(
|
return sql.replace(
|
||||||
"WHERE 1 = 1",
|
"WHERE 1 = 1",
|
||||||
"WHERE 1 = 1 AND (`status` IS NULL OR `status` <> '%s')".formatted(GaoEvent.Status.DELETED.name())
|
"WHERE 1 = 1 AND (`status` IS NULL OR `status` <> '%s') AND (`biz_type` IS NULL OR `biz_type` <> '%s')".formatted(GaoEvent.Status.DELETED.name(), GaoEvent.BizType.LEGACY_CONSUMPTION.name())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.mapper;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Delete;
|
||||||
|
import org.apache.ibatis.annotations.Insert;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/// GAO 积分流水消费事实依赖 Mapper
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public interface GaoPointLedgerConsumptionRecordMapper {
|
||||||
|
|
||||||
|
/// 写入积分流水对消费事实的依赖
|
||||||
|
///
|
||||||
|
/// @param pointLedgerId 积分流水 ID
|
||||||
|
/// @param consumptionRecordId 消费事实 ID
|
||||||
|
/// @param createdAt 创建时间
|
||||||
|
/// @return 写入行数
|
||||||
|
@Insert("INSERT IGNORE INTO `gao_point_ledger_consumption_record` (`point_ledger_id`, `consumption_record_id`, `created_at`) VALUES (#{pointLedgerId}, #{consumptionRecordId}, #{createdAt})")
|
||||||
|
int insert(@Param("pointLedgerId") String pointLedgerId, @Param("consumptionRecordId") String consumptionRecordId, @Param("createdAt") long createdAt);
|
||||||
|
|
||||||
|
/// 查询积分流水依赖的消费事实 ID
|
||||||
|
///
|
||||||
|
/// @param pointLedgerId 积分流水 ID
|
||||||
|
/// @return 消费事实 ID 列表
|
||||||
|
@Select("SELECT `consumption_record_id` FROM `gao_point_ledger_consumption_record` WHERE `point_ledger_id` = #{pointLedgerId}")
|
||||||
|
List<String> selectConsumptionRecordIdList(@Param("pointLedgerId") String pointLedgerId);
|
||||||
|
|
||||||
|
/// 删除积分流水的依赖关系
|
||||||
|
///
|
||||||
|
/// @param pointLedgerId 积分流水 ID
|
||||||
|
/// @return 删除行数
|
||||||
|
@Delete("DELETE FROM `gao_point_ledger_consumption_record` WHERE `point_ledger_id` = #{pointLedgerId}")
|
||||||
|
int deleteByPointLedgerId(@Param("pointLedgerId") String pointLedgerId);
|
||||||
|
}
|
||||||
@@ -19,11 +19,18 @@ public interface GaoPointLedgerMapper extends BaseMapper<GaoPointLedger, String>
|
|||||||
/// @return 流水
|
/// @return 流水
|
||||||
GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey);
|
GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey);
|
||||||
|
|
||||||
/// 查询登记记录产生的原始规则积分流水
|
/// 查询触发来源产生的原始规则积分流水
|
||||||
///
|
///
|
||||||
/// @param eventRecordId 登记记录 ID
|
/// @param triggerSourceType 直接触发来源类型
|
||||||
|
/// @param triggerSourceId 直接触发来源 ID
|
||||||
/// @return 原始规则积分流水
|
/// @return 原始规则积分流水
|
||||||
List<GaoPointLedger> selectRuleChangeByEventRecordId(@Param("eventRecordId") String eventRecordId);
|
List<GaoPointLedger> selectRuleChangeByTriggerSource(@Param("triggerSourceType") GaoPointLedger.TriggerSourceType triggerSourceType, @Param("triggerSourceId") String triggerSourceId);
|
||||||
|
|
||||||
|
/// 查询依赖指定消费事实的原始规则积分流水
|
||||||
|
///
|
||||||
|
/// @param consumptionRecordId 消费事实 ID
|
||||||
|
/// @return 原始规则积分流水
|
||||||
|
List<GaoPointLedger> selectRuleChangeByConsumptionRecordId(@Param("consumptionRecordId") String consumptionRecordId);
|
||||||
|
|
||||||
/// 批量查询幂等键对应的流水
|
/// 批量查询幂等键对应的流水
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ public interface GaoPointRuleMapper extends BaseMapper<GaoPointRule, String> {
|
|||||||
/// 查询当前时间可匹配的启用规则
|
/// 查询当前时间可匹配的启用规则
|
||||||
///
|
///
|
||||||
/// @param storeId 门店 ID
|
/// @param storeId 门店 ID
|
||||||
/// @param eventId 登记事件 ID
|
/// @param triggerSourceType 直接触发来源类型
|
||||||
|
/// @param triggerRefId 触发对象筛选 ID,空为不限
|
||||||
/// @param now 当前时间戳
|
/// @param now 当前时间戳
|
||||||
/// @return 按优先级排序的规则列表
|
/// @return 按优先级排序的规则列表
|
||||||
List<GaoPointRule> selectActiveByStoreIdAndEventId(@Param("storeId") String storeId, @Param("eventId") String eventId, @Param("now") long now);
|
List<GaoPointRule> selectActiveByTrigger(@Param("storeId") String storeId, @Param("triggerSourceType") GaoPointRule.TriggerSourceType triggerSourceType, @Param("triggerRefId") String triggerRefId, @Param("now") long now);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public interface GaoSalesOrderMapper extends BaseMapper<GaoSalesOrder, String> {
|
|||||||
|
|
||||||
/// 更新订单付款汇总
|
/// 更新订单付款汇总
|
||||||
int updatePayment(@Param("id") String id, @Param("pendingPaidAmount") Long pendingPaidAmount, @Param("paidAmount") Long paidAmount,
|
int updatePayment(@Param("id") String id, @Param("pendingPaidAmount") Long pendingPaidAmount, @Param("paidAmount") Long paidAmount,
|
||||||
@Param("refundedAmount") Long refundedAmount, @Param("paymentStatus") GaoSalesOrder.PaymentStatus paymentStatus,
|
@Param("paidAt") Long paidAt, @Param("refundedAmount") Long refundedAmount, @Param("paymentStatus") GaoSalesOrder.PaymentStatus paymentStatus,
|
||||||
@Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus);
|
@Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus);
|
||||||
|
|
||||||
/// 带状态条件取消订单
|
/// 带状态条件取消订单
|
||||||
@@ -35,9 +35,6 @@ public interface GaoSalesOrderMapper extends BaseMapper<GaoSalesOrder, String> {
|
|||||||
@Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId,
|
@Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId,
|
||||||
@Param("cancelReason") String cancelReason, @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus);
|
@Param("cancelReason") String cancelReason, @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus);
|
||||||
|
|
||||||
/// 写入订单来源事件
|
|
||||||
int updateSourceEventRecordId(@Param("id") String id, @Param("sourceEventRecordId") String sourceEventRecordId);
|
|
||||||
|
|
||||||
/// 更新订单履约状态
|
/// 更新订单履约状态
|
||||||
int updateStatus(@Param("id") String id, @Param("status") GaoSalesOrder.Status status);
|
int updateStatus(@Param("id") String id, @Param("status") GaoSalesOrder.Status status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.script;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/// GAO 积分规则脚本决策
|
||||||
|
///
|
||||||
|
/// 脚本通过上下文构造本对象返回声明式积分决策,不能直接写积分账户或积分流水
|
||||||
|
///
|
||||||
|
/// @param pointDelta 积分变化,正为发放,负为扣减,不能为 0
|
||||||
|
/// @param reason 业务原因
|
||||||
|
/// @param conditionSnapshot 条件快照
|
||||||
|
/// @param consumptionRecordIdList 依赖的消费事实 ID 列表
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public record GaoPointDecision(long pointDelta, String reason, Map<String, Object> conditionSnapshot, List<String> consumptionRecordIdList) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.script;
|
||||||
|
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/// GAO 积分规则脚本执行上下文
|
||||||
|
///
|
||||||
|
/// 只向脚本提供归一化触发信息和只读消费事实查询,滚动窗口固定使用 Asia/Shanghai 时区
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public class GaoPointScriptContext {
|
||||||
|
|
||||||
|
/// 滚动窗口固定时区
|
||||||
|
public static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
|
private final GaoCustomerConsumptionRecordService consumptionService;
|
||||||
|
private final GaoEventRecordService eventRecordService;
|
||||||
|
private final GaoPointRule rule;
|
||||||
|
private final String storeId;
|
||||||
|
private final String customerId;
|
||||||
|
private final String triggerSourceType;
|
||||||
|
private final String triggerRefType;
|
||||||
|
private final String triggerRefId;
|
||||||
|
private final long occurredAt;
|
||||||
|
|
||||||
|
/// 参与过金额计算的消费事实,ID 到金额摘要
|
||||||
|
private final Map<String, Long> consumptionFactMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public GaoPointScriptContext(GaoCustomerConsumptionRecordService consumptionService, GaoEventRecordService eventRecordService,
|
||||||
|
GaoPointRule rule, String storeId, String customerId, String triggerRefType, String triggerRefId, long occurredAt) {
|
||||||
|
this.consumptionService = consumptionService;
|
||||||
|
this.eventRecordService = eventRecordService;
|
||||||
|
this.rule = rule;
|
||||||
|
this.storeId = storeId;
|
||||||
|
this.customerId = customerId;
|
||||||
|
this.triggerSourceType = rule.getTriggerSourceType() == null ? null : rule.getTriggerSourceType().name();
|
||||||
|
this.triggerRefType = triggerRefType;
|
||||||
|
this.triggerRefId = triggerRefId;
|
||||||
|
this.occurredAt = occurredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 触发来源门店 ID
|
||||||
|
public String getStoreId() {
|
||||||
|
return storeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 触发来源客户 ID
|
||||||
|
public String getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 直接触发来源类型
|
||||||
|
public String getTriggerSourceType() {
|
||||||
|
return triggerSourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 触发对象筛选类型
|
||||||
|
public String getTriggerRefType() {
|
||||||
|
return triggerRefType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 触发对象筛选 ID
|
||||||
|
public String getTriggerRefId() {
|
||||||
|
return triggerRefId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 触发发生时间,毫秒
|
||||||
|
public long getOccurredAt() {
|
||||||
|
return occurredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询触发时间往前滚动一年的有效消费金额,窗口按 Asia/Shanghai 自然日对齐
|
||||||
|
///
|
||||||
|
/// @return 窗口内 ACTIVE 消费事实金额合计,单位分
|
||||||
|
public long rollingYearConsumptionAmount() {
|
||||||
|
LocalDate currentDate = Instant.ofEpochMilli(occurredAt).atZone(ZONE).toLocalDate();
|
||||||
|
long beginAt = currentDate.minusYears(1).atStartOfDay(ZONE).toInstant().toEpochMilli();
|
||||||
|
return consumptionAmountBetween(beginAt, occurredAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询指定时间窗口内的有效消费金额
|
||||||
|
///
|
||||||
|
/// 查询上限不会晚于触发时间,保证历史复算不受后续新增消费影响
|
||||||
|
///
|
||||||
|
/// @param beginAt 窗口起始,包含
|
||||||
|
/// @param endAt 窗口结束,包含
|
||||||
|
/// @return 窗口内 ACTIVE 消费事实金额合计,单位分
|
||||||
|
public long consumptionAmountBetween(long beginAt, long endAt) {
|
||||||
|
long cappedEndAt = Math.min(endAt, occurredAt);
|
||||||
|
long total = 0L;
|
||||||
|
for (GaoCustomerConsumptionRecord record : consumptionService.listActiveByWindow(storeId, customerId, beginAt, cappedEndAt)) {
|
||||||
|
consumptionFactMap.putIfAbsent(record.getId(), record.getAmount());
|
||||||
|
total = Math.addExact(total, record.getAmount());
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询客户在触发对象事件上截至触发时间的登记次数
|
||||||
|
///
|
||||||
|
/// @return 登记次数
|
||||||
|
public long eventCount() {
|
||||||
|
return eventRecordService.count(storeId, customerId, triggerRefId, null, occurredAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构造积分决策
|
||||||
|
///
|
||||||
|
/// @param pointDelta 积分变化,不能为 0
|
||||||
|
/// @param reason 业务原因
|
||||||
|
/// @param conditionSnapshot 条件快照
|
||||||
|
/// @return 积分决策
|
||||||
|
public GaoPointDecision decision(long pointDelta, String reason, Map<String, Object> conditionSnapshot) {
|
||||||
|
return decision(pointDelta, reason, conditionSnapshot, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构造积分决策并显式追加依赖的消费事实
|
||||||
|
///
|
||||||
|
/// @param pointDelta 积分变化,不能为 0
|
||||||
|
/// @param reason 业务原因
|
||||||
|
/// @param conditionSnapshot 条件快照
|
||||||
|
/// @param consumptionRecordIdList 额外依赖的消费事实 ID 列表
|
||||||
|
/// @return 积分决策
|
||||||
|
public GaoPointDecision decision(long pointDelta, String reason, Map<String, Object> conditionSnapshot, List<String> consumptionRecordIdList) {
|
||||||
|
TimiException.requiredTrue(pointDelta != 0, "脚本积分变化不能为 0");
|
||||||
|
Map<String, Object> snapshot = conditionSnapshot == null ? new LinkedHashMap<>() : new LinkedHashMap<>(conditionSnapshot);
|
||||||
|
List<Map<String, Object>> consumptionSummary = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, Long> entry : consumptionFactMap.entrySet()) {
|
||||||
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
|
item.put("id", entry.getKey());
|
||||||
|
item.put("amount", entry.getValue());
|
||||||
|
consumptionSummary.add(item);
|
||||||
|
}
|
||||||
|
snapshot.put("consumptionRecords", consumptionSummary);
|
||||||
|
snapshot.put("consumptionTotal", consumptionFactMap.values().stream().mapToLong(Long::longValue).sum());
|
||||||
|
List<String> deps = new ArrayList<>(consumptionFactMap.keySet());
|
||||||
|
for (String id : consumptionRecordIdList == null ? List.<String>of() : consumptionRecordIdList) {
|
||||||
|
if (id != null && !deps.contains(id)) {
|
||||||
|
deps.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new GaoPointDecision(pointDelta, reason, snapshot, deps);
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.service;
|
||||||
|
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
|
||||||
|
import com.imyeyu.spring.service.BaseService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/// GAO 客户有效消费事实服务
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
public interface GaoCustomerConsumptionRecordService extends BaseService<GaoCustomerConsumptionRecord, String> {
|
||||||
|
|
||||||
|
/// 写入销售订单消费事实
|
||||||
|
///
|
||||||
|
/// 幂等,重复执行返回已有事实
|
||||||
|
///
|
||||||
|
/// @param salesOrderId 销售订单 ID
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
/// @param customerId 客户 ID
|
||||||
|
/// @param amount 有效消费金额,单位分,必须大于 0
|
||||||
|
/// @param effectiveAt 全额实收时间
|
||||||
|
/// @param orderType 订单类型
|
||||||
|
/// @return 消费事实
|
||||||
|
GaoCustomerConsumptionRecord createSalesOrder(String salesOrderId, String storeId, String customerId, Long amount, Long effectiveAt, com.imyeyu.api.modules.gao.entity.GaoSalesOrder.OrderType orderType);
|
||||||
|
|
||||||
|
/// 撤销销售订单消费事实
|
||||||
|
///
|
||||||
|
/// @param salesOrderId 销售订单 ID
|
||||||
|
/// @param reason 失效原因
|
||||||
|
void revokeSalesOrder(String salesOrderId, String reason);
|
||||||
|
|
||||||
|
/// 撤销历史事件迁移出的消费事实
|
||||||
|
///
|
||||||
|
/// @param eventRecordId 原事件记录 ID
|
||||||
|
/// @param reason 失效原因
|
||||||
|
void revokeLegacyEvent(String eventRecordId, String reason);
|
||||||
|
|
||||||
|
/// 按来源查询消费事实
|
||||||
|
///
|
||||||
|
/// @param sourceType 来源类型
|
||||||
|
/// @param sourceId 来源业务对象 ID
|
||||||
|
/// @return 消费事实,不存在时返回 null
|
||||||
|
GaoCustomerConsumptionRecord getBySource(GaoCustomerConsumptionRecord.SourceType sourceType, String sourceId);
|
||||||
|
|
||||||
|
/// 查询时间窗口内的 ACTIVE 消费事实
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
/// @param customerId 客户 ID
|
||||||
|
/// @param beginAt 起始时间,包含
|
||||||
|
/// @param endAt 结束时间,包含
|
||||||
|
/// @return 消费事实列表
|
||||||
|
List<GaoCustomerConsumptionRecord> listActiveByWindow(String storeId, String customerId, long beginAt, long endAt);
|
||||||
|
}
|
||||||
@@ -104,23 +104,6 @@ public interface GaoEventRecordService extends BaseService<GaoEventRecord, Strin
|
|||||||
/// @return 创建后的事件登记
|
/// @return 创建后的事件登记
|
||||||
GaoEventRecord createOne(GaoEventRecord record);
|
GaoEventRecord createOne(GaoEventRecord record);
|
||||||
|
|
||||||
/// 由套餐购买服务创建购买事件登记
|
|
||||||
///
|
|
||||||
/// @param record 购买事件登记
|
|
||||||
/// @return 创建后的事件登记
|
|
||||||
GaoEventRecord createPackagePurchaseRecord(GaoEventRecord record);
|
|
||||||
|
|
||||||
/// 由直接产品订单内部创建消费事件记录
|
|
||||||
///
|
|
||||||
/// @param record 订单消费事件记录
|
|
||||||
/// @return 创建后的事件记录
|
|
||||||
GaoEventRecord createOrderConsumeRecord(GaoEventRecord record);
|
|
||||||
|
|
||||||
/// 撤销订单消费事件产生的全部积分
|
|
||||||
///
|
|
||||||
/// @param record 订单消费事件记录
|
|
||||||
void revokeOrderConsumeRecord(GaoEventRecord record);
|
|
||||||
|
|
||||||
/// 拒绝修改或删除已被订单引用的事件记录
|
/// 拒绝修改或删除已被订单引用的事件记录
|
||||||
///
|
///
|
||||||
/// @param recordId 事件记录 ID
|
/// @param recordId 事件记录 ID
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.imyeyu.api.modules.gao.service;
|
package com.imyeyu.api.modules.gao.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
import com.imyeyu.api.modules.gao.script.GaoPointDecision;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
|
import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage;
|
||||||
import com.imyeyu.spring.bean.PageResult;
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
@@ -17,21 +17,34 @@ import java.util.List;
|
|||||||
/// @since 2026-08-11
|
/// @since 2026-08-11
|
||||||
public interface GaoPointLedgerService extends BaseService<GaoPointLedger, String> {
|
public interface GaoPointLedgerService extends BaseService<GaoPointLedger, String> {
|
||||||
|
|
||||||
/// 写入登记规则积分流水
|
/// 写入规则发放或扣减积分流水
|
||||||
|
///
|
||||||
|
/// 幂等,重复触发返回已有流水;积分流水只追加,不修改不删除
|
||||||
///
|
///
|
||||||
/// @param record 登记记录
|
|
||||||
/// @param rule 命中规则
|
/// @param rule 命中规则
|
||||||
/// @param pointDelta 积分变化
|
/// @param decision 积分决策
|
||||||
/// @param sourcePayload 来源快照
|
/// @param execution Groovy 脚本执行结果,简单规则为空
|
||||||
|
/// @param triggerSourceType 直接触发来源类型
|
||||||
|
/// @param triggerSourceId 直接触发来源 ID
|
||||||
|
/// @param salesOrderId 关联销售订单 ID,可空
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
/// @param customerId 客户 ID
|
||||||
|
/// @param occurredAt 发生时间
|
||||||
|
/// @param operatorUserId 操作用户 ID
|
||||||
|
/// @param consumptionRecordIdList 依赖的消费事实 ID 列表
|
||||||
/// @return 积分流水
|
/// @return 积分流水
|
||||||
GaoPointLedger applyRule(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload);
|
GaoPointLedger applyRule(GaoPointRule rule, GaoPointDecision decision, GroovyScriptExecutionResult<?> execution,
|
||||||
|
GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId,
|
||||||
|
String storeId, String customerId, Long occurredAt, String operatorUserId, List<String> consumptionRecordIdList);
|
||||||
|
|
||||||
/// 写入删除登记记录的撤销流水
|
/// 追加撤销流水
|
||||||
|
///
|
||||||
|
/// 撤销流水以原流水为基准反向冲抵,幂等键按原流水设计
|
||||||
///
|
///
|
||||||
/// @param originLedger 原始规则积分流水
|
/// @param originLedger 原始规则积分流水
|
||||||
/// @param record 登记记录
|
/// @param reason 撤销原因
|
||||||
/// @return 撤销流水
|
/// @return 撤销流水
|
||||||
GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record);
|
GaoPointLedger revoke(GaoPointLedger originLedger, String reason);
|
||||||
|
|
||||||
/// 写入人工调整流水
|
/// 写入人工调整流水
|
||||||
///
|
///
|
||||||
@@ -40,11 +53,18 @@ public interface GaoPointLedgerService extends BaseService<GaoPointLedger, Strin
|
|||||||
/// @return 调整流水
|
/// @return 调整流水
|
||||||
GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId);
|
GaoPointLedger adjust(GaoPointAdjustRequest request, String operatorUserId);
|
||||||
|
|
||||||
/// 查询登记记录产生的原始规则积分流水
|
/// 查询触发来源产生的原始规则积分流水
|
||||||
///
|
///
|
||||||
/// @param eventRecordId 登记记录 ID
|
/// @param triggerSourceType 直接触发来源类型
|
||||||
|
/// @param triggerSourceId 直接触发来源 ID
|
||||||
/// @return 原始积分流水
|
/// @return 原始积分流水
|
||||||
List<GaoPointLedger> listRuleChangeByEventRecordId(String eventRecordId);
|
List<GaoPointLedger> listRuleChangeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId);
|
||||||
|
|
||||||
|
/// 查询依赖指定消费事实的原始规则积分流水
|
||||||
|
///
|
||||||
|
/// @param consumptionRecordId 消费事实 ID
|
||||||
|
/// @return 原始积分流水
|
||||||
|
List<GaoPointLedger> listRuleChangeByConsumptionRecordId(String consumptionRecordId);
|
||||||
|
|
||||||
/// 按幂等键批量查询积分流水
|
/// 按幂等键批量查询积分流水
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ public interface GaoPointRuleService extends BaseService<GaoPointRule, String> {
|
|||||||
|
|
||||||
/// 查询当前时间可匹配的启用规则
|
/// 查询当前时间可匹配的启用规则
|
||||||
///
|
///
|
||||||
|
/// 触发对象筛选 ID 为空时匹配未设置触发对象引用的规则
|
||||||
|
///
|
||||||
/// @param storeId 门店 ID
|
/// @param storeId 门店 ID
|
||||||
/// @param eventId 登记事件 ID
|
/// @param triggerSourceType 直接触发来源类型
|
||||||
|
/// @param triggerRefId 触发对象筛选 ID,可空
|
||||||
/// @param now 当前时间戳
|
/// @param now 当前时间戳
|
||||||
/// @return 规则列表
|
/// @return 按优先级排序的规则列表
|
||||||
List<GaoPointRule> listActive(String storeId, String eventId, long now);
|
List<GaoPointRule> listActive(String storeId, GaoPointRule.TriggerSourceType triggerSourceType, String triggerRefId, long now);
|
||||||
|
|
||||||
/// 查询规则 ID 列表
|
/// 查询规则 ID 列表
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.imyeyu.api.modules.gao.service;
|
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.entity.GaoEventRecord;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
||||||
|
|
||||||
/// GAO 客户积分触发服务
|
/// GAO 客户积分触发服务
|
||||||
@@ -15,11 +16,22 @@ public interface GaoPointTriggerService {
|
|||||||
/// @param record 登记记录
|
/// @param record 登记记录
|
||||||
void onEventRecordCreated(GaoEventRecord record);
|
void onEventRecordCreated(GaoEventRecord record);
|
||||||
|
|
||||||
/// 撤销登记记录产生的全部获得积分
|
/// 撤销登记记录触发的全部获得积分,并撤销迁移消费事实及其依赖积分
|
||||||
///
|
///
|
||||||
/// @param record 登记记录
|
/// @param record 登记记录
|
||||||
void revokeByEventRecord(GaoEventRecord record);
|
void revokeByEventRecord(GaoEventRecord record);
|
||||||
|
|
||||||
|
/// 在订单首次全额实收时写入消费事实并触发订单积分
|
||||||
|
///
|
||||||
|
/// @param order 销售订单
|
||||||
|
void onSalesOrderPaid(GaoSalesOrder order);
|
||||||
|
|
||||||
|
/// 订单取消或全额实收被冲正时撤销消费事实及其依赖积分
|
||||||
|
///
|
||||||
|
/// @param order 销售订单
|
||||||
|
/// @param reason 撤销原因
|
||||||
|
void revokeBySalesOrder(GaoSalesOrder order, String reason);
|
||||||
|
|
||||||
/// 执行人工调整
|
/// 执行人工调整
|
||||||
///
|
///
|
||||||
/// @param request 人工调整请求
|
/// @param request 人工调整请求
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.imyeyu.api.modules.gao.service;
|
package com.imyeyu.api.modules.gao.service;
|
||||||
|
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoEvent;
|
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoStore;
|
import com.imyeyu.api.modules.gao.entity.GaoStore;
|
||||||
import com.imyeyu.spring.bean.Page;
|
import com.imyeyu.spring.bean.Page;
|
||||||
import com.imyeyu.spring.service.BaseService;
|
import com.imyeyu.spring.service.BaseService;
|
||||||
@@ -15,18 +14,6 @@ import java.util.List;
|
|||||||
/// @since 2026-08-06
|
/// @since 2026-08-06
|
||||||
public interface GaoStoreService extends BaseService<GaoStore, String> {
|
public interface GaoStoreService extends BaseService<GaoStore, String> {
|
||||||
|
|
||||||
/// 校验直接产品订单使用的消费事件
|
|
||||||
///
|
|
||||||
/// @param store 门店
|
|
||||||
/// @return 合法的消费事件
|
|
||||||
GaoEvent validateDirectProductSaleEvent(GaoStore store);
|
|
||||||
|
|
||||||
/// 查询可绑定到直接产品订单的消费事件
|
|
||||||
///
|
|
||||||
/// @param storeId 门店 ID
|
|
||||||
/// @return 当前门店可绑定的消费事件
|
|
||||||
List<GaoEvent> listDirectProductSaleEvents(String storeId);
|
|
||||||
|
|
||||||
/// 查询用户所属门店
|
/// 查询用户所属门店
|
||||||
///
|
///
|
||||||
/// @param userId 用户 ID
|
/// @param userId 用户 ID
|
||||||
|
|||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.service.implement;
|
||||||
|
|
||||||
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
|
||||||
|
import com.imyeyu.api.modules.gao.mapper.GaoCustomerConsumptionRecordMapper;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService;
|
||||||
|
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.List;
|
||||||
|
|
||||||
|
/// GAO 客户有效消费事实服务实现
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-09-04
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GaoCustomerConsumptionRecordServiceImplement extends AbstractEntityService<GaoCustomerConsumptionRecord, String> implements GaoCustomerConsumptionRecordService {
|
||||||
|
|
||||||
|
private final GaoCustomerConsumptionRecordMapper mapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BaseMapper<GaoCustomerConsumptionRecord, String> mapper() {
|
||||||
|
return mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public GaoCustomerConsumptionRecord createSalesOrder(String salesOrderId, String storeId, String customerId, Long amount, Long effectiveAt, GaoSalesOrder.OrderType orderType) {
|
||||||
|
TimiException.requiredTrue(amount != null && 0 < amount, "有效消费金额必须大于 0");
|
||||||
|
GaoCustomerConsumptionRecord existing = mapper.selectBySource(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER, salesOrderId);
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
GaoCustomerConsumptionRecord record = new GaoCustomerConsumptionRecord();
|
||||||
|
record.setStoreId(storeId);
|
||||||
|
record.setCustomerId(customerId);
|
||||||
|
record.setSourceType(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER);
|
||||||
|
record.setSourceId(salesOrderId);
|
||||||
|
record.setSalesOrderId(salesOrderId);
|
||||||
|
record.setOrderType(orderType);
|
||||||
|
record.setAmount(amount);
|
||||||
|
record.setEffectiveAt(effectiveAt);
|
||||||
|
record.setStatus(GaoCustomerConsumptionRecord.Status.ACTIVE);
|
||||||
|
super.create(record);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void revokeSalesOrder(String salesOrderId, String reason) {
|
||||||
|
mapper.revokeBySource(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER, salesOrderId, Time.now(), reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void revokeLegacyEvent(String eventRecordId, String reason) {
|
||||||
|
mapper.revokeBySource(GaoCustomerConsumptionRecord.SourceType.LEGACY_EVENT, eventRecordId, Time.now(), reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GaoCustomerConsumptionRecord getBySource(GaoCustomerConsumptionRecord.SourceType sourceType, String sourceId) {
|
||||||
|
TimiException.required(sourceType, "not found sourceType");
|
||||||
|
TimiException.required(sourceId, "not found sourceId");
|
||||||
|
return mapper.selectBySource(sourceType, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GaoCustomerConsumptionRecord> listActiveByWindow(String storeId, String customerId, long beginAt, long endAt) {
|
||||||
|
TimiException.required(storeId, "not found storeId");
|
||||||
|
TimiException.required(customerId, "not found customerId");
|
||||||
|
return mapper.selectActiveByWindow(storeId, customerId, beginAt, endAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-41
@@ -2,11 +2,11 @@ package com.imyeyu.api.modules.gao.service.implement;
|
|||||||
|
|
||||||
import com.imyeyu.api.bean.ModuleCode;
|
import com.imyeyu.api.bean.ModuleCode;
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoEvent;
|
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
|
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
|
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
|
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
|
||||||
@@ -15,8 +15,6 @@ import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper;
|
|||||||
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
|
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
|
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
|
||||||
import com.imyeyu.api.modules.gao.service.GaoEventService;
|
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
|
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoProductService;
|
import com.imyeyu.api.modules.gao.service.GaoProductService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
|
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
|
||||||
@@ -64,10 +62,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
private final GaoPackageConfigService packageConfigService;
|
private final GaoPackageConfigService packageConfigService;
|
||||||
private final GaoProductService productService;
|
private final GaoProductService productService;
|
||||||
private final GaoCustomerService customerService;
|
private final GaoCustomerService customerService;
|
||||||
private final GaoEventService eventService;
|
|
||||||
private final GaoEventRecordService eventRecordService;
|
|
||||||
private final GaoSalesOrderService salesOrderService;
|
private final GaoSalesOrderService salesOrderService;
|
||||||
private final GaoStoreScopeService storeScopeService;
|
private final GaoStoreScopeService storeScopeService;
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
private final PermissionChecker permissionChecker;
|
private final PermissionChecker permissionChecker;
|
||||||
private final LoggerService loggerService;
|
private final LoggerService loggerService;
|
||||||
@@ -169,14 +166,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
TimiException.required(request, "not found purchase request");
|
TimiException.required(request, "not found purchase request");
|
||||||
TimiException.required(request.getRequestNo(), "not found requestNo");
|
TimiException.required(request.getRequestNo(), "not found requestNo");
|
||||||
request.setRequestNo(request.getRequestNo().trim());
|
request.setRequestNo(request.getRequestNo().trim());
|
||||||
TimiException.required(request.getEventId(), "not found eventId");
|
TimiException.required(request.getPackageConfigId(), "not found packageConfigId");
|
||||||
TimiException.required(request.getCustomerId(), "not found customerId");
|
TimiException.required(request.getCustomerId(), "not found customerId");
|
||||||
|
|
||||||
GaoEvent event = validatePurchaseEvent(request.getEventId());
|
GaoPackageConfig config = packageConfigService.get(request.getPackageConfigId());
|
||||||
storeScopeService.checkStoreId(event.getStoreId());
|
TimiException.required(config, "套餐配置不存在");
|
||||||
GaoPackageConfig config = packageConfigService.get(event.getPackageConfigId());
|
storeScopeService.checkStoreId(config.getStoreId());
|
||||||
TimiException.required(config, "购买事件绑定的套餐配置不存在");
|
|
||||||
TimiException.requiredTrue(event.getStoreId().equals(config.getStoreId()), "购买事件和套餐配置不属于同一门店");
|
|
||||||
List<GaoPackageConfigItem> configItemList = packageConfigService.listItemByPackageConfigId(config.getId());
|
List<GaoPackageConfigItem> configItemList = packageConfigService.listItemByPackageConfigId(config.getId());
|
||||||
request.setSaleAmount(TimiJava.defaultIfNull(request.getSaleAmount(), config.getStandardAmount()));
|
request.setSaleAmount(TimiJava.defaultIfNull(request.getSaleAmount(), config.getStandardAmount()));
|
||||||
TimiException.requiredTrue(0 <= request.getSaleAmount(), "成交价不能为负数");
|
TimiException.requiredTrue(0 <= request.getSaleAmount(), "成交价不能为负数");
|
||||||
@@ -209,16 +204,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
verifySameQuantityAsConfig(itemList, configItemList);
|
verifySameQuantityAsConfig(itemList, configItemList);
|
||||||
}
|
}
|
||||||
|
|
||||||
GaoEventRecord eventRecord = new GaoEventRecord();
|
|
||||||
eventRecord.setCustomerId(customer.getId());
|
|
||||||
eventRecord.setStoreId(config.getStoreId());
|
|
||||||
eventRecord.setEventId(event.getId());
|
|
||||||
eventRecord.setRegisteredAt(purchasedAt);
|
|
||||||
eventRecord.setRemark(request.getRemark());
|
|
||||||
if (event.getValueType() == GaoEvent.ValueType.NUMBER && event.getValueUnit() == GaoEvent.ValueUnit.CENT) {
|
|
||||||
eventRecord.setNumberValue(request.getSaleAmount());
|
|
||||||
}
|
|
||||||
GaoEventRecord createdEventRecord = eventRecordService.createPackagePurchaseRecord(eventRecordWithAttachments(eventRecord, request));
|
|
||||||
GaoPackageOrderCreateData orderData = new GaoPackageOrderCreateData();
|
GaoPackageOrderCreateData orderData = new GaoPackageOrderCreateData();
|
||||||
orderData.setStoreId(config.getStoreId());
|
orderData.setStoreId(config.getStoreId());
|
||||||
orderData.setCustomerId(customer.getId());
|
orderData.setCustomerId(customer.getId());
|
||||||
@@ -226,12 +211,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
orderData.setPackageNameSnapshot(config.getName());
|
orderData.setPackageNameSnapshot(config.getName());
|
||||||
orderData.setStandardAmountSnapshot(config.getStandardAmount());
|
orderData.setStandardAmountSnapshot(config.getStandardAmount());
|
||||||
orderData.setSaleAmount(request.getSaleAmount());
|
orderData.setSaleAmount(request.getSaleAmount());
|
||||||
orderData.setSourceEventRecordId(createdEventRecord.getId());
|
|
||||||
orderData.setRequestNo(request.getRequestNo());
|
orderData.setRequestNo(request.getRequestNo());
|
||||||
orderData.setPurchasedAt(purchasedAt);
|
orderData.setPurchasedAt(purchasedAt);
|
||||||
orderData.setOperatorUserId(userLoginService.getRequireLoginUserId());
|
orderData.setOperatorUserId(userLoginService.getRequireLoginUserId());
|
||||||
orderData.setRemark(request.getRemark());
|
orderData.setRemark(request.getRemark());
|
||||||
GaoSalesOrder salesOrder = salesOrderService.createPackageOrder(orderData);
|
GaoSalesOrder salesOrder = salesOrderService.createPackageOrder(orderData);
|
||||||
|
attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, salesOrder.getId(), request.getAttachmentList());
|
||||||
|
|
||||||
GaoCustomerPackage result = new GaoCustomerPackage();
|
GaoCustomerPackage result = new GaoCustomerPackage();
|
||||||
result.setStoreId(config.getStoreId());
|
result.setStoreId(config.getStoreId());
|
||||||
@@ -288,11 +273,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
return packageEntity;
|
return packageEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GaoEventRecord eventRecordWithAttachments(GaoEventRecord record, GaoCustomerPackagePurchaseRequest request) {
|
|
||||||
record.setAttachmentList(request.getAttachmentList());
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GaoCustomer validateCustomer(String customerId, String storeId) {
|
private GaoCustomer validateCustomer(String customerId, String storeId) {
|
||||||
GaoCustomer customer = customerService.get(customerId);
|
GaoCustomer customer = customerService.get(customerId);
|
||||||
TimiException.required(customer, "not found customer");
|
TimiException.required(customer, "not found customer");
|
||||||
@@ -300,14 +280,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
return customer;
|
return customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GaoEvent validatePurchaseEvent(String eventId) {
|
|
||||||
GaoEvent event = eventService.get(eventId);
|
|
||||||
TimiException.required(event, "not found purchase event");
|
|
||||||
TimiException.requiredTrue(event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE, "事件不是套餐购买事件");
|
|
||||||
TimiException.required(event.getPackageConfigId(), "购买事件未绑定套餐配置");
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<GaoCustomerPackagePurchaseItem> defaultPurchaseItems(List<GaoPackageConfigItem> configItemList) {
|
private List<GaoCustomerPackagePurchaseItem> defaultPurchaseItems(List<GaoPackageConfigItem> configItemList) {
|
||||||
return configItemList.stream().map(configItem -> {
|
return configItemList.stream().map(configItem -> {
|
||||||
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
|
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
|
||||||
@@ -350,11 +322,11 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
|
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
|
||||||
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致");
|
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致");
|
||||||
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity()));
|
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity()));
|
||||||
for (GaoPackageConfigItem configItem : configItemList) {
|
for (GaoPackageConfigItem configItem : configItemList) {
|
||||||
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId());
|
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId());
|
||||||
TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致");
|
TimiException.required(item, "套餐权益项目必须与套餐配置一致");
|
||||||
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
|
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
|
||||||
item.setUnitSnapshot(configItem.getUnitSnapshot());
|
item.setUnitSnapshot(configItem.getUnitSnapshot());
|
||||||
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
|
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
|
||||||
@@ -376,9 +348,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
TimiException.requiredTrue(existingOrder.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
|
TimiException.requiredTrue(existingOrder.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
|
||||||
&& existingOrder.getCustomerId().equals(request.getCustomerId())
|
&& existingOrder.getCustomerId().equals(request.getCustomerId())
|
||||||
&& existingOrder.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用");
|
&& existingOrder.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用");
|
||||||
GaoEventRecord eventRecord = eventRecordService.get(existingOrder.getSourceEventRecordId());
|
TimiException.requiredTrue(existingPackage.getPackageConfigId().equals(request.getPackageConfigId())
|
||||||
TimiException.required(eventRecord, "购买事件登记不存在");
|
|
||||||
TimiException.requiredTrue(eventRecord.getEventId().equals(request.getEventId())
|
|
||||||
&& (request.getPurchasedAt() == null || existingOrder.getPurchasedAt().equals(request.getPurchasedAt()))
|
&& (request.getPurchasedAt() == null || existingOrder.getPurchasedAt().equals(request.getPurchasedAt()))
|
||||||
&& sameText(existingOrder.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致");
|
&& sameText(existingOrder.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致");
|
||||||
List<GaoCustomerPackageItem> existingItemList = itemMapper.selectByCustomerPackageId(existingPackage.getId());
|
List<GaoCustomerPackageItem> existingItemList = itemMapper.selectByCustomerPackageId(existingPackage.getId());
|
||||||
|
|||||||
+8
-29
@@ -214,30 +214,13 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void create(GaoEventRecord record) {
|
public void create(GaoEventRecord record) {
|
||||||
createInternal(record, false, false);
|
createInternal(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public GaoEventRecord createOne(GaoEventRecord record) {
|
public GaoEventRecord createOne(GaoEventRecord record) {
|
||||||
return createInternal(record, false, false);
|
return createInternal(record);
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
|
||||||
@Override
|
|
||||||
public GaoEventRecord createPackagePurchaseRecord(GaoEventRecord record) {
|
|
||||||
return createInternal(record, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GaoEventRecord createOrderConsumeRecord(GaoEventRecord record) {
|
|
||||||
return createInternal(record, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void revokeOrderConsumeRecord(GaoEventRecord record) {
|
|
||||||
TimiException.required(record, "not found order event record");
|
|
||||||
pointTriggerService.revokeByEventRecord(record);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -246,7 +229,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
TimiException.requiredTrue(mapper.countBySourceEventRecordId(recordId) == 0, "订单关联事件记录不允许修改或删除");
|
TimiException.requiredTrue(mapper.countBySourceEventRecordId(recordId) == 0, "订单关联事件记录不允许修改或删除");
|
||||||
}
|
}
|
||||||
|
|
||||||
private GaoEventRecord createInternal(GaoEventRecord record, boolean packagePurchase, boolean orderConsume) {
|
private GaoEventRecord createInternal(GaoEventRecord record) {
|
||||||
TimiException.required(record, "not found record");
|
TimiException.required(record, "not found record");
|
||||||
|
|
||||||
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_BATCH_CREATE");
|
Logger logger = new Logger(Logger.Module.GAO, "GAO_EVENT_RECORD_BATCH_CREATE");
|
||||||
@@ -258,20 +241,14 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
GaoEvent event = eventService.get(record.getEventId());
|
GaoEvent event = eventService.get(record.getEventId());
|
||||||
TimiException.required(event, "not found event");
|
TimiException.required(event, "not found event");
|
||||||
TimiException.required(event.getStoreId(), "not found event.storeId");
|
TimiException.required(event.getStoreId(), "not found event.storeId");
|
||||||
if (packagePurchase) {
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件不可登记");
|
||||||
TimiException.requiredTrue(event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE, "购买事件类型无效");
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.PACKAGE_PURCHASE, "购买套餐事件已退役,不可登记");
|
||||||
TimiException.requiredTrue(!Boolean.TRUE.equals(event.getRequireVerification()), "购买套餐事件不能要求核销");
|
|
||||||
} else {
|
|
||||||
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.PACKAGE_PURCHASE, "购买套餐事件必须通过套餐购买接口登记");
|
|
||||||
}
|
|
||||||
eventService.attachOptionGroupList(List.of(event));
|
eventService.attachOptionGroupList(List.of(event));
|
||||||
List<GaoEventRecordOption> canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList());
|
List<GaoEventRecordOption> canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList());
|
||||||
verifyRecordValue(event, record.getNumberValue());
|
verifyRecordValue(event, record.getNumberValue());
|
||||||
verifyRecordAttachment(event, record.getAttachmentList());
|
verifyRecordAttachment(event, record.getAttachmentList());
|
||||||
// 检查权限
|
// 检查权限
|
||||||
if (!orderConsume) {
|
checkEventPermission(event);
|
||||||
checkEventPermission(event);
|
|
||||||
}
|
|
||||||
int registeredDateValue = dateValue(registeredAt);
|
int registeredDateValue = dateValue(registeredAt);
|
||||||
long businessDayNo = businessDayService.resolveOpenBusinessDay(event.getStoreId(), registeredDateValue).getBusinessDayNo();
|
long businessDayNo = businessDayService.resolveOpenBusinessDay(event.getStoreId(), registeredDateValue).getBusinessDayNo();
|
||||||
|
|
||||||
@@ -383,6 +360,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
GaoEvent event = eventService.get(dbRecord.getEventId());
|
GaoEvent event = eventService.get(dbRecord.getEventId());
|
||||||
TimiException.required(event, "not found event");
|
TimiException.required(event, "not found event");
|
||||||
TimiException.required(event.getStoreId(), "not found event.storeId");
|
TimiException.required(event.getStoreId(), "not found event.storeId");
|
||||||
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件登记不可修改");
|
||||||
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.PACKAGE_PURCHASE, "购买套餐事件登记不可通过通用接口修改");
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.PACKAGE_PURCHASE, "购买套餐事件登记不可通过通用接口修改");
|
||||||
List<GaoEventRecordOption> canonicalOptionList = null;
|
List<GaoEventRecordOption> canonicalOptionList = null;
|
||||||
if (record.getSelectedOptionGroupList() != null) {
|
if (record.getSelectedOptionGroupList() != null) {
|
||||||
@@ -428,6 +406,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService<GaoEve
|
|||||||
GaoEventRecord record = get(id);
|
GaoEventRecord record = get(id);
|
||||||
TimiException.required(record, "not found event record");
|
TimiException.required(record, "not found event record");
|
||||||
GaoEvent event = eventService.get(record.getEventId());
|
GaoEvent event = eventService.get(record.getEventId());
|
||||||
|
TimiException.requiredTrue(event == null || event.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件登记不可删除");
|
||||||
TimiException.requiredTrue(event == null || event.getBizType() != GaoEvent.BizType.PACKAGE_PURCHASE, "购买套餐事件登记不可通过通用接口删除");
|
TimiException.requiredTrue(event == null || event.getBizType() != GaoEvent.BizType.PACKAGE_PURCHASE, "购买套餐事件登记不可通过通用接口删除");
|
||||||
pointTriggerService.revokeByEventRecord(record);
|
pointTriggerService.revokeByEventRecord(record);
|
||||||
GaoEventRecordOption optionExample = new GaoEventRecordOption();
|
GaoEventRecordOption optionExample = new GaoEventRecordOption();
|
||||||
|
|||||||
+5
@@ -85,6 +85,7 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
|
|||||||
|
|
||||||
event.setStatus(TimiJava.defaultIfNull(event.getStatus(), GaoEvent.Status.DRAFT));
|
event.setStatus(TimiJava.defaultIfNull(event.getStatus(), GaoEvent.Status.DRAFT));
|
||||||
event.setBizType(TimiJava.defaultIfNull(event.getBizType(), GaoEvent.BizType.GENERAL));
|
event.setBizType(TimiJava.defaultIfNull(event.getBizType(), GaoEvent.BizType.GENERAL));
|
||||||
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件类型只能由迁移流程标记");
|
||||||
event.setValueType(TimiJava.defaultIfNull(event.getValueType(), GaoEvent.ValueType.NONE));
|
event.setValueType(TimiJava.defaultIfNull(event.getValueType(), GaoEvent.ValueType.NONE));
|
||||||
event.setRequireValue(TimiJava.defaultIfNull(event.getRequireValue(), false));
|
event.setRequireValue(TimiJava.defaultIfNull(event.getRequireValue(), false));
|
||||||
event.setRequirePhoto(TimiJava.defaultIfNull(event.getRequirePhoto(), false));
|
event.setRequirePhoto(TimiJava.defaultIfNull(event.getRequirePhoto(), false));
|
||||||
@@ -122,6 +123,7 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
|
|||||||
TimiException.required(dbEvent, "not found event");
|
TimiException.required(dbEvent, "not found event");
|
||||||
TimiException.requiredTrue(dbEvent.getStoreId().equals(event.getStoreId()), "event.storeId invalid");
|
TimiException.requiredTrue(dbEvent.getStoreId().equals(event.getStoreId()), "event.storeId invalid");
|
||||||
TimiException.requiredTrue(dbEvent.getStatus() != GaoEvent.Status.DELETED, "已删除登记事件不可编辑");
|
TimiException.requiredTrue(dbEvent.getStatus() != GaoEvent.Status.DELETED, "已删除登记事件不可编辑");
|
||||||
|
TimiException.requiredTrue(dbEvent.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件不可编辑");
|
||||||
GaoEvent.ValueType oldValueType = dbEvent.getValueType();
|
GaoEvent.ValueType oldValueType = dbEvent.getValueType();
|
||||||
GaoEvent.ValueUnit oldValueUnit = dbEvent.getValueUnit();
|
GaoEvent.ValueUnit oldValueUnit = dbEvent.getValueUnit();
|
||||||
dbEvent.setName(event.getName());
|
dbEvent.setName(event.getName());
|
||||||
@@ -192,6 +194,7 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
|
|||||||
GaoEvent event = get(eventId);
|
GaoEvent event = get(eventId);
|
||||||
TimiException.required(event, "not found event");
|
TimiException.required(event, "not found event");
|
||||||
TimiException.requiredTrue(event.getStatus() != GaoEvent.Status.DELETED, "已删除登记事件不可编辑");
|
TimiException.requiredTrue(event.getStatus() != GaoEvent.Status.DELETED, "已删除登记事件不可编辑");
|
||||||
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件不可编辑");
|
||||||
verifyOptionGroupList(optionGroupList);
|
verifyOptionGroupList(optionGroupList);
|
||||||
saveOptionGroupList(eventId, optionGroupList);
|
saveOptionGroupList(eventId, optionGroupList);
|
||||||
logger.setLevel(Logger.Level.INFO);
|
logger.setLevel(Logger.Level.INFO);
|
||||||
@@ -217,6 +220,8 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
|
|||||||
try {
|
try {
|
||||||
logger.setContent(id);
|
logger.setContent(id);
|
||||||
GaoEvent event = get(id);
|
GaoEvent event = get(id);
|
||||||
|
TimiException.required(event, "not found event");
|
||||||
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件不可删除");
|
||||||
event.setStatus(GaoEvent.Status.DELETED);
|
event.setStatus(GaoEvent.Status.DELETED);
|
||||||
super.update(event);
|
super.update(event);
|
||||||
logger.setLevel(Logger.Level.INFO);
|
logger.setLevel(Logger.Level.INFO);
|
||||||
|
|||||||
+24
-12
@@ -7,6 +7,7 @@ import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
|
|||||||
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
|
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
|
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
|
import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
|
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest;
|
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest;
|
import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest;
|
||||||
@@ -36,6 +37,7 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
|
|||||||
|
|
||||||
private final GaoFundRecordService fundRecordService;
|
private final GaoFundRecordService fundRecordService;
|
||||||
private final GaoSalesOrderService orderService;
|
private final GaoSalesOrderService orderService;
|
||||||
|
private final GaoPointTriggerService pointTriggerService;
|
||||||
private final AttachmentService attachmentService;
|
private final AttachmentService attachmentService;
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
private final LoggerService loggerService;
|
private final LoggerService loggerService;
|
||||||
@@ -79,18 +81,23 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
|
|||||||
public GaoFundRecord verify(String fundRecordId) {
|
public GaoFundRecord verify(String fundRecordId) {
|
||||||
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_VERIFY", fundRecordId, () -> {
|
return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_VERIFY", fundRecordId, () -> {
|
||||||
GaoFundRecord record = lockOrderRecord(fundRecordId);
|
GaoFundRecord record = lockOrderRecord(fundRecordId);
|
||||||
TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态");
|
TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态");
|
||||||
GaoSalesOrder order = orderService.getForUpdate(record.getBizId());
|
GaoSalesOrder order = orderService.getForUpdate(record.getBizId());
|
||||||
order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount());
|
Long paidAtBefore = order.getPaidAt();
|
||||||
order.setPaidAmount(safe(order.getPaidAmount()) + record.getAmount());
|
order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount());
|
||||||
orderService.recomputePaymentStatus(order);
|
order.setPaidAmount(safe(order.getPaidAmount()) + record.getAmount());
|
||||||
String userId = userLoginService.getRequireLoginUserId();
|
orderService.recomputePaymentStatus(order);
|
||||||
TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化");
|
String userId = userLoginService.getRequireLoginUserId();
|
||||||
orderService.updatePayment(order);
|
TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化");
|
||||||
record.setStatus(GaoFundRecord.Status.VERIFIED);
|
orderService.updatePayment(order);
|
||||||
record.setVerifierUserId(userId);
|
record.setStatus(GaoFundRecord.Status.VERIFIED);
|
||||||
record.setVerifiedAt(Time.now());
|
record.setVerifierUserId(userId);
|
||||||
return record;
|
record.setVerifiedAt(Time.now());
|
||||||
|
if (paidAtBefore == null && order.getPaidAt() != null) {
|
||||||
|
// 订单首次全额实收,写入消费事实并触发订单积分
|
||||||
|
pointTriggerService.onSalesOrderPaid(order);
|
||||||
|
}
|
||||||
|
return record;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +216,7 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
|
|||||||
}
|
}
|
||||||
TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正");
|
TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正");
|
||||||
TimiException.requiredNull(fundRecordService.getRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正");
|
TimiException.requiredNull(fundRecordService.getRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正");
|
||||||
|
Long paidAtBefore = order.getPaidAt();
|
||||||
if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) {
|
if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) {
|
||||||
order.setPaidAmount(safe(order.getPaidAmount()) - source.getAmount());
|
order.setPaidAmount(safe(order.getPaidAmount()) - source.getAmount());
|
||||||
orderService.recomputePaymentStatus(order);
|
orderService.recomputePaymentStatus(order);
|
||||||
@@ -224,6 +232,10 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService {
|
|||||||
revoke.setReason(reason);
|
revoke.setReason(reason);
|
||||||
fundRecordService.createByOrderPayment(revoke);
|
fundRecordService.createByOrderPayment(revoke);
|
||||||
orderService.updatePayment(order);
|
orderService.updatePayment(order);
|
||||||
|
if (paidAtBefore != null && order.getPaymentStatus() != GaoSalesOrder.PaymentStatus.PAID) {
|
||||||
|
// 已全额实收的收款被冲正,撤销消费事实及其依赖积分
|
||||||
|
pointTriggerService.revokeBySalesOrder(order, reason);
|
||||||
|
}
|
||||||
TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "资金冲正必须上传照片");
|
TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "资金冲正必须上传照片");
|
||||||
attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList());
|
attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList());
|
||||||
return revoke;
|
return revoke;
|
||||||
|
|||||||
+109
-18
@@ -1,15 +1,19 @@
|
|||||||
package com.imyeyu.api.modules.gao.service.implement;
|
package com.imyeyu.api.modules.gao.service.implement;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
|
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerConsumptionRecordMapper;
|
||||||
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper;
|
import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper;
|
||||||
|
import com.imyeyu.api.modules.gao.script.GaoPointDecision;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
|
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
|
||||||
@@ -42,7 +46,9 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPointLedger, String> implements GaoPointLedgerService {
|
public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPointLedger, String> implements GaoPointLedgerService {
|
||||||
|
|
||||||
|
private final ObjectMapper jackson;
|
||||||
private final GaoPointLedgerMapper mapper;
|
private final GaoPointLedgerMapper mapper;
|
||||||
|
private final GaoPointLedgerConsumptionRecordMapper consumptionRecordMapper;
|
||||||
private final GaoPointAccountService accountService;
|
private final GaoPointAccountService accountService;
|
||||||
private final GaoCustomerService customerService;
|
private final GaoCustomerService customerService;
|
||||||
private final AttachmentService attachmentService;
|
private final AttachmentService attachmentService;
|
||||||
@@ -71,23 +77,35 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public GaoPointLedger applyRule(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload) {
|
public GaoPointLedger applyRule(GaoPointRule rule, GaoPointDecision decision, GroovyScriptExecutionResult<?> execution,
|
||||||
TimiException.required(record, "not found event record");
|
GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId,
|
||||||
|
String storeId, String customerId, Long occurredAt, String operatorUserId, List<String> consumptionRecordIdList) {
|
||||||
TimiException.required(rule, "not found point rule");
|
TimiException.required(rule, "not found point rule");
|
||||||
TimiException.requiredTrue(pointDelta != 0, "pointDelta invalid");
|
TimiException.required(decision, "not found point decision");
|
||||||
String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId());
|
TimiException.required(triggerSourceType, "not found triggerSourceType");
|
||||||
|
TimiException.required(triggerSourceId, "not found triggerSourceId");
|
||||||
|
TimiException.requiredTrue(decision.pointDelta() != 0, "pointDelta invalid");
|
||||||
|
long pointDelta = decision.pointDelta();
|
||||||
|
String idempotencyKey = earnKey(triggerSourceType, triggerSourceId, rule.getId());
|
||||||
GaoPointLedger.ChangeType changeType = pointDelta < 0 ? GaoPointLedger.ChangeType.CONSUME : GaoPointLedger.ChangeType.EARN;
|
GaoPointLedger.ChangeType changeType = pointDelta < 0 ? GaoPointLedger.ChangeType.CONSUME : GaoPointLedger.ChangeType.EARN;
|
||||||
return apply(record.getStoreId(), record.getCustomerId(), changeType, pointDelta, rule.getId(), record.getEventId(), record.getId(), sourcePayload, idempotencyKey, rule.getRemark(), record.getOperatorUserId(), record.getRegisteredAt());
|
GaoPointLedger ledger = apply(storeId, customerId, changeType, pointDelta, rule.getId(), triggerSourceType, triggerSourceId, salesOrderId,
|
||||||
|
ruleSnapshot(rule, execution), sourcePayload(decision, execution), idempotencyKey,
|
||||||
|
TimiJava.defaultIfNull(decision.reason(), rule.getName()), operatorUserId, occurredAt);
|
||||||
|
saveConsumptionDependencies(ledger.getId(), consumptionRecordIdList);
|
||||||
|
return ledger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record) {
|
public GaoPointLedger revoke(GaoPointLedger originLedger, String reason) {
|
||||||
TimiException.required(originLedger, "not found origin ledger");
|
TimiException.required(originLedger, "not found origin ledger");
|
||||||
TimiException.required(record, "not found event record");
|
TimiException.required(originLedger.getId(), "not found origin ledger.id");
|
||||||
TimiException.requiredTrue(originLedger.getPointDelta() != 0, "origin ledger pointDelta invalid");
|
TimiException.requiredTrue(originLedger.getPointDelta() != null && originLedger.getPointDelta() != 0, "origin ledger pointDelta invalid");
|
||||||
String idempotencyKey = "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId());
|
String idempotencyKey = "GAO_POINT_LEDGER:%s:REVOKE".formatted(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());
|
return apply(originLedger.getStoreId(), originLedger.getCustomerId(), GaoPointLedger.ChangeType.REVOKE, -originLedger.getPointDelta(),
|
||||||
|
originLedger.getRuleId(), originLedger.getTriggerSourceType(), originLedger.getTriggerSourceId(), originLedger.getSalesOrderId(),
|
||||||
|
originLedger.getRuleSnapshot(), originLedger.getSourcePayload(), idempotencyKey, reason,
|
||||||
|
userLoginService.getRequireLoginUserId(), Time.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@@ -102,13 +120,21 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
TimiException.requiredTrue(TimiJava.isNotEmpty(request.getRemark()), "not found adjust remark");
|
TimiException.requiredTrue(TimiJava.isNotEmpty(request.getRemark()), "not found adjust remark");
|
||||||
TimiException.required(operatorUserId, "not found operatorUserId");
|
TimiException.required(operatorUserId, "not found operatorUserId");
|
||||||
String idempotencyKey = "MANUAL_ADJUST:%s".formatted(request.getAdjustRequestId());
|
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());
|
return apply(request.getStoreId(), request.getCustomerId(), GaoPointLedger.ChangeType.ADJUST, request.getPointDelta(),
|
||||||
|
null, null, null, null, null, null, idempotencyKey, request.getRemark(), operatorUserId, Time.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<GaoPointLedger> listRuleChangeByEventRecordId(String eventRecordId) {
|
public List<GaoPointLedger> listRuleChangeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId) {
|
||||||
TimiException.required(eventRecordId, "not found eventRecordId");
|
TimiException.required(triggerSourceType, "not found triggerSourceType");
|
||||||
return mapper.selectRuleChangeByEventRecordId(eventRecordId);
|
TimiException.required(triggerSourceId, "not found triggerSourceId");
|
||||||
|
return mapper.selectRuleChangeByTriggerSource(triggerSourceType, triggerSourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GaoPointLedger> listRuleChangeByConsumptionRecordId(String consumptionRecordId) {
|
||||||
|
TimiException.required(consumptionRecordId, "not found consumptionRecordId");
|
||||||
|
return mapper.selectRuleChangeByConsumptionRecordId(consumptionRecordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -145,7 +171,62 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
private void saveConsumptionDependencies(String pointLedgerId, List<String> consumptionRecordIdList) {
|
||||||
|
if (TimiJava.isEmpty(consumptionRecordIdList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long now = Time.now();
|
||||||
|
for (String consumptionRecordId : consumptionRecordIdList) {
|
||||||
|
TimiException.required(consumptionRecordId, "not found consumptionRecordId");
|
||||||
|
consumptionRecordMapper.insert(pointLedgerId, consumptionRecordId, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String earnKey(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String ruleId) {
|
||||||
|
return switch (triggerSourceType) {
|
||||||
|
case EVENT_RECORD -> "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(triggerSourceId, ruleId);
|
||||||
|
case SALES_ORDER_PAID -> "GAO_SALES_ORDER:%s:RULE:%s:EARN".formatted(triggerSourceId, ruleId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode ruleSnapshot(GaoPointRule rule, GroovyScriptExecutionResult<?> execution) {
|
||||||
|
ObjectNode snapshot = jackson.createObjectNode();
|
||||||
|
snapshot.put("ruleId", rule.getId());
|
||||||
|
snapshot.put("ruleName", rule.getName());
|
||||||
|
if (rule.getRuleMode() != null) {
|
||||||
|
snapshot.put("ruleMode", rule.getRuleMode().name());
|
||||||
|
}
|
||||||
|
if (rule.getPointValue() != null) {
|
||||||
|
snapshot.put("pointValue", rule.getPointValue());
|
||||||
|
}
|
||||||
|
if (rule.getGroovyScriptId() != null) {
|
||||||
|
snapshot.put("groovyScriptId", rule.getGroovyScriptId());
|
||||||
|
}
|
||||||
|
if (execution != null) {
|
||||||
|
snapshot.put("groovyScriptVersionId", execution.versionId());
|
||||||
|
snapshot.put("groovyScriptVersion", execution.version());
|
||||||
|
snapshot.put("groovyScriptChecksum", execution.checksum());
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode sourcePayload(GaoPointDecision decision, GroovyScriptExecutionResult<?> execution) {
|
||||||
|
ObjectNode payload = jackson.createObjectNode();
|
||||||
|
payload.put("reason", decision.reason());
|
||||||
|
payload.set("consumptionRecordIdList", jackson.valueToTree(decision.consumptionRecordIdList()));
|
||||||
|
if (decision.conditionSnapshot() != null) {
|
||||||
|
payload.set("conditionSnapshot", jackson.valueToTree(decision.conditionSnapshot()));
|
||||||
|
}
|
||||||
|
if (execution != null) {
|
||||||
|
payload.put("groovyScriptId", execution.scriptId());
|
||||||
|
payload.put("groovyScriptVersionId", execution.versionId());
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GaoPointLedger apply(String storeId, String customerId, GaoPointLedger.ChangeType changeType, long pointDelta, String ruleId,
|
||||||
|
GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId, JsonNode ruleSnapshot,
|
||||||
|
JsonNode sourcePayload, String idempotencyKey, String remark, String operatorUserId, Long occurredAt) {
|
||||||
Logger logger = new Logger(Logger.Module.GAO, "GAO_POINT_LEDGER_APPLY");
|
Logger logger = new Logger(Logger.Module.GAO, "GAO_POINT_LEDGER_APPLY");
|
||||||
logger.setContent("storeId=%s, customerId=%s, changeType=%s, pointDelta=%d, idempotencyKey=%s, remark=%s".formatted(storeId, customerId, changeType, pointDelta, idempotencyKey, remark));
|
logger.setContent("storeId=%s, customerId=%s, changeType=%s, pointDelta=%d, idempotencyKey=%s, remark=%s".formatted(storeId, customerId, changeType, pointDelta, idempotencyKey, remark));
|
||||||
try {
|
try {
|
||||||
@@ -181,8 +262,11 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
ledger.setBalanceBefore(balanceBefore);
|
ledger.setBalanceBefore(balanceBefore);
|
||||||
ledger.setBalanceAfter(balanceAfter);
|
ledger.setBalanceAfter(balanceAfter);
|
||||||
ledger.setRuleId(ruleId);
|
ledger.setRuleId(ruleId);
|
||||||
ledger.setEventId(eventId);
|
ledger.setTriggerSourceType(triggerSourceType);
|
||||||
ledger.setEventRecordId(eventRecordId);
|
ledger.setTriggerSourceId(triggerSourceId);
|
||||||
|
ledger.setSalesOrderId(salesOrderId);
|
||||||
|
ledger.setGroovyScriptVersionId(executionVersionId(ruleSnapshot));
|
||||||
|
ledger.setRuleSnapshot(ruleSnapshot);
|
||||||
ledger.setSourcePayload(sourcePayload);
|
ledger.setSourcePayload(sourcePayload);
|
||||||
ledger.setIdempotencyKey(idempotencyKey);
|
ledger.setIdempotencyKey(idempotencyKey);
|
||||||
ledger.setRemark(remark);
|
ledger.setRemark(remark);
|
||||||
@@ -207,4 +291,11 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
loggerService.create(logger);
|
loggerService.create(logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String executionVersionId(JsonNode ruleSnapshot) {
|
||||||
|
if (ruleSnapshot == null || ruleSnapshot.get("groovyScriptVersionId") == null || ruleSnapshot.get("groovyScriptVersionId").isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ruleSnapshot.get("groovyScriptVersionId").asText();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-127
@@ -1,12 +1,13 @@
|
|||||||
package com.imyeyu.api.modules.gao.service.implement;
|
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.config.dbsource.TimiServerDBConfig;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
import com.imyeyu.api.modules.common.entity.GroovyScript;
|
||||||
|
import com.imyeyu.api.modules.common.service.GroovyScriptService;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoEvent;
|
import com.imyeyu.api.modules.gao.entity.GaoEvent;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||||
import com.imyeyu.api.modules.gao.mapper.GaoPointRuleMapper;
|
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.api.modules.gao.service.GaoEventService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
import com.imyeyu.java.bean.timi.TimiException;
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
import com.imyeyu.spring.mapper.BaseMapper;
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
@@ -15,9 +16,9 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -30,10 +31,9 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPointRule, String> implements GaoPointRuleService {
|
public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPointRule, String> implements GaoPointRuleService {
|
||||||
|
|
||||||
private static final int MAX_TIER_COUNT = 100;
|
|
||||||
|
|
||||||
private final GaoPointRuleMapper mapper;
|
private final GaoPointRuleMapper mapper;
|
||||||
private final GaoEventService eventService;
|
private final GaoEventService eventService;
|
||||||
|
private final GroovyScriptService groovyScriptService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected BaseMapper<GaoPointRule, String> mapper() {
|
protected BaseMapper<GaoPointRule, String> mapper() {
|
||||||
@@ -41,10 +41,10 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<GaoPointRule> listActive(String storeId, String eventId, long now) {
|
public List<GaoPointRule> listActive(String storeId, GaoPointRule.TriggerSourceType triggerSourceType, String triggerRefId, long now) {
|
||||||
TimiException.required(storeId, "not found storeId");
|
TimiException.required(storeId, "not found storeId");
|
||||||
TimiException.required(eventId, "not found eventId");
|
TimiException.required(triggerSourceType, "not found triggerSourceType");
|
||||||
return mapper.selectActiveByStoreIdAndEventId(storeId, eventId, now);
|
return mapper.selectActiveByTrigger(storeId, triggerSourceType, triggerRefId, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -89,9 +89,7 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPoint
|
|||||||
public void create(GaoPointRule rule) {
|
public void create(GaoPointRule rule) {
|
||||||
TimiException.required(rule, "not found rule");
|
TimiException.required(rule, "not found rule");
|
||||||
TimiException.required(rule.getStoreId(), "not found rule.storeId");
|
TimiException.required(rule.getStoreId(), "not found rule.storeId");
|
||||||
TimiException.required(rule.getEventId(), "not found rule.eventId");
|
|
||||||
TimiException.required(rule.getName(), "not found rule.name");
|
TimiException.required(rule.getName(), "not found rule.name");
|
||||||
checkEvent(rule.getStoreId(), rule.getEventId());
|
|
||||||
rule.setStatus(TimiJava.defaultIfNull(rule.getStatus(), GaoPointRule.Status.DRAFT));
|
rule.setStatus(TimiJava.defaultIfNull(rule.getStatus(), GaoPointRule.Status.DRAFT));
|
||||||
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
|
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
|
||||||
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
|
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
|
||||||
@@ -105,22 +103,21 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPoint
|
|||||||
TimiException.required(rule, "not found rule");
|
TimiException.required(rule, "not found rule");
|
||||||
TimiException.required(rule.getId(), "not found rule.id");
|
TimiException.required(rule.getId(), "not found rule.id");
|
||||||
TimiException.required(rule.getStoreId(), "not found rule.storeId");
|
TimiException.required(rule.getStoreId(), "not found rule.storeId");
|
||||||
TimiException.required(rule.getEventId(), "not found rule.eventId");
|
|
||||||
TimiException.required(rule.getName(), "not found rule.name");
|
TimiException.required(rule.getName(), "not found rule.name");
|
||||||
GaoPointRule dbRule = get(rule.getId());
|
GaoPointRule dbRule = get(rule.getId());
|
||||||
TimiException.required(dbRule, "not found rule");
|
TimiException.required(dbRule, "not found rule");
|
||||||
TimiException.requiredTrue(dbRule.getStoreId().equals(rule.getStoreId()), "rule.storeId invalid");
|
TimiException.requiredTrue(dbRule.getStoreId().equals(rule.getStoreId()), "rule.storeId invalid");
|
||||||
checkEvent(rule.getStoreId(), rule.getEventId());
|
|
||||||
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
|
rule.setPriority(TimiJava.defaultIfNull(rule.getPriority(), 0));
|
||||||
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
|
rule.setExclusive(TimiJava.defaultIfNull(rule.getExclusive(), false));
|
||||||
verifyRule(rule);
|
verifyRule(rule);
|
||||||
dbRule.setEventId(rule.getEventId());
|
dbRule.setRuleMode(rule.getRuleMode());
|
||||||
|
dbRule.setTriggerSourceType(rule.getTriggerSourceType());
|
||||||
|
dbRule.setTriggerRefType(rule.getTriggerRefType());
|
||||||
|
dbRule.setTriggerRefId(rule.getTriggerRefId());
|
||||||
dbRule.setName(rule.getName());
|
dbRule.setName(rule.getName());
|
||||||
dbRule.setStatus(rule.getStatus());
|
dbRule.setStatus(rule.getStatus());
|
||||||
dbRule.setTriggerType(rule.getTriggerType());
|
|
||||||
dbRule.setPointMode(rule.getPointMode());
|
|
||||||
dbRule.setPointValue(rule.getPointValue());
|
dbRule.setPointValue(rule.getPointValue());
|
||||||
dbRule.setConditionJson(rule.getConditionJson());
|
dbRule.setGroovyScriptId(rule.getGroovyScriptId());
|
||||||
dbRule.setBeginAt(rule.getBeginAt());
|
dbRule.setBeginAt(rule.getBeginAt());
|
||||||
dbRule.setEndAt(rule.getEndAt());
|
dbRule.setEndAt(rule.getEndAt());
|
||||||
dbRule.setPriority(rule.getPriority());
|
dbRule.setPriority(rule.getPriority());
|
||||||
@@ -140,123 +137,33 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPoint
|
|||||||
super.delete(id);
|
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) {
|
private void verifyRule(GaoPointRule rule) {
|
||||||
TimiException.required(rule.getStatus(), "not found rule.status");
|
TimiException.required(rule.getStatus(), "not found rule.status");
|
||||||
TimiException.required(rule.getTriggerType(), "not found rule.triggerType");
|
TimiException.required(rule.getRuleMode(), "not found rule.ruleMode");
|
||||||
TimiException.required(rule.getPointMode(), "not found rule.pointMode");
|
TimiException.required(rule.getTriggerSourceType(), "not found rule.triggerSourceType");
|
||||||
TimiException.requiredTrue(rule.getPriority() != null && 0 <= rule.getPriority(), "rule.priority invalid");
|
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");
|
TimiException.requiredTrue(rule.getBeginAt() == null || rule.getEndAt() == null || rule.getBeginAt() <= rule.getEndAt(), "rule.beginAt or rule.endAt invalid");
|
||||||
|
TimiException.requiredTrue(rule.getTriggerRefType() == null || rule.getTriggerRefType() == GaoPointRule.TriggerRefType.EVENT, "rule.triggerRefType invalid");
|
||||||
|
TimiException.requiredTrue(rule.getTriggerRefId() == null || rule.getTriggerRefType() != null, "触发对象筛选 ID 必须搭配触发对象筛选类型");
|
||||||
|
if (rule.getTriggerRefType() == GaoPointRule.TriggerRefType.EVENT) {
|
||||||
|
TimiException.required(rule.getTriggerRefId(), "not found rule.triggerRefId");
|
||||||
|
GaoEvent event = eventService.get(rule.getTriggerRefId());
|
||||||
|
TimiException.required(event, "触发对象事件不存在");
|
||||||
|
TimiException.requiredTrue(rule.getStoreId().equals(event.getStoreId()), "规则和触发对象事件不属于同一门店");
|
||||||
|
TimiException.requiredTrue(event.getBizType() != GaoEvent.BizType.LEGACY_CONSUMPTION, "退役消费事件不能作为触发对象");
|
||||||
|
}
|
||||||
|
|
||||||
if (rule.getTriggerType() == GaoPointRule.TriggerType.SOURCE_VALUE) {
|
if (rule.getRuleMode() == GaoPointRule.RuleMode.SIMPLE) {
|
||||||
TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.SOURCE_VALUE, "SOURCE_VALUE 规则必须使用 SOURCE_VALUE 积分值模式");
|
TimiException.requiredNull(rule.getGroovyScriptId(), "简单规则不能绑定 Groovy 脚本");
|
||||||
TimiException.requiredNull(rule.getPointValue(), "SOURCE_VALUE 规则不能填写固定积分值");
|
TimiException.required(rule.getPointValue(), "not found rule.pointValue");
|
||||||
verifySourceCondition(rule.getConditionJson());
|
TimiException.requiredTrue(rule.getPointValue() != 0, "rule.pointValue invalid");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule.getTriggerType() == GaoPointRule.TriggerType.CUSTOMER_EVENT_VALUE_TIER) {
|
TimiException.required(rule.getGroovyScriptId(), "not found rule.groovyScriptId");
|
||||||
TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.TIER, "CUSTOMER_EVENT_VALUE_TIER 规则必须使用 TIER 积分值模式");
|
TimiException.requiredNull(rule.getPointValue(), "Groovy 规则不能填写固定积分值");
|
||||||
TimiException.requiredNull(rule.getPointValue(), "TIER 规则不能填写规则级积分值");
|
GroovyScript script = groovyScriptService.get(rule.getGroovyScriptId());
|
||||||
verifyTierCondition(rule.getStoreId(), rule.getConditionJson());
|
TimiException.required(script, "Groovy 脚本不存在");
|
||||||
return;
|
TimiException.requiredTrue(script.getBizType() == GroovyScript.BizType.GAO_POINT_RULE, "Groovy 脚本业务归属无效");
|
||||||
}
|
|
||||||
|
|
||||||
TimiException.requiredTrue(rule.getPointMode() == GaoPointRule.PointMode.FIXED, "固定触发规则必须使用 FIXED 积分值模式");
|
|
||||||
TimiException.required(rule.getPointValue(), "not found rule.pointValue");
|
|
||||||
TimiException.requiredTrue(rule.getPointValue() != 0, "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 verifyTierCondition(String storeId, JsonNode conditionJson) {
|
|
||||||
JsonNode condition = requireObjectCondition(conditionJson);
|
|
||||||
JsonNode sourceEventIdNode = condition.get("sourceEventId");
|
|
||||||
TimiException.requiredTrue(sourceEventIdNode != null && sourceEventIdNode.isTextual() && TimiJava.isNotEmpty(sourceEventIdNode.asText().trim()), "TIER.sourceEventId invalid");
|
|
||||||
String sourceEventId = sourceEventIdNode.asText().trim();
|
|
||||||
GaoEvent sourceEvent = eventService.get(sourceEventId);
|
|
||||||
TimiException.required(sourceEvent, "TIER.sourceEventId 对应事件不存在");
|
|
||||||
TimiException.requiredTrue(storeId.equals(sourceEvent.getStoreId()), "TIER 来源事件和规则不属于同一门店");
|
|
||||||
TimiException.requiredTrue(sourceEvent.getValueType() == GaoEvent.ValueType.NUMBER, "TIER 来源事件必须配置为 NUMBER 类型");
|
|
||||||
TimiException.required(sourceEvent.getValueUnit(), "TIER 来源事件必须配置数值单位");
|
|
||||||
|
|
||||||
JsonNode sourceValueField = condition.get("sourceValueField");
|
|
||||||
TimiException.requiredTrue(sourceValueField != null && sourceValueField.isTextual() && "numberValue".equals(sourceValueField.asText()), "TIER.sourceValueField 目前只能是 numberValue");
|
|
||||||
|
|
||||||
JsonNode window = condition.get("window");
|
|
||||||
TimiException.requiredTrue(window != null && window.isObject(), "TIER.window 必须是对象");
|
|
||||||
JsonNode windowType = window.get("type");
|
|
||||||
TimiException.requiredTrue(windowType != null && windowType.isTextual() && "ROLLING".equals(windowType.asText()), "TIER.window.type 目前只能是 ROLLING");
|
|
||||||
JsonNode windowAmount = window.get("amount");
|
|
||||||
TimiException.requiredTrue(windowAmount != null && windowAmount.isIntegralNumber() && 0 < windowAmount.longValue(), "TIER.window.amount invalid");
|
|
||||||
JsonNode windowUnit = window.get("unit");
|
|
||||||
TimiException.requiredTrue(windowUnit != null && windowUnit.isTextual() && "YEAR".equals(windowUnit.asText()), "TIER.window.unit 目前只能是 YEAR");
|
|
||||||
|
|
||||||
JsonNode tiers = condition.get("tiers");
|
|
||||||
TimiException.requiredTrue(tiers != null && tiers.isArray() && !tiers.isEmpty(), "TIER.tiers 不能为空");
|
|
||||||
TimiException.requiredTrue(tiers.size() <= MAX_TIER_COUNT, "TIER.tiers 数量不能超过 %d".formatted(MAX_TIER_COUNT));
|
|
||||||
Long previousMax = null;
|
|
||||||
for (int i = 0; i < tiers.size(); i++) {
|
|
||||||
JsonNode tier = tiers.get(i);
|
|
||||||
TimiException.requiredTrue(tier != null && tier.isObject(), "TIER.tiers[%d] 必须是对象".formatted(i));
|
|
||||||
JsonNode minNode = tier.get("min");
|
|
||||||
TimiException.requiredTrue(minNode != null && minNode.isIntegralNumber() && 0 <= minNode.longValue(), "TIER.tiers[%d].min invalid".formatted(i));
|
|
||||||
long min = minNode.longValue();
|
|
||||||
if (0 < i) {
|
|
||||||
TimiException.requiredTrue(previousMax != null && min == previousMax + 1, "TIER 分档必须连续且不能重叠");
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonNode maxNode = tier.get("max");
|
|
||||||
Long max = null;
|
|
||||||
if (maxNode != null && !maxNode.isNull()) {
|
|
||||||
TimiException.requiredTrue(maxNode.isIntegralNumber(), "TIER.tiers[%d].max invalid".formatted(i));
|
|
||||||
max = maxNode.longValue();
|
|
||||||
TimiException.requiredTrue(min <= max, "TIER.tiers[%d].min 或 max invalid".formatted(i));
|
|
||||||
}
|
|
||||||
if (i < tiers.size() - 1) {
|
|
||||||
TimiException.requiredTrue(max != null, "除最后一档外 TIER.max 不能为空");
|
|
||||||
TimiException.requiredTrue(max < Long.MAX_VALUE, "TIER 分档上限过大,无法继续配置下一档");
|
|
||||||
} else {
|
|
||||||
TimiException.requiredTrue(max == null, "最后一档 TIER.max 必须为空");
|
|
||||||
}
|
|
||||||
JsonNode pointValue = tier.get("pointValue");
|
|
||||||
TimiException.requiredTrue(pointValue != null && pointValue.isIntegralNumber(), "TIER.tiers[%d].pointValue invalid".formatted(i));
|
|
||||||
previousMax = max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 || min != 0, "SOURCE_VALUE.min invalid");
|
|
||||||
TimiException.requiredTrue(max == null || max != 0, "SOURCE_VALUE.max invalid");
|
|
||||||
TimiException.requiredTrue(min == null || max == null || min <= max, "SOURCE_VALUE.min or SOURCE_VALUE.max invalid");
|
|
||||||
TimiException.requiredTrue(min == null || max == null || (min < 0) == (max < 0), "SOURCE_VALUE.min or SOURCE_VALUE.max direction 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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+192
-108
@@ -1,22 +1,24 @@
|
|||||||
package com.imyeyu.api.modules.gao.service.implement;
|
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.config.dbsource.TimiServerDBConfig;
|
||||||
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult;
|
||||||
|
import com.imyeyu.api.modules.common.entity.GroovyScript;
|
||||||
|
import com.imyeyu.api.modules.common.service.GroovyScriptService;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
import com.imyeyu.api.modules.gao.entity.GaoEventRecord;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
import com.imyeyu.api.modules.gao.entity.GaoPointLedger;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
import com.imyeyu.api.modules.gao.entity.GaoPointRule;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoSalesOrder;
|
||||||
|
import com.imyeyu.api.modules.gao.script.GaoPointDecision;
|
||||||
|
import com.imyeyu.api.modules.gao.script.GaoPointScriptContext;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
|
import com.imyeyu.api.modules.gao.service.GaoPointLedgerService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
|
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest;
|
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.api.modules.user.service.UserLoginService;
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
import com.imyeyu.java.bean.timi.TimiException;
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
@@ -26,26 +28,28 @@ import org.springframework.beans.factory.ObjectProvider;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.util.HashMap;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/// GAO 客户积分触发服务实现
|
/// GAO 客户积分触发服务实现
|
||||||
///
|
///
|
||||||
|
/// 简单规则直接发放固定积分,复杂规则由版本化 Groovy 脚本返回声明式积分决策;
|
||||||
|
/// 脚本只接收归一化上下文和只读消费事实查询,积分服务统一负责幂等与写账
|
||||||
|
///
|
||||||
/// @author Codex
|
/// @author Codex
|
||||||
/// @since 2026-08-11
|
/// @since 2026-08-11
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
||||||
|
|
||||||
private final ObjectMapper jackson;
|
|
||||||
private final PermissionChecker permissionChecker;
|
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
private final GaoPointRuleService ruleService;
|
private final GaoPointRuleService ruleService;
|
||||||
private final GaoPointLedgerService ledgerService;
|
private final GaoPointLedgerService ledgerService;
|
||||||
|
private final GaoCustomerConsumptionRecordService consumptionService;
|
||||||
|
private final GroovyScriptService groovyScriptService;
|
||||||
private final GaoCustomerService customerService;
|
private final GaoCustomerService customerService;
|
||||||
private final ObjectProvider<GaoEventRecordService> eventRecordServiceProvider;
|
private final ObjectProvider<GaoEventRecordService> eventRecordServiceProvider;
|
||||||
|
|
||||||
@@ -59,22 +63,16 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
|||||||
TimiException.required(record.getEventId(), "not found record.eventId");
|
TimiException.required(record.getEventId(), "not found record.eventId");
|
||||||
TimiException.required(record.getRegisteredAt(), "not found record.registeredAt");
|
TimiException.required(record.getRegisteredAt(), "not found record.registeredAt");
|
||||||
|
|
||||||
List<GaoPointRule> ruleList = ruleService.listActive(record.getStoreId(), record.getEventId(), Time.now());
|
|
||||||
Long count = null;
|
|
||||||
long pointDeltaTotal = 0L;
|
long pointDeltaTotal = 0L;
|
||||||
for (GaoPointRule rule : ruleList) {
|
for (GaoPointRule rule : ruleService.listActive(record.getStoreId(), GaoPointRule.TriggerSourceType.EVENT_RECORD, record.getEventId(), record.getRegisteredAt())) {
|
||||||
RuleResolution resolution = resolveRule(record, rule, count);
|
GaoPointScriptContext context = eventContext(rule, record);
|
||||||
if (resolution == null) {
|
GroovyScriptExecutionResult<GaoPointDecision> execution = evaluate(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(),
|
||||||
|
GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId());
|
||||||
|
if (execution == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (rule.getTriggerType() == GaoPointRule.TriggerType.FIRST_TIME || rule.getTriggerType() == GaoPointRule.TriggerType.AFTER_COUNT_EVERY_TIME) {
|
pointDeltaTotal += applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), null,
|
||||||
count = count == null ? count(record) : count;
|
record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), record.getOperatorUserId(), context);
|
||||||
}
|
|
||||||
long pointDelta = resolution.pointDelta();
|
|
||||||
if (pointDelta != 0) {
|
|
||||||
ledgerService.applyRule(record, rule, pointDelta, resolution.sourcePayload());
|
|
||||||
pointDeltaTotal = Math.addExact(pointDeltaTotal, pointDelta);
|
|
||||||
}
|
|
||||||
if (Boolean.TRUE.equals(rule.getExclusive())) {
|
if (Boolean.TRUE.equals(rule.getExclusive())) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -87,19 +85,60 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
|||||||
@Override
|
@Override
|
||||||
public void revokeByEventRecord(GaoEventRecord record) {
|
public void revokeByEventRecord(GaoEventRecord record) {
|
||||||
TimiException.required(record, "not found event record");
|
TimiException.required(record, "not found event record");
|
||||||
List<GaoPointLedger> originLedgerList = ledgerService.listRuleChangeByEventRecordId(record.getId());
|
TimiException.required(record.getId(), "not found event record.id");
|
||||||
if (originLedgerList.isEmpty()) {
|
revokeByTriggerSource(GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), "撤销登记记录积分");
|
||||||
return;
|
GaoCustomerConsumptionRecord fact = consumptionService.getBySource(GaoCustomerConsumptionRecord.SourceType.LEGACY_EVENT, record.getId());
|
||||||
|
if (fact != null) {
|
||||||
|
consumptionService.revokeLegacyEvent(record.getId(), "撤销历史消费事件对应的消费事实");
|
||||||
|
revokeDependentLedgers(fact.getId());
|
||||||
}
|
}
|
||||||
Set<String> revokedKeySet = new HashSet<>();
|
}
|
||||||
List<String> revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList();
|
|
||||||
for (GaoPointLedger ledger : ledgerService.listByIdempotencyKeyList(revokeKeyList)) {
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
revokedKeySet.add(ledger.getIdempotencyKey());
|
@Override
|
||||||
|
public void onSalesOrderPaid(GaoSalesOrder order) {
|
||||||
|
TimiException.required(order, "not found sales order");
|
||||||
|
TimiException.required(order.getId(), "not found sales order.id");
|
||||||
|
TimiException.required(order.getStoreId(), "not found order.storeId");
|
||||||
|
TimiException.required(order.getCustomerId(), "not found order.customerId");
|
||||||
|
TimiException.requiredTrue(order.getPaymentStatus() == GaoSalesOrder.PaymentStatus.PAID, "订单尚未全额实收");
|
||||||
|
TimiException.required(order.getPaidAt(), "not found order.paidAt");
|
||||||
|
if (order.getSaleAmount() != null && 0 < order.getSaleAmount()) {
|
||||||
|
consumptionService.createSalesOrder(order.getId(), order.getStoreId(), order.getCustomerId(), order.getSaleAmount(), order.getPaidAt(), order.getOrderType());
|
||||||
}
|
}
|
||||||
for (GaoPointLedger originLedger : originLedgerList) {
|
for (GaoPointRule rule : ruleService.listActive(order.getStoreId(), GaoPointRule.TriggerSourceType.SALES_ORDER_PAID, null, order.getPaidAt())) {
|
||||||
if (!revokedKeySet.contains(revokeKey(record, originLedger))) {
|
GaoPointScriptContext context = orderContext(rule, order);
|
||||||
ledgerService.revoke(originLedger, record);
|
GroovyScriptExecutionResult<GaoPointDecision> execution = evaluate(rule, order.getStoreId(), order.getCustomerId(), order.getPaidAt(), null, null);
|
||||||
|
if (execution == null) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.SALES_ORDER_PAID, order.getId(), order.getId(),
|
||||||
|
order.getStoreId(), order.getCustomerId(), order.getPaidAt(), order.getOperatorUserId(), context);
|
||||||
|
if (Boolean.TRUE.equals(rule.getExclusive())) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void revokeBySalesOrder(GaoSalesOrder order, String reason) {
|
||||||
|
TimiException.required(order, "not found sales order");
|
||||||
|
TimiException.required(order.getId(), "not found sales order.id");
|
||||||
|
TimiException.required(reason, "not found revoke reason");
|
||||||
|
revokeByTriggerSource(GaoPointLedger.TriggerSourceType.SALES_ORDER_PAID, order.getId(), reason);
|
||||||
|
GaoCustomerConsumptionRecord fact = consumptionService.getBySource(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER, order.getId());
|
||||||
|
if (fact != null) {
|
||||||
|
consumptionService.revokeSalesOrder(order.getId(), reason);
|
||||||
|
} else if (TimiJava.isNotEmpty(order.getSourceEventRecordId())) {
|
||||||
|
// 切换前的旧订单通过原事件记录映射定位迁移出的消费事实
|
||||||
|
fact = consumptionService.getBySource(GaoCustomerConsumptionRecord.SourceType.LEGACY_EVENT, order.getSourceEventRecordId());
|
||||||
|
if (fact != null) {
|
||||||
|
consumptionService.revokeLegacyEvent(order.getSourceEventRecordId(), reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fact != null) {
|
||||||
|
revokeDependentLedgers(fact.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,94 +153,139 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
|||||||
return ledgerService.adjust(request, userLoginService.getRequireLoginUserId());
|
return ledgerService.adjust(request, userLoginService.getRequireLoginUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private RuleResolution resolveRule(GaoEventRecord record, GaoPointRule rule, Long count) {
|
/// 按规则评估积分决策
|
||||||
return switch (rule.getTriggerType()) {
|
///
|
||||||
case EVERY_TIME -> new RuleResolution(rule.getPointValue(), null);
|
/// 简单规则直接构造固定积分决策;Groovy 规则执行发布版本脚本;脚本未返回决策视为未命中
|
||||||
case FIRST_TIME -> {
|
///
|
||||||
long currentCount = count == null ? count(record) : count;
|
/// @param rule 积分规则
|
||||||
yield currentCount == 1 ? new RuleResolution(rule.getPointValue(), null) : null;
|
/// @param storeId 门店 ID
|
||||||
}
|
/// @param customerId 客户 ID
|
||||||
case AFTER_COUNT_EVERY_TIME -> {
|
/// @param occurredAt 发生时间
|
||||||
JsonNode condition = rule.getConditionJson();
|
/// @param triggerRefType 触发对象筛选类型,可空
|
||||||
long threshold = condition.get("threshold").longValue();
|
/// @param triggerRefId 触发对象筛选 ID,可空
|
||||||
boolean includeCurrent = condition.get("includeCurrent") == null || condition.get("includeCurrent").isNull() || condition.get("includeCurrent").asBoolean();
|
/// @return 脚本执行结果,未命中返回 null
|
||||||
long currentCount = count == null ? count(record) : count;
|
private GroovyScriptExecutionResult<GaoPointDecision> evaluate(GaoPointRule rule, String storeId, String customerId, long occurredAt, String triggerRefType, String triggerRefId) {
|
||||||
yield includeCurrent ? threshold <= currentCount ? new RuleResolution(rule.getPointValue(), null) : null : threshold < currentCount ? new RuleResolution(rule.getPointValue(), null) : null;
|
if (rule.getRuleMode() == GaoPointRule.RuleMode.SIMPLE) {
|
||||||
}
|
GaoPointDecision decision = new GaoPointDecision(rule.getPointValue(), rule.getName(), Map.of("ruleMode", GaoPointRule.RuleMode.SIMPLE.name()), List.of());
|
||||||
case SOURCE_VALUE -> resolveSourceValue(record, rule);
|
return new GroovyScriptExecutionResult<>(null, null, null, null, decision);
|
||||||
case CUSTOMER_EVENT_VALUE_TIER -> resolveTier(record, rule);
|
}
|
||||||
};
|
GroovyScript script = groovyScriptService.get(rule.getGroovyScriptId());
|
||||||
|
TimiException.required(script, "规则绑定的 Groovy 脚本不存在");
|
||||||
|
GaoPointScriptContext context = buildContext(rule, storeId, customerId, occurredAt, triggerRefType, triggerRefId);
|
||||||
|
Map<String, Object> bindings = new HashMap<>();
|
||||||
|
bindings.put("context", context);
|
||||||
|
return groovyScriptService.executePublished(script.getBizType(), script.getCode(), bindings, GaoPointDecision.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RuleResolution resolveSourceValue(GaoEventRecord record, GaoPointRule rule) {
|
/// 写入积分流水并累计决策结果
|
||||||
if (record.getPointValue() == null) {
|
///
|
||||||
return null;
|
/// @return 本次决策的积分变化
|
||||||
}
|
private long applyDecision(GaoPointRule rule, GroovyScriptExecutionResult<GaoPointDecision> execution,
|
||||||
permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.POINT_SOURCE_VALUE.getValue());
|
GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId,
|
||||||
long pointValue = record.getPointValue();
|
String storeId, String customerId, Long occurredAt, String operatorUserId, GaoPointScriptContext context) {
|
||||||
TimiException.requiredTrue(pointValue != 0, "pointValue invalid");
|
GaoPointDecision decision = execution.result();
|
||||||
JsonNode condition = rule.getConditionJson();
|
TimiException.required(decision, "规则未返回积分决策");
|
||||||
if (condition != null) {
|
TimiException.requiredTrue(decision.pointDelta() != 0, "规则积分变化不能为 0");
|
||||||
JsonNode min = condition.get("min");
|
ledgerService.applyRule(rule, decision, isGroovy(execution) ? execution : null, triggerSourceType, triggerSourceId, salesOrderId,
|
||||||
JsonNode max = condition.get("max");
|
storeId, customerId, occurredAt, operatorUserId, decision.consumptionRecordIdList());
|
||||||
TimiException.requiredTrue(min == null || min.isNull() || min.longValue() <= pointValue, "pointValue 小于规则最小值");
|
return decision.pointDelta();
|
||||||
TimiException.requiredTrue(max == null || max.isNull() || pointValue <= max.longValue(), "pointValue 大于规则最大值");
|
|
||||||
}
|
|
||||||
ObjectNode payload = jackson.createObjectNode();
|
|
||||||
payload.put("submittedPointValue", pointValue);
|
|
||||||
payload.put("validatedPointValue", pointValue);
|
|
||||||
return new RuleResolution(pointValue, payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RuleResolution resolveTier(GaoEventRecord record, GaoPointRule rule) {
|
/// 撤销指定触发来源产生的全部规则积分
|
||||||
JsonNode condition = rule.getConditionJson();
|
private void revokeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String reason) {
|
||||||
String sourceEventId = condition.get("sourceEventId").asText();
|
List<GaoPointLedger> originLedgerList = ledgerService.listRuleChangeByTriggerSource(triggerSourceType, triggerSourceId);
|
||||||
long amount = condition.get("window").get("amount").longValue();
|
revokeAll(originLedgerList, reason);
|
||||||
ZoneId zone = ZoneId.systemDefault();
|
}
|
||||||
LocalDate currentDate = Instant.ofEpochMilli(record.getRegisteredAt()).atZone(zone).toLocalDate();
|
|
||||||
long windowBeginAt = currentDate.minusYears(amount).atStartOfDay(zone).toInstant().toEpochMilli();
|
|
||||||
long windowEndAt = Math.addExact(record.getRegisteredAt(), 1L);
|
|
||||||
long sumValue = eventRecordServiceProvider.getObject().sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt);
|
|
||||||
|
|
||||||
JsonNode matchedTier = null;
|
/// 复算并撤销依赖指定消费事实的条件积分
|
||||||
for (JsonNode tier : condition.get("tiers")) {
|
///
|
||||||
long min = tier.get("min").longValue();
|
/// 按原积分流水保存的脚本版本和原触发时间重算,只有不再满足原条件的积分才撤销
|
||||||
JsonNode maxNode = tier.get("max");
|
private void revokeDependentLedgers(String consumptionRecordId) {
|
||||||
if (min <= sumValue && (maxNode == null || maxNode.isNull() || sumValue <= maxNode.longValue())) {
|
List<GaoPointLedger> dependentList = ledgerService.listRuleChangeByConsumptionRecordId(consumptionRecordId);
|
||||||
matchedTier = tier;
|
if (dependentList.isEmpty()) {
|
||||||
break;
|
return;
|
||||||
|
}
|
||||||
|
List<GaoPointLedger> revokeCandidateList = new java.util.ArrayList<>();
|
||||||
|
for (GaoPointLedger origin : dependentList) {
|
||||||
|
if (!isGroovyLedger(origin) || stillSatisfied(origin)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
revokeCandidateList.add(origin);
|
||||||
|
}
|
||||||
|
revokeAll(revokeCandidateList, "撤销依赖已失效消费事实的积分");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 判断条件积分按原脚本版本和原触发时间重算后是否仍满足
|
||||||
|
private boolean stillSatisfied(GaoPointLedger origin) {
|
||||||
|
if (TimiJava.isEmpty(origin.getGroovyScriptVersionId()) || origin.getTriggerSourceType() == null) {
|
||||||
|
// 无脚本版本的条件积分无法复算,保守起见视为不再满足
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GaoEventRecord record = null;
|
||||||
|
if (origin.getTriggerSourceType() == GaoPointLedger.TriggerSourceType.EVENT_RECORD) {
|
||||||
|
record = eventRecordServiceProvider.getObject().get(origin.getTriggerSourceId());
|
||||||
|
if (record == null) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matchedTier == null) {
|
GaoPointRule replayRule = new GaoPointRule();
|
||||||
return null;
|
replayRule.setRuleMode(GaoPointRule.RuleMode.GROOVY);
|
||||||
|
replayRule.setTriggerSourceType(switch (origin.getTriggerSourceType()) {
|
||||||
|
case EVENT_RECORD -> GaoPointRule.TriggerSourceType.EVENT_RECORD;
|
||||||
|
case SALES_ORDER_PAID -> GaoPointRule.TriggerSourceType.SALES_ORDER_PAID;
|
||||||
|
});
|
||||||
|
GaoPointScriptContext context = buildContext(replayRule, origin.getStoreId(), origin.getCustomerId(),
|
||||||
|
origin.getOccurredAt() == null ? Time.now() : origin.getOccurredAt(),
|
||||||
|
origin.getTriggerSourceType() == GaoPointLedger.TriggerSourceType.EVENT_RECORD ? GaoPointRule.TriggerRefType.EVENT.name() : null,
|
||||||
|
record == null ? null : record.getEventId());
|
||||||
|
Map<String, Object> bindings = new HashMap<>();
|
||||||
|
bindings.put("context", context);
|
||||||
|
try {
|
||||||
|
GroovyScriptExecutionResult<GaoPointDecision> execution = groovyScriptService.executeVersion(origin.getGroovyScriptVersionId(), bindings, GaoPointDecision.class);
|
||||||
|
GaoPointDecision decision = execution.result();
|
||||||
|
return decision != null && decision.pointDelta() == origin.getPointDelta();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// 脚本版本已不可用或执行失败时按不满足处理,交给撤销流程兜底
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
long pointDelta = matchedTier.get("pointValue").longValue();
|
}
|
||||||
ObjectNode payload = jackson.createObjectNode();
|
|
||||||
payload.put("triggerType", GaoPointRule.TriggerType.CUSTOMER_EVENT_VALUE_TIER.name());
|
/// 幂等撤销一批原始积分流水
|
||||||
payload.put("sourceEventId", sourceEventId);
|
private void revokeAll(List<GaoPointLedger> originLedgerList, String reason) {
|
||||||
payload.put("sourceValueField", "numberValue");
|
if (originLedgerList == null || originLedgerList.isEmpty()) {
|
||||||
payload.put("windowBeginAt", windowBeginAt);
|
return;
|
||||||
payload.put("windowEndAt", windowEndAt);
|
}
|
||||||
payload.put("sumValue", sumValue);
|
List<String> revokeKeyList = originLedgerList.stream().map(origin -> "GAO_POINT_LEDGER:%s:REVOKE".formatted(origin.getId())).toList();
|
||||||
ObjectNode tierPayload = payload.putObject("tier");
|
Set<String> revokedKeySet = new HashSet<>();
|
||||||
tierPayload.put("min", matchedTier.get("min").longValue());
|
for (GaoPointLedger ledger : ledgerService.listByIdempotencyKeyList(revokeKeyList)) {
|
||||||
JsonNode maxNode = matchedTier.get("max");
|
revokedKeySet.add(ledger.getIdempotencyKey());
|
||||||
if (maxNode != null && !maxNode.isNull()) {
|
}
|
||||||
tierPayload.put("max", maxNode.longValue());
|
for (GaoPointLedger originLedger : originLedgerList) {
|
||||||
|
String revokeKey = "GAO_POINT_LEDGER:%s:REVOKE".formatted(originLedger.getId());
|
||||||
|
if (!revokedKeySet.contains(revokeKey)) {
|
||||||
|
ledgerService.revoke(originLedger, reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tierPayload.put("pointValue", pointDelta);
|
|
||||||
payload.put("pointDelta", pointDelta);
|
|
||||||
return new RuleResolution(pointDelta, payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long count(GaoEventRecord record) {
|
private GaoPointScriptContext eventContext(GaoPointRule rule, GaoEventRecord record) {
|
||||||
return eventRecordServiceProvider.getObject().count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null);
|
return buildContext(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(),
|
||||||
|
GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) {
|
private GaoPointScriptContext orderContext(GaoPointRule rule, GaoSalesOrder order) {
|
||||||
return "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId());
|
return buildContext(rule, order.getStoreId(), order.getCustomerId(), order.getPaidAt(), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record RuleResolution(Long pointDelta, JsonNode sourcePayload) {
|
private GaoPointScriptContext buildContext(GaoPointRule rule, String storeId, String customerId, long occurredAt, String triggerRefType, String triggerRefId) {
|
||||||
|
return new GaoPointScriptContext(consumptionService, eventRecordServiceProvider.getObject(), rule, storeId, customerId, triggerRefType, triggerRefId, occurredAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGroovy(GroovyScriptExecutionResult<GaoPointDecision> execution) {
|
||||||
|
return execution != null && execution.versionId() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGroovyLedger(GaoPointLedger origin) {
|
||||||
|
return TimiJava.isNotEmpty(origin.getGroovyScriptVersionId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-24
@@ -2,11 +2,12 @@ package com.imyeyu.api.modules.gao.service.implement;
|
|||||||
|
|
||||||
import com.imyeyu.api.bean.ModuleCode;
|
import com.imyeyu.api.bean.ModuleCode;
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
||||||
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
|
import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage;
|
||||||
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.entity.GaoFundRecord;
|
import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoProduct;
|
import com.imyeyu.api.modules.gao.entity.GaoProduct;
|
||||||
@@ -21,6 +22,7 @@ import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
|
|||||||
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
import com.imyeyu.api.modules.gao.service.GaoEventRecordService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
|
import com.imyeyu.api.modules.gao.service.GaoFundRecordService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
|
import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoProductService;
|
import com.imyeyu.api.modules.gao.service.GaoProductService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
|
import com.imyeyu.api.modules.gao.service.GaoProductUnitService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
|
import com.imyeyu.api.modules.gao.service.GaoSalesOrderService;
|
||||||
@@ -79,6 +81,8 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
private final GaoStoreService storeService;
|
private final GaoStoreService storeService;
|
||||||
private final GaoStoreScopeService storeScopeService;
|
private final GaoStoreScopeService storeScopeService;
|
||||||
private final GaoEventRecordService eventRecordService;
|
private final GaoEventRecordService eventRecordService;
|
||||||
|
private final GaoPointTriggerService pointTriggerService;
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
private final PermissionChecker permissionChecker;
|
private final PermissionChecker permissionChecker;
|
||||||
private final RoleChecker roleChecker;
|
private final RoleChecker roleChecker;
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
@@ -145,7 +149,6 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
GaoStore store = storeService.get(storeId);
|
GaoStore store = storeService.get(storeId);
|
||||||
TimiException.required(store, "not found store");
|
TimiException.required(store, "not found store");
|
||||||
storeScopeService.checkStore(store);
|
storeScopeService.checkStore(store);
|
||||||
GaoEvent consumeEvent = storeService.validateDirectProductSaleEvent(store);
|
|
||||||
List<GaoSalesOrderItem> itemList = resolveDirectItems(storeId, request.getItemList());
|
List<GaoSalesOrderItem> itemList = resolveDirectItems(storeId, request.getItemList());
|
||||||
long saleAmount = 0L;
|
long saleAmount = 0L;
|
||||||
for (GaoSalesOrderItem item : itemList) {
|
for (GaoSalesOrderItem item : itemList) {
|
||||||
@@ -155,24 +158,24 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
String operatorUserId = userLoginService.getRequireLoginUserId();
|
String operatorUserId = userLoginService.getRequireLoginUserId();
|
||||||
GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.DIRECT_PRODUCT, requestNo, purchasedAt, operatorUserId, request.getRemark());
|
GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.DIRECT_PRODUCT, requestNo, purchasedAt, operatorUserId, request.getRemark());
|
||||||
order.setSaleAmount(saleAmount);
|
order.setSaleAmount(saleAmount);
|
||||||
order.setPaymentStatus(saleAmount == 0 ? GaoSalesOrder.PaymentStatus.PAID : GaoSalesOrder.PaymentStatus.UNPAID);
|
// 零元订单创建时即为全额实收
|
||||||
|
boolean paidAtCreate = saleAmount == 0;
|
||||||
|
if (paidAtCreate) {
|
||||||
|
order.setPaymentStatus(GaoSalesOrder.PaymentStatus.PAID);
|
||||||
|
order.setPaidAt(Time.now());
|
||||||
|
} else {
|
||||||
|
order.setPaymentStatus(GaoSalesOrder.PaymentStatus.UNPAID);
|
||||||
|
}
|
||||||
super.create(order);
|
super.create(order);
|
||||||
for (GaoSalesOrderItem item : itemList) {
|
for (GaoSalesOrderItem item : itemList) {
|
||||||
item.setSalesOrderId(order.getId());
|
item.setSalesOrderId(order.getId());
|
||||||
itemMapper.insert(item);
|
itemMapper.insert(item);
|
||||||
}
|
}
|
||||||
GaoEventRecord record = new GaoEventRecord();
|
attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId(), request.getAttachmentList());
|
||||||
record.setCustomerId(customer.getId());
|
|
||||||
record.setStoreId(storeId);
|
|
||||||
record.setEventId(consumeEvent.getId());
|
|
||||||
record.setNumberValue(saleAmount);
|
|
||||||
record.setRegisteredAt(purchasedAt);
|
|
||||||
record.setRemark(request.getRemark());
|
|
||||||
record.setAttachmentList(request.getAttachmentList());
|
|
||||||
GaoEventRecord createdRecord = eventRecordService.createOrderConsumeRecord(record);
|
|
||||||
TimiException.requiredTrue(1 == mapper.updateSourceEventRecordId(order.getId(), createdRecord.getId()), "订单来源事件写入失败");
|
|
||||||
order.setSourceEventRecordId(createdRecord.getId());
|
|
||||||
order.setItemList(itemList);
|
order.setItemList(itemList);
|
||||||
|
if (paidAtCreate) {
|
||||||
|
pointTriggerService.onSalesOrderPaid(order);
|
||||||
|
}
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,22 +197,29 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
|
TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
|
||||||
&& data.getCustomerId().equals(existing.getCustomerId())
|
&& data.getCustomerId().equals(existing.getCustomerId())
|
||||||
&& data.getSaleAmount().equals(existing.getSaleAmount())
|
&& data.getSaleAmount().equals(existing.getSaleAmount()), "requestNo 已被其他订单使用");
|
||||||
&& data.getSourceEventRecordId().equals(existing.getSourceEventRecordId()), "requestNo 已被其他订单使用");
|
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
GaoCustomer customer = customerService.get(data.getCustomerId());
|
GaoCustomer customer = customerService.get(data.getCustomerId());
|
||||||
TimiException.required(customer, "not found customer");
|
TimiException.required(customer, "not found customer");
|
||||||
TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店");
|
TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店");
|
||||||
TimiException.required(data.getPackageConfigId(), "not found packageConfigId");
|
TimiException.required(data.getPackageConfigId(), "not found packageConfigId");
|
||||||
TimiException.required(data.getSourceEventRecordId(), "not found sourceEventRecordId");
|
|
||||||
TimiException.requiredTrue(0 <= data.getSaleAmount(), "成交价不能为负数");
|
TimiException.requiredTrue(0 <= data.getSaleAmount(), "成交价不能为负数");
|
||||||
GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.PACKAGE, requestNo,
|
GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.PACKAGE, requestNo,
|
||||||
TimiJava.defaultIfNull(data.getPurchasedAt(), Time.now()), data.getOperatorUserId(), data.getRemark());
|
TimiJava.defaultIfNull(data.getPurchasedAt(), Time.now()), data.getOperatorUserId(), data.getRemark());
|
||||||
order.setSaleAmount(data.getSaleAmount());
|
order.setSaleAmount(data.getSaleAmount());
|
||||||
order.setSourceEventRecordId(data.getSourceEventRecordId());
|
// 零元订单创建时即为全额实收
|
||||||
order.setPaymentStatus(data.getSaleAmount() == 0 ? GaoSalesOrder.PaymentStatus.PAID : GaoSalesOrder.PaymentStatus.UNPAID);
|
boolean paidAtCreate = data.getSaleAmount() == 0;
|
||||||
|
if (paidAtCreate) {
|
||||||
|
order.setPaymentStatus(GaoSalesOrder.PaymentStatus.PAID);
|
||||||
|
order.setPaidAt(Time.now());
|
||||||
|
} else {
|
||||||
|
order.setPaymentStatus(GaoSalesOrder.PaymentStatus.UNPAID);
|
||||||
|
}
|
||||||
super.create(order);
|
super.create(order);
|
||||||
|
if (paidAtCreate) {
|
||||||
|
pointTriggerService.onSalesOrderPaid(order);
|
||||||
|
}
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,14 +242,12 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.ACTIVE, "当前订单不可取消");
|
TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.ACTIVE, "当前订单不可取消");
|
||||||
TimiException.requiredTrue(fundRecordService.countPendingReceiveByBizId(order.getId()) == 0, "订单存在待核销收款,不能取消");
|
TimiException.requiredTrue(fundRecordService.countPendingReceiveByBizId(order.getId()) == 0, "订单存在待核销收款,不能取消");
|
||||||
TimiException.requiredTrue(fulfillmentServiceProvider.getObject().countPendingBySalesOrderId(order.getId()) == 0, "订单存在待履约操作,不能取消");
|
TimiException.requiredTrue(fulfillmentServiceProvider.getObject().countPendingBySalesOrderId(order.getId()) == 0, "订单存在待履约操作,不能取消");
|
||||||
GaoEventRecord sourceRecord = order.getSourceEventRecordId() == null ? null : eventRecordService.get(order.getSourceEventRecordId());
|
|
||||||
if (sourceRecord != null) {
|
|
||||||
eventRecordService.revokeOrderConsumeRecord(sourceRecord);
|
|
||||||
}
|
|
||||||
String userId = userLoginService.getRequireLoginUserId();
|
String userId = userLoginService.getRequireLoginUserId();
|
||||||
GaoSalesOrder.RefundStatus refundStatus = safe(order.getPaidAmount()) == 0 ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING;
|
GaoSalesOrder.RefundStatus refundStatus = safe(order.getPaidAmount()) == 0 ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING;
|
||||||
TimiException.requiredTrue(1 == mapper.cancelIf(order.getId(), GaoSalesOrder.Status.ACTIVE, GaoSalesOrder.Status.CANCELLED,
|
TimiException.requiredTrue(1 == mapper.cancelIf(order.getId(), GaoSalesOrder.Status.ACTIVE, GaoSalesOrder.Status.CANCELLED,
|
||||||
Time.now(), userId, reason, refundStatus), "订单状态已变化");
|
Time.now(), userId, reason, refundStatus), "订单状态已变化");
|
||||||
|
// 订单取消立即失去有效消费资格,不等待退款完成
|
||||||
|
pointTriggerService.revokeBySalesOrder(order, reason);
|
||||||
order.setStatus(GaoSalesOrder.Status.CANCELLED);
|
order.setStatus(GaoSalesOrder.Status.CANCELLED);
|
||||||
order.setCancelledAt(Time.now());
|
order.setCancelledAt(Time.now());
|
||||||
order.setCancelOperatorUserId(userId);
|
order.setCancelOperatorUserId(userId);
|
||||||
@@ -263,7 +271,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
public void updatePayment(GaoSalesOrder order) {
|
public void updatePayment(GaoSalesOrder order) {
|
||||||
TimiException.required(order, "not found sales order");
|
TimiException.required(order, "not found sales order");
|
||||||
TimiException.requiredTrue(1 == mapper.updatePayment(order.getId(), safeLong(order.getPendingPaidAmount()), safeLong(order.getPaidAmount()),
|
TimiException.requiredTrue(1 == mapper.updatePayment(order.getId(), safeLong(order.getPendingPaidAmount()), safeLong(order.getPaidAmount()),
|
||||||
safeLong(order.getRefundedAmount()), order.getPaymentStatus(), order.getRefundStatus()), "订单付款汇总更新失败");
|
order.getPaidAt(), safeLong(order.getRefundedAmount()), order.getPaymentStatus(), order.getRefundStatus()), "订单付款汇总更新失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -275,6 +283,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
TimiException.requiredTrue(0 <= pendingAmount && pendingAmount + paidAmount <= saleAmount, "待核销收款超出订单金额");
|
TimiException.requiredTrue(0 <= pendingAmount && pendingAmount + paidAmount <= saleAmount, "待核销收款超出订单金额");
|
||||||
order.setPaymentStatus(saleAmount == 0 || paidAmount == saleAmount
|
order.setPaymentStatus(saleAmount == 0 || paidAmount == saleAmount
|
||||||
? GaoSalesOrder.PaymentStatus.PAID : (0 < paidAmount ? GaoSalesOrder.PaymentStatus.PARTIALLY_PAID : GaoSalesOrder.PaymentStatus.UNPAID));
|
? GaoSalesOrder.PaymentStatus.PAID : (0 < paidAmount ? GaoSalesOrder.PaymentStatus.PARTIALLY_PAID : GaoSalesOrder.PaymentStatus.UNPAID));
|
||||||
|
if (order.getPaymentStatus() == GaoSalesOrder.PaymentStatus.PAID && order.getPaidAt() == null) {
|
||||||
|
order.setPaidAt(Time.now());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
-52
@@ -80,7 +80,6 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
|
|||||||
try {
|
try {
|
||||||
logger.setContent(jackson.writeValueAsString(store));
|
logger.setContent(jackson.writeValueAsString(store));
|
||||||
checkUnique(store);
|
checkUnique(store);
|
||||||
validateDirectProductSaleEventIfPresent(store);
|
|
||||||
store.setStatus(TimiJava.defaultIfNull(store.getStatus(), GaoStore.Status.ACTIVE));
|
store.setStatus(TimiJava.defaultIfNull(store.getStatus(), GaoStore.Status.ACTIVE));
|
||||||
store.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
|
store.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false));
|
||||||
store.setSort(TimiJava.defaultIfNull(store.getSort(), 0));
|
store.setSort(TimiJava.defaultIfNull(store.getSort(), 0));
|
||||||
@@ -113,7 +112,6 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
|
|||||||
try {
|
try {
|
||||||
logger.setContent(jackson.writeValueAsString(store));
|
logger.setContent(jackson.writeValueAsString(store));
|
||||||
checkUnique(store);
|
checkUnique(store);
|
||||||
validateDirectProductSaleEventIfPresent(store);
|
|
||||||
super.update(store);
|
super.update(store);
|
||||||
logger.setResult(store.getId());
|
logger.setResult(store.getId());
|
||||||
} catch (TimiException e) {
|
} catch (TimiException e) {
|
||||||
@@ -227,56 +225,6 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
|
|||||||
return mapper.selectByUserId(userId);
|
return mapper.selectByUserId(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public GaoEvent validateDirectProductSaleEvent(GaoStore store) {
|
|
||||||
TimiException.required(store, "not found store");
|
|
||||||
TimiException.required(store.getDirectProductSaleEventId(), "门店未配置直接产品消费事件");
|
|
||||||
TimiException.required(store.getId(), "门店尚未生成 ID,不能绑定直接产品消费事件");
|
|
||||||
GaoEvent event = eventService.get(store.getDirectProductSaleEventId());
|
|
||||||
TimiException.required(event, "直接产品消费事件不存在");
|
|
||||||
TimiException.requiredTrue(store.getId().equals(event.getStoreId()), "直接产品消费事件不属于当前门店");
|
|
||||||
TimiException.requiredTrue(event.getStatus() == GaoEvent.Status.ACTIVE, "直接产品消费事件未启用");
|
|
||||||
TimiException.requiredTrue(event.getBizType() == GaoEvent.BizType.GENERAL, "直接产品消费事件必须是 GENERAL 类型");
|
|
||||||
TimiException.requiredTrue(event.getValueType() == GaoEvent.ValueType.NUMBER && event.getValueUnit() == GaoEvent.ValueUnit.CENT, "直接产品消费事件必须使用 NUMBER/CENT 数值");
|
|
||||||
TimiException.requiredTrue(!Boolean.TRUE.equals(event.getRequireVerification()), "直接产品消费事件不能要求核销");
|
|
||||||
eventService.attachOptionGroupList(List.of(event));
|
|
||||||
for (var group : TimiJava.safeIterable(event.getOptionGroupList())) {
|
|
||||||
TimiException.requiredTrue(!Boolean.TRUE.equals(group.getRequired()), "直接产品消费事件不能配置必填选项");
|
|
||||||
}
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<GaoEvent> listDirectProductSaleEvents(String storeId) {
|
|
||||||
TimiException.required(storeId, "not found storeId");
|
|
||||||
return eventService.listValid(storeId).stream()
|
|
||||||
.filter(this::isValidDirectProductSaleEvent)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidDirectProductSaleEvent(GaoEvent event) {
|
|
||||||
if (event == null
|
|
||||||
|| event.getStatus() != GaoEvent.Status.ACTIVE
|
|
||||||
|| event.getBizType() != GaoEvent.BizType.GENERAL
|
|
||||||
|| event.getValueType() != GaoEvent.ValueType.NUMBER
|
|
||||||
|| event.getValueUnit() != GaoEvent.ValueUnit.CENT
|
|
||||||
|| Boolean.TRUE.equals(event.getRequireVerification())) { // 消费事件为 true,订单要求为 false,检查是否设计冲突
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (var group : TimiJava.safeIterable(event.getOptionGroupList())) {
|
|
||||||
if (Boolean.TRUE.equals(group.getRequired())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateDirectProductSaleEventIfPresent(GaoStore store) {
|
|
||||||
if (store.getDirectProductSaleEventId() != null && !store.getDirectProductSaleEventId().isBlank()) {
|
|
||||||
validateDirectProductSaleEvent(store);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBelongIdByRequiredLoginUserId() {
|
public String getBelongIdByRequiredLoginUserId() {
|
||||||
return getByUserId(userLoginService.getRequireLoginUserId()).getId();
|
return getByUserId(userLoginService.getRequireLoginUserId()).getId();
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ public class GaoCustomerPackagePurchaseRequest {
|
|||||||
/// 客户端幂等请求号
|
/// 客户端幂等请求号
|
||||||
private String requestNo;
|
private String requestNo;
|
||||||
|
|
||||||
/// 购买事件 ID
|
/// 套餐配置 ID
|
||||||
private String eventId;
|
private String packageConfigId;
|
||||||
|
|
||||||
/// 客户 ID
|
/// 客户 ID
|
||||||
private String customerId;
|
private String customerId;
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ public class GaoPackageOrderCreateData {
|
|||||||
/// 成交价
|
/// 成交价
|
||||||
private Long saleAmount;
|
private Long saleAmount;
|
||||||
|
|
||||||
/// 来源购买事件记录 ID
|
|
||||||
private String sourceEventRecordId;
|
|
||||||
|
|
||||||
/// 幂等号
|
/// 幂等号
|
||||||
private String requestNo;
|
private String requestNo;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
CREATE TABLE `groovy_script` (
|
||||||
|
`id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`biz_type` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务归属',
|
||||||
|
`code` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务内唯一代码',
|
||||||
|
`name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
|
||||||
|
`description` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '说明',
|
||||||
|
`enabled` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为允许执行已发布版本',
|
||||||
|
`published_version_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '当前发布版本 ID',
|
||||||
|
`created_at` BIGINT(20),
|
||||||
|
`updated_at` BIGINT(20),
|
||||||
|
`deleted_at` BIGINT(20),
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `uk_groovy_script_biz_code` (`biz_type`, `code`),
|
||||||
|
KEY `idx_groovy_script_published_version` (`published_version_id`),
|
||||||
|
KEY `idx_groovy_script_deleted_created` (`deleted_at`, `created_at`)
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Groovy 脚本' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
CREATE TABLE `groovy_script_version` (
|
||||||
|
`id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`script_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '脚本 ID',
|
||||||
|
`version` INT NOT NULL COMMENT '版本号',
|
||||||
|
`content` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '脚本内容',
|
||||||
|
`checksum` CHAR(64) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL COMMENT '脚本内容 SHA-256',
|
||||||
|
`created_at` BIGINT(20),
|
||||||
|
`updated_at` BIGINT(20),
|
||||||
|
`deleted_at` BIGINT(20),
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `uk_groovy_script_version` (`script_id`, `version`),
|
||||||
|
KEY `idx_groovy_script_version_script_created` (`script_id`, `created_at`)
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Groovy 脚本版本' ROW_FORMAT = Dynamic;
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
CREATE TABLE `gao_customer_consumption_record` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
|
||||||
|
`customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID',
|
||||||
|
`source_type` VARCHAR(32) NOT NULL COMMENT '来源类型',
|
||||||
|
`source_id` VARCHAR(36) NOT NULL COMMENT '来源业务对象 ID',
|
||||||
|
`sales_order_id` VARCHAR(36) NULL COMMENT '销售订单 ID',
|
||||||
|
`order_type` VARCHAR(32) NULL COMMENT '订单类型',
|
||||||
|
`amount` BIGINT NOT NULL COMMENT '有效消费金额,单位分',
|
||||||
|
`effective_at` BIGINT NOT NULL COMMENT '成为有效消费时间',
|
||||||
|
`status` VARCHAR(32) NOT NULL COMMENT '状态',
|
||||||
|
`revoked_at` BIGINT NULL COMMENT '失效时间',
|
||||||
|
`revoke_reason` TEXT NULL COMMENT '失效原因',
|
||||||
|
`source_payload` JSON NULL COMMENT '来源快照',
|
||||||
|
`created_at` BIGINT NOT NULL,
|
||||||
|
`updated_at` BIGINT NULL,
|
||||||
|
`deleted_at` BIGINT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_gao_customer_consumption_source` (`source_type`, `source_id`),
|
||||||
|
KEY `idx_gao_customer_consumption_customer_time` (`store_id`, `customer_id`, `status`, `effective_at`),
|
||||||
|
KEY `idx_gao_customer_consumption_order` (`sales_order_id`)
|
||||||
|
) COMMENT='GAO 客户有效消费事实';
|
||||||
|
|
||||||
|
CREATE TABLE `gao_point_ledger_consumption_record` (
|
||||||
|
`point_ledger_id` VARCHAR(36) NOT NULL COMMENT '积分流水 ID',
|
||||||
|
`consumption_record_id` VARCHAR(36) NOT NULL COMMENT '消费事实 ID',
|
||||||
|
`created_at` BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (`point_ledger_id`, `consumption_record_id`),
|
||||||
|
KEY `idx_gao_point_ledger_consumption_record_consumption` (`consumption_record_id`)
|
||||||
|
) COMMENT='GAO 积分流水消费事实依赖';
|
||||||
|
|
||||||
|
ALTER TABLE `gao_sales_order`
|
||||||
|
ADD COLUMN `paid_at` BIGINT NULL COMMENT '首次全额实收时间' AFTER `paid_amount`;
|
||||||
|
|
||||||
|
ALTER TABLE `gao_point_rule`
|
||||||
|
ADD COLUMN `rule_mode` VARCHAR(16) NULL COMMENT '规则模式' AFTER `status`,
|
||||||
|
ADD COLUMN `trigger_source_type` VARCHAR(32) NULL COMMENT '触发来源类型' AFTER `rule_mode`,
|
||||||
|
ADD COLUMN `trigger_ref_type` VARCHAR(32) NULL COMMENT '触发对象类型' AFTER `trigger_source_type`,
|
||||||
|
ADD COLUMN `trigger_ref_id` VARCHAR(36) NULL COMMENT '触发对象 ID' AFTER `trigger_ref_type`,
|
||||||
|
ADD COLUMN `groovy_script_id` VARCHAR(36) NULL COMMENT 'Groovy 脚本 ID' AFTER `point_value`,
|
||||||
|
ADD KEY `idx_gao_point_rule_trigger` (`store_id`, `trigger_source_type`, `trigger_ref_id`, `status`, `deleted_at`, `priority`);
|
||||||
|
|
||||||
|
ALTER TABLE `gao_point_ledger`
|
||||||
|
ADD COLUMN `trigger_source_type` VARCHAR(32) NULL COMMENT '直接触发来源类型' AFTER `rule_id`,
|
||||||
|
ADD COLUMN `trigger_source_id` VARCHAR(36) NULL COMMENT '直接触发来源 ID' AFTER `trigger_source_type`,
|
||||||
|
ADD COLUMN `groovy_script_version_id` VARCHAR(36) NULL COMMENT '实际执行 Groovy 脚本版本 ID' AFTER `trigger_source_id`,
|
||||||
|
ADD COLUMN `rule_snapshot` JSON NULL COMMENT '规则快照' AFTER `groovy_script_version_id`,
|
||||||
|
ADD KEY `idx_gao_point_ledger_trigger_source` (`trigger_source_type`, `trigger_source_id`, `change_type`);
|
||||||
|
|
||||||
|
UPDATE `gao_point_rule`
|
||||||
|
SET `rule_mode` = 'SIMPLE',
|
||||||
|
`trigger_source_type` = 'EVENT_RECORD',
|
||||||
|
`trigger_ref_type` = 'GAO_EVENT',
|
||||||
|
`trigger_ref_id` = `event_id`
|
||||||
|
WHERE `trigger_type` = 'EVERY_TIME'
|
||||||
|
AND `point_mode` = 'FIXED'
|
||||||
|
AND `condition_json` IS NULL;
|
||||||
|
|
||||||
|
UPDATE `gao_point_ledger`
|
||||||
|
SET `trigger_source_type` = 'EVENT_RECORD',
|
||||||
|
`trigger_source_id` = `event_record_id`
|
||||||
|
WHERE `event_record_id` IS NOT NULL;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
INSERT INTO `groovy_script` (`id`, `biz_type`, `code`, `name`, `description`, `enabled`, `published_version_id`, `created_at`)
|
||||||
|
VALUES
|
||||||
|
('f2511e0e-6e55-4a59-a221-700000000001', 'GAO_POINT_RULE', 'SIGN_IN_CONSUMPTION_TIER', '签到按消费分档积分', '签到时按过去一年有效消费分档积分', TRUE, 'f2511e0e-6e55-4a59-a221-700000000101', 1787248052654),
|
||||||
|
('f2511e0e-6e55-4a59-a221-700000000002', 'GAO_POINT_RULE', 'SIGN_IN_AFTER_THIRTY', '无消费累计签到积分', '第 30 次签到及以后每次积分', TRUE, 'f2511e0e-6e55-4a59-a221-700000000102', 1787041413269);
|
||||||
|
|
||||||
|
INSERT INTO `groovy_script_version` (`id`, `script_id`, `version`, `content`, `checksum`, `created_at`)
|
||||||
|
VALUES
|
||||||
|
('f2511e0e-6e55-4a59-a221-700000000101', 'f2511e0e-6e55-4a59-a221-700000000001', 1,
|
||||||
|
'long amount = context.rollingYearConsumptionAmount()\nif (amount < 1L) return null\nlong point = amount < 200000L ? 20L : amount < 2000000L ? 50L : amount < 5000000L ? 80L : 100L\nreturn context.decision(point, "签到消费分档积分", [amount: amount, window: "ROLLING_1_YEAR"])',
|
||||||
|
SHA2('long amount = context.rollingYearConsumptionAmount()\nif (amount < 1L) return null\nlong point = amount < 200000L ? 20L : amount < 2000000L ? 50L : amount < 5000000L ? 80L : 100L\nreturn context.decision(point, "签到消费分档积分", [amount: amount, window: "ROLLING_1_YEAR"])', 256), 1787248052654),
|
||||||
|
('f2511e0e-6e55-4a59-a221-700000000102', 'f2511e0e-6e55-4a59-a221-700000000002', 1,
|
||||||
|
'if (context.eventCount() < 30L) return null\nreturn context.decision(10L, "第 30 次及以后签到积分", [eventCount: context.eventCount()])',
|
||||||
|
SHA2('if (context.eventCount() < 30L) return null\nreturn context.decision(10L, "第 30 次及以后签到积分", [eventCount: context.eventCount()])', 256), 1787041413269);
|
||||||
|
|
||||||
|
UPDATE `gao_point_rule`
|
||||||
|
SET `rule_mode` = 'GROOVY', `trigger_source_type` = 'EVENT_RECORD', `trigger_ref_type` = 'EVENT',
|
||||||
|
`trigger_ref_id` = `event_id`, `groovy_script_id` = 'f2511e0e-6e55-4a59-a221-700000000001', `point_value` = NULL
|
||||||
|
WHERE `id` = '1746879e-3726-4927-95a6-a3b738cbc134';
|
||||||
|
|
||||||
|
UPDATE `gao_point_rule`
|
||||||
|
SET `rule_mode` = 'GROOVY', `trigger_source_type` = 'EVENT_RECORD', `trigger_ref_type` = 'EVENT',
|
||||||
|
`trigger_ref_id` = `event_id`, `groovy_script_id` = 'f2511e0e-6e55-4a59-a221-700000000002', `point_value` = NULL
|
||||||
|
WHERE `id` = '1b71dbb8-be2e-4783-9209-e6a94a8745b4';
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
ALTER TABLE `gao_point_ledger`
|
||||||
|
ADD COLUMN `sales_order_id` VARCHAR(36) NULL COMMENT '关联销售订单 ID' AFTER `trigger_source_id`,
|
||||||
|
ADD KEY `idx_gao_point_ledger_sales_order` (`sales_order_id`);
|
||||||
|
|
||||||
|
-- 将旧规则 sourceEventId 指向的登记事件标记为退役消费事件
|
||||||
|
UPDATE `gao_event` e
|
||||||
|
SET e.`biz_type` = 'LEGACY_CONSUMPTION'
|
||||||
|
WHERE e.`deleted_at` IS NULL
|
||||||
|
AND e.`biz_type` <> 'PACKAGE_PURCHASE'
|
||||||
|
AND e.`id` IN (
|
||||||
|
SELECT DISTINCT JSON_UNQUOTE(JSON_EXTRACT(r.`condition_json`, '$.sourceEventId'))
|
||||||
|
FROM `gao_point_rule` r
|
||||||
|
WHERE r.`condition_json` IS NOT NULL
|
||||||
|
AND JSON_EXTRACT(r.`condition_json`, '$.sourceEventId') IS NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 历史消费事件迁移为 LEGACY_EVENT 消费事实,来源唯一约束保证可重跑
|
||||||
|
-- 不能使用 INSERT IGNORE,否则数据异常会被静默吞掉,导致消费累计缺失
|
||||||
|
INSERT INTO `gao_customer_consumption_record`
|
||||||
|
(`id`, `store_id`, `customer_id`, `source_type`, `source_id`, `sales_order_id`, `order_type`, `amount`, `effective_at`, `status`, `revoked_at`, `revoke_reason`, `source_payload`, `created_at`, `updated_at`, `deleted_at`)
|
||||||
|
SELECT
|
||||||
|
UUID(),
|
||||||
|
r.`store_id`,
|
||||||
|
r.`customer_id`,
|
||||||
|
'LEGACY_EVENT',
|
||||||
|
r.`id`,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
r.`number_value`,
|
||||||
|
r.`registered_at`,
|
||||||
|
'ACTIVE',
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
JSON_OBJECT('migrationBatch', 'V66', 'eventId', r.`event_id`, 'eventName', e.`name`, 'eventRecordId', r.`id`, 'registeredAt', r.`registered_at`),
|
||||||
|
FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000),
|
||||||
|
NULL,
|
||||||
|
NULL
|
||||||
|
FROM `gao_event_record` r
|
||||||
|
INNER JOIN `gao_event` e ON e.`id` = r.`event_id` AND e.`deleted_at` IS NULL
|
||||||
|
WHERE e.`biz_type` = 'LEGACY_CONSUMPTION'
|
||||||
|
AND r.`number_value` IS NOT NULL
|
||||||
|
AND r.`number_value` > 0
|
||||||
|
AND r.`deleted_at` IS NULL
|
||||||
|
ON DUPLICATE KEY UPDATE `id` = `id`;
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
-- V64 曾把枚举值误写为 GAO_EVENT,MyBatis 读取时会无法映射到 TriggerRefType.EVENT
|
||||||
|
UPDATE `gao_point_rule`
|
||||||
|
SET `trigger_ref_type` = 'EVENT'
|
||||||
|
WHERE `trigger_ref_type` = 'GAO_EVENT';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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.GaoCustomerConsumptionRecordMapper">
|
||||||
|
<select id="selectBySource" resultType="com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord">
|
||||||
|
SELECT * FROM `gao_customer_consumption_record`
|
||||||
|
WHERE `source_type` = #{sourceType} AND `source_id` = #{sourceId} AND `deleted_at` IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
<select id="selectActiveByWindow" resultType="com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord">
|
||||||
|
SELECT * FROM `gao_customer_consumption_record`
|
||||||
|
WHERE `store_id` = #{storeId} AND `customer_id` = #{customerId} AND `status` = 'ACTIVE'
|
||||||
|
AND `effective_at` >= #{beginAt} AND `effective_at` <= #{endAt} AND `deleted_at` IS NULL
|
||||||
|
ORDER BY `effective_at` ASC, `id` ASC
|
||||||
|
</select>
|
||||||
|
<update id="revokeBySource">
|
||||||
|
UPDATE `gao_customer_consumption_record`
|
||||||
|
SET `status` = 'REVOKED', `revoked_at` = #{revokedAt}, `revoke_reason` = #{revokeReason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
||||||
|
WHERE `source_type` = #{sourceType} AND `source_id` = #{sourceId} AND `status` = 'ACTIVE' AND `deleted_at` IS NULL
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
WHERE
|
WHERE
|
||||||
`store_id` = #{storeId}
|
`store_id` = #{storeId}
|
||||||
AND `status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}'
|
AND `status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}'
|
||||||
|
AND (`biz_type` IS NULL OR `biz_type` != 'LEGACY_CONSUMPTION')
|
||||||
AND (`begin_at` IS NULL OR `begin_at` <= #{now})
|
AND (`begin_at` IS NULL OR `begin_at` <= #{now})
|
||||||
AND (`end_at` IS NULL OR #{now} <= `end_at`)
|
AND (`end_at` IS NULL OR #{now} <= `end_at`)
|
||||||
AND `deleted_at` IS NULL
|
AND `deleted_at` IS NULL
|
||||||
|
|||||||
@@ -119,6 +119,12 @@
|
|||||||
#{eventId}
|
#{eventId}
|
||||||
</foreach>
|
</foreach>
|
||||||
</if>
|
</if>
|
||||||
|
<if test="eventIdList == null or eventIdList.size() == 0">
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `gao_event` re
|
||||||
|
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
|
||||||
|
)
|
||||||
|
</if>
|
||||||
<if test="optionIdList != null and optionIdList.size() > 0">
|
<if test="optionIdList != null and optionIdList.size() > 0">
|
||||||
<foreach collection="optionIdList" item="optionId">
|
<foreach collection="optionIdList" item="optionId">
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
@@ -212,6 +218,12 @@
|
|||||||
#{eventId}
|
#{eventId}
|
||||||
</foreach>
|
</foreach>
|
||||||
</if>
|
</if>
|
||||||
|
<if test="eventIdList == null or eventIdList.size() == 0">
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `gao_event` re
|
||||||
|
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
|
||||||
|
)
|
||||||
|
</if>
|
||||||
<if test="rankType != null and rankType.name() == 'TOTAL_NUMBER_VALUE'">
|
<if test="rankType != null and rankType.name() == 'TOTAL_NUMBER_VALUE'">
|
||||||
AND r.`number_value` IS NOT NULL
|
AND r.`number_value` IS NOT NULL
|
||||||
</if>
|
</if>
|
||||||
@@ -386,6 +398,12 @@
|
|||||||
#{eventId}
|
#{eventId}
|
||||||
</foreach>
|
</foreach>
|
||||||
</if>
|
</if>
|
||||||
|
<if test="eventIdList == null or eventIdList.size() == 0">
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `gao_event` re
|
||||||
|
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
|
||||||
|
)
|
||||||
|
</if>
|
||||||
</sql>
|
</sql>
|
||||||
<select id="stateDailyByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView">
|
<select id="stateDailyByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView">
|
||||||
SELECT
|
SELECT
|
||||||
@@ -447,6 +465,12 @@
|
|||||||
#{eventId}
|
#{eventId}
|
||||||
</foreach>
|
</foreach>
|
||||||
</if>
|
</if>
|
||||||
|
<if test="eventIdList == null or eventIdList.size() == 0">
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `gao_event` re
|
||||||
|
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
|
||||||
|
)
|
||||||
|
</if>
|
||||||
</sql>
|
</sql>
|
||||||
<select id="stateCustomerDailyByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView">
|
<select id="stateCustomerDailyByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView">
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
@@ -9,15 +9,26 @@
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectRuleChangeByEventRecordId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
<select id="selectRuleChangeByTriggerSource" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM `gao_point_ledger`
|
FROM `gao_point_ledger`
|
||||||
WHERE `event_record_id` = #{eventRecordId}
|
WHERE `trigger_source_type` = #{triggerSourceType}
|
||||||
|
AND `trigger_source_id` = #{triggerSourceId}
|
||||||
AND `change_type` IN ('EARN', 'CONSUME')
|
AND `change_type` IN ('EARN', 'CONSUME')
|
||||||
AND `deleted_at` IS NULL
|
AND `deleted_at` IS NULL
|
||||||
ORDER BY `created_at` ASC, `id` ASC
|
ORDER BY `created_at` ASC, `id` ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectRuleChangeByConsumptionRecordId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
|
SELECT l.*
|
||||||
|
FROM `gao_point_ledger` l
|
||||||
|
INNER JOIN `gao_point_ledger_consumption_record` d ON d.`point_ledger_id` = l.`id`
|
||||||
|
WHERE d.`consumption_record_id` = #{consumptionRecordId}
|
||||||
|
AND l.`change_type` IN ('EARN', 'CONSUME')
|
||||||
|
AND l.`deleted_at` IS NULL
|
||||||
|
ORDER BY l.`created_at` ASC, l.`id` ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectByIdempotencyKeyList" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
<select id="selectByIdempotencyKeyList" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM `gao_point_ledger`
|
FROM `gao_point_ledger`
|
||||||
|
|||||||
@@ -11,11 +11,16 @@
|
|||||||
</foreach>
|
</foreach>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectActiveByStoreIdAndEventId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointRule">
|
<select id="selectActiveByTrigger" resultType="com.imyeyu.api.modules.gao.entity.GaoPointRule">
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM `gao_point_rule`
|
FROM `gao_point_rule`
|
||||||
WHERE `store_id` = #{storeId}
|
WHERE `store_id` = #{storeId}
|
||||||
AND `event_id` = #{eventId}
|
AND `trigger_source_type` = #{triggerSourceType}
|
||||||
|
AND (`trigger_ref_id` IS NULL
|
||||||
|
<if test="triggerRefId != null and triggerRefId != ''">
|
||||||
|
OR `trigger_ref_id` = #{triggerRefId}
|
||||||
|
</if>
|
||||||
|
)
|
||||||
AND `status` = 'ACTIVE'
|
AND `status` = 'ACTIVE'
|
||||||
AND (`begin_at` IS NULL OR `begin_at` <= #{now})
|
AND (`begin_at` IS NULL OR `begin_at` <= #{now})
|
||||||
AND (`end_at` IS NULL OR #{now} <= `end_at`)
|
AND (`end_at` IS NULL OR #{now} <= `end_at`)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<update id="updatePayment">
|
<update id="updatePayment">
|
||||||
UPDATE `gao_sales_order`
|
UPDATE `gao_sales_order`
|
||||||
SET `pending_paid_amount` = #{pendingPaidAmount}, `paid_amount` = #{paidAmount}, `refunded_amount` = #{refundedAmount},
|
SET `pending_paid_amount` = #{pendingPaidAmount}, `paid_amount` = #{paidAmount}, `paid_at` = #{paidAt}, `refunded_amount` = #{refundedAmount},
|
||||||
`payment_status` = #{paymentStatus}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
`payment_status` = #{paymentStatus}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
||||||
WHERE `id` = #{id} AND `deleted_at` IS NULL
|
WHERE `id` = #{id} AND `deleted_at` IS NULL
|
||||||
</update>
|
</update>
|
||||||
@@ -43,11 +43,6 @@
|
|||||||
`cancel_reason` = #{cancelReason}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
`cancel_reason` = #{cancelReason}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
||||||
WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL
|
WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL
|
||||||
</update>
|
</update>
|
||||||
<update id="updateSourceEventRecordId">
|
|
||||||
UPDATE `gao_sales_order`
|
|
||||||
SET `source_event_record_id` = #{sourceEventRecordId}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
|
||||||
WHERE `id` = #{id} AND `source_event_record_id` IS NULL AND `deleted_at` IS NULL
|
|
||||||
</update>
|
|
||||||
<update id="updateStatus">
|
<update id="updateStatus">
|
||||||
UPDATE `gao_sales_order`
|
UPDATE `gao_sales_order`
|
||||||
SET `status` = #{status}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
SET `status` = #{status}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
||||||
|
|||||||
Reference in New Issue
Block a user