diff --git a/pom.xml b/pom.xml index d7cfc82..d4e9dd9 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.imyeyu.timiserverapi TimiServerAPI - 1.0.30 + 1.0.31 jar TimiServerAPI imyeyu.com API @@ -180,6 +180,11 @@ + + org.apache.groovy + groovy + 4.0.29 + com.imyeyu.spring timi-spring diff --git a/src/main/java/com/imyeyu/api/modules/common/bean/GroovyScriptExecutionResult.java b/src/main/java/com/imyeyu/api/modules/common/bean/GroovyScriptExecutionResult.java new file mode 100644 index 0000000..22de318 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/bean/GroovyScriptExecutionResult.java @@ -0,0 +1,9 @@ +package com.imyeyu.api.modules.common.bean; + +/// Groovy 脚本执行结果 +/// +/// @param 脚本返回值类型 +/// @author Codex +/// @since 2026-09-04 +public record GroovyScriptExecutionResult(String scriptId, String versionId, Integer version, String checksum, T result) { +} diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java index 79489ed..deabf72 100644 --- a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java +++ b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java @@ -38,12 +38,20 @@ public class Attachment extends UUIDEntity { GAO_CUSTOMER, + GAO_PRODUCT, + + GAO_PRODUCT_SKU, + GAO_EVENT_RECORD, - GAO_PACKAGE_DELIVERY, - GAO_FUND_RECORD, + GAO_STOCK_RECORD, + + GAO_ORDER_FULFILLMENT, + + GAO_SALES_ORDER, + TEMP_FILE, MIRROR diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java new file mode 100644 index 0000000..a6c3d02 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java @@ -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; +} diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScriptVersion.java b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScriptVersion.java new file mode 100644 index 0000000..f8c57f1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScriptVersion.java @@ -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; +} diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptMapper.java new file mode 100644 index 0000000..b7e16f6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptMapper.java @@ -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 { +} diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java new file mode 100644 index 0000000..f563e21 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java @@ -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 { +} diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java index 10ff654..5636f9f 100644 --- a/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java @@ -3,6 +3,7 @@ package com.imyeyu.api.modules.common.mapper; import com.imyeyu.api.modules.common.entity.Notify; import com.imyeyu.api.modules.common.vo.NotifyManagementItem; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import com.imyeyu.spring.mapper.BaseMapper; import java.util.List; @@ -15,6 +16,10 @@ import java.util.List; */ public interface NotifyMapper extends BaseMapper { + /// 查询业务提醒是否已创建 + @Select("SELECT EXISTS (SELECT 1 FROM `notify` WHERE `biz_type` = #{bizType} AND `biz_reminded_type` = #{bizRemindedType} AND `biz_reminded_id` = #{bizRemindedId} AND `deleted_at` IS NULL)") + boolean existsByBizReminded(@Param("bizType") String bizType, @Param("bizRemindedType") String bizRemindedType, @Param("bizRemindedId") String bizRemindedId); + /// 分页查询管理端通知 /// /// @param userId 管理员用户 ID diff --git a/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java b/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java index 8590a20..270deb6 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java @@ -3,6 +3,8 @@ package com.imyeyu.api.modules.common.service; import com.imyeyu.api.modules.common.entity.Article; import com.imyeyu.spring.service.BaseService; +import java.util.List; + /** * 文章服务 * @@ -10,4 +12,10 @@ import com.imyeyu.spring.service.BaseService; * @since 2021-02-23 21:33 */ public interface ArticleService extends BaseService { + + /// 按示例查询文章 + /// + /// @param example 查询示例 + /// @return 文章列表 + List
listByExample(Article example); } diff --git a/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java b/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java new file mode 100644 index 0000000..eee5ef4 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java @@ -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 { + + /// 按业务归属和代码查询脚本 + /// + /// @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 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 bindings); + + /// 执行已发布且启用的脚本并返回实际执行版本 + /// + /// @param bizType 业务归属 + /// @param code 业务内唯一代码 + /// @param bindings 传给脚本的变量 + /// @param resultType 返回值类型 + /// @param 返回值类型 + /// @return 脚本执行结果 + GroovyScriptExecutionResult executePublished(GroovyScript.BizType bizType, String code, Map bindings, Class resultType); + + /// 按指定历史版本执行脚本 + /// + /// 用于按积分发生时的脚本版本复算,脚本当前是否发布或启用不影响执行;脚本返回 null 表示未命中,属合法结果 + /// + /// @param versionId 脚本版本 ID + /// @param bindings 传给脚本的变量 + /// @param resultType 返回值类型 + /// @param 返回值类型 + /// @return 脚本执行结果 + GroovyScriptExecutionResult executeVersion(String versionId, Map bindings, Class resultType); + + /// 执行已发布且启用的脚本并验证返回值类型 + /// + /// @param bizType 业务归属 + /// @param code 业务内唯一代码 + /// @param bindings 传给脚本的变量 + /// @param resultType 返回值类型 + /// @param 返回值类型 + /// @return 脚本返回值 + T execute(GroovyScript.BizType bizType, String code, Map bindings, Class resultType); + + /// 清除脚本的本地编译缓存 + /// + /// 多实例部署时,其他节点在收到脚本变更通知后调用此方法 + /// + /// @param scriptId 脚本 ID + void invalidate(String scriptId); +} diff --git a/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java b/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java index 4b41082..4b4bd09 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java @@ -14,6 +14,14 @@ import com.imyeyu.spring.service.BaseService; */ public interface NotifyService extends BaseService { + /// 查询业务提醒是否已创建 + /// + /// @param bizType 业务类型 + /// @param bizRemindedType 被提醒业务类型 + /// @param bizRemindedId 被提醒业务 ID + /// @return true 为已创建 + boolean existsByBizReminded(String bizType, String bizRemindedType, String bizRemindedId); + /// 分页查询当前用户创建的通知 /// /// @param page 分页参数 diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java index 9f8b58a..945b7a1 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java @@ -16,6 +16,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.List; + /** * 文章服务实现 * @@ -37,6 +39,11 @@ public class ArticleServiceImplement extends AbstractEntityService listByExample(Article example) { + return mapper.selectAllByExample(example); + } + @Override public void create(Article article) { Logger logger = new Logger(Logger.Module.COMMON, "COMMON_ARTICLE_CREATE"); diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java new file mode 100644 index 0000000..890571c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java @@ -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 implements GroovyScriptService { + + private final GroovyScriptMapper mapper; + private final GroovyScriptVersionMapper versionMapper; + private final Map compiledScriptMap = new ConcurrentHashMap<>(); + + @Override + protected BaseMapper 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 listVersion(String scriptId) { + TimiException.required(scriptId, "未找到脚本 ID"); + GroovyScriptVersion example = new GroovyScriptVersion(); + example.setScriptId(scriptId); + List 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 bindings) { + return executePublished(bizType, code, bindings, Object.class).result(); + } + + @Override + public GroovyScriptExecutionResult executePublished(GroovyScript.BizType bizType, String code, Map bindings, Class 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 result = executeVersion(script.getPublishedVersionId(), bindings, resultType); + TimiException.requiredTrue(script.getId().equals(result.scriptId()), "Groovy 脚本已发布版本无效"); + return result; + } + + @Override + public T execute(GroovyScript.BizType bizType, String code, Map bindings, Class resultType) { + return executePublished(bizType, code, bindings, resultType).result(); + } + + @Override + public GroovyScriptExecutionResult executeVersion(String versionId, Map bindings, Class 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 type, GroovyClassLoader classLoader) { + + private Object execute(Map 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) { + } + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java index 85ae375..fc631bd 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java @@ -46,6 +46,11 @@ public class NotifyServiceImplement extends AbstractEntityService result = service.pageByQuery(page); - if (result.getList().isEmpty()) { - return result; - } - List customerIdList = result.getList().stream() - .map(GaoCustomerPackage::getCustomerId) - .filter(value -> value != null && !value.isBlank()) - .distinct() - .toList(); - Map customerMap = customerService.mapByIdList(customerIdList); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name()); - for (GaoCustomerPackage packageEntity : result.getList()) { - GaoCustomer customer = customerMap.get(packageEntity.getCustomerId()); - if (customer != null) { - customer.setAttachmentList(attachmentMap.get(customer.getId())); - } - packageEntity.setCustomer(customer); - } + fillTransient(result.getList()); return result; } @@ -102,15 +74,7 @@ public class GaoCustomerPackageController { customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); } result.setCustomer(customer); - GaoEventRecord purchaseRecord = eventRecordService.get(result.getPurchaseEventRecordId()); - if (purchaseRecord != null) { - purchaseRecord.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, purchaseRecord.getId())); - } - result.setPurchaseEventRecord(purchaseRecord); - result.setDeliveryList(deliveryService.listByCustomerPackageId(result.getId())); - fillDeliveryTransient(result.getDeliveryList()); - result.setFundRecordList(fundRecordService.listByCustomerPackageId(result.getId())); - fillFundTransient(result.getFundRecordList()); + result.setFulfillmentList(fulfillmentService.listBySalesOrderId(result.getSalesOrderId())); return result; } @@ -134,141 +98,46 @@ public class GaoCustomerPackageController { return result; } - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) - @PostMapping("/payment/receive") - public GaoCustomerPackage receive(@RequestBody GaoFundReceiveRequest request) { - fundRecordService.receive(request); - return packageResponse(request.getCustomerPackageId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) - @PostMapping("/payment/verify") - public GaoCustomerPackage verifyPayment(@RequestParam String id) { - fundRecordService.verify(id); - return packageAfterFundAction(id); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) - @PostMapping("/payment/reject") - public GaoCustomerPackage rejectPayment(@RequestBody GaoFundActionRequest request) { - fundRecordService.reject(request); - return packageAfterFundAction(request.getFundRecordId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) - @PostMapping("/payment/cancel") - public GaoCustomerPackage cancelPayment(@RequestBody GaoFundActionRequest request) { - fundRecordService.cancel(request); - return packageAfterFundAction(request.getFundRecordId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) - @PostMapping("/refund") - public GaoCustomerPackage refund(@RequestBody GaoFundRefundRequest request) { - fundRecordService.refund(request); - return packageResponse(request.getCustomerPackageId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) - @PostMapping("/refund/confirm-none") - public GaoCustomerPackage confirmNoRefund(@RequestBody GaoFundConfirmNoRefundRequest request) { - fundRecordService.confirmNoRefund(request); - return packageResponse(request.getCustomerPackageId()); - } - - private GaoCustomerPackage packageAfterFundAction(String fundRecordId) { - var record = fundRecordService.get(fundRecordId); - TimiException.required(record, "not found fund record"); - return packageResponse(record.getBizId()); - } - - private GaoCustomerPackage packageResponse(String packageId) { - GaoCustomerPackage result = service.get(packageId); - TimiException.required(result, "not found customer package"); - fillTransient(List.of(result)); - return result; - } - private void fillTransient(List packageList) { if (packageList == null || packageList.isEmpty()) { return; } - Map> itemMap = service.mapItemListByCustomerPackageIdList(packageList.stream().map(GaoCustomerPackage::getId).toList()); + Map orderMap = salesOrderService.mapByIdList(packageList.stream() + .map(GaoCustomerPackage::getSalesOrderId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList()); + List customerIdList = packageList.stream() + .map(GaoCustomerPackage::getCustomerId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map customerMap = customerIdList.isEmpty() ? Map.of() : customerService.mapByIdList(customerIdList); + Map> attachmentMap = customerIdList.isEmpty() + ? Map.of() : attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name()); List userIdList = packageList.stream() - .flatMap(packageEntity -> Stream.of(packageEntity.getOperatorUserId(), packageEntity.getCancelOperatorUserId())) + .map(packageEntity -> orderMap.get(packageEntity.getSalesOrderId())) + .filter(order -> order != null) + .map(GaoSalesOrder::getCancelOperatorUserId) .filter(value -> value != null && !value.isBlank()) .distinct() .toList(); Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); for (GaoCustomerPackage packageEntity : packageList) { - List itemList = itemMap.getOrDefault(packageEntity.getId(), List.of()); - for (GaoCustomerPackageItem item : itemList) { - fillDerived(item); + GaoSalesOrder order = orderMap.get(packageEntity.getSalesOrderId()); + packageEntity.setSalesOrder(order); + packageEntity.setStatus(order == null ? null : order.getStatus()); + packageEntity.setCancelledAt(order == null ? null : order.getCancelledAt()); + packageEntity.setCancelOperatorUserId(order == null ? null : order.getCancelOperatorUserId()); + packageEntity.setCancelReason(order == null ? null : order.getCancelReason()); + packageEntity.setItemList(order == null ? List.of() : order.getItemList()); + GaoCustomer customer = customerMap.get(packageEntity.getCustomerId()); + if (customer != null) { + customer.setAttachmentList(attachmentMap.getOrDefault(customer.getId(), List.of())); } - packageEntity.setItemList(itemList); - packageEntity.setOperator(userMap.get(packageEntity.getOperatorUserId())); - packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId())); + packageEntity.setCustomer(customer); + String cancelOperatorUserId = order == null ? null : order.getCancelOperatorUserId(); + packageEntity.setCancelOperator(cancelOperatorUserId == null ? null : userMap.get(cancelOperatorUserId)); } } - - private void fillDeliveryTransient(List deliveryList) { - if (deliveryList == null || deliveryList.isEmpty()) { - return; - } - List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList(); - Map> itemMap = deliveryService.mapItemListByDeliveryIdList(idList); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList); - List userIdList = deliveryList.stream() - .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId())) - .filter(value -> value != null && !value.isBlank()) - .distinct() - .toList(); - Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); - for (GaoPackageDelivery delivery : deliveryList) { - delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of())); - delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of())); - delivery.setOperator(userMap.get(delivery.getOperatorUserId())); - delivery.setVerifier(userMap.get(delivery.getVerifierUserId())); - } - } - - private void fillFundTransient(List recordList) { - if (recordList == null || recordList.isEmpty()) { - return; - } - List idList = recordList.stream().map(GaoFundRecord::getId).toList(); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList); - List userIdList = recordList.stream() - .flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId())) - .filter(value -> value != null && !value.isBlank()) - .distinct() - .toList(); - Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); - for (GaoFundRecord record : recordList) { - record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); - record.setOperator(userMap.get(record.getOperatorUserId())); - record.setVerifier(userMap.get(record.getVerifierUserId())); - } - } - - private void fillDerived(GaoCustomerPackageItem item) { - BigDecimal reserved = item.getReservedQuantity() == null ? BigDecimal.ZERO : item.getReservedQuantity(); - BigDecimal delivered = item.getDeliveredQuantity() == null ? BigDecimal.ZERO : item.getDeliveredQuantity(); - BigDecimal total = item.getTotalQuantity() == null ? BigDecimal.ZERO : item.getTotalQuantity(); - item.setPendingQuantity(reserved); - item.setAvailableQuantity(total.subtract(reserved).subtract(delivered)); - item.setRemainingQuantity(total.subtract(delivered)); - } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java index 1cd87f1..4b306fe 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java @@ -127,6 +127,7 @@ public class GaoEventController { @PostMapping("/create") public void create(@RequestBody GaoEvent event) { event.setStoreId(storeScopeService.resolveStoreId(event.getStoreId())); + validatePackageConfigBinding(event); service.create(event); } @@ -141,9 +142,21 @@ public class GaoEventController { TimiException.required(dbEvent, "not found event"); storeScopeService.checkStoreId(dbEvent.getStoreId()); event.setStoreId(dbEvent.getStoreId()); + validatePackageConfigBinding(event); service.update(event); } + private void validatePackageConfigBinding(GaoEvent event) { + if (event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE) { + TimiException.required(event.getPackageConfigId(), "购买套餐事件必须绑定套餐配置"); + GaoPackageConfig packageConfig = packageConfigService.get(event.getPackageConfigId()); + TimiException.required(packageConfig, "购买套餐事件绑定的套餐配置不存在"); + TimiException.requiredTrue(event.getStoreId().equals(packageConfig.getStoreId()), "购买事件和套餐配置不属于同一门店"); + } else { + TimiException.requiredNull(event.getPackageConfigId(), "普通事件不能绑定套餐配置"); + } + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE) diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java index 11686dc..30d1ae3 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java @@ -6,9 +6,11 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.java.bean.timi.TimiException; @@ -36,6 +38,7 @@ import java.util.stream.Stream; public class GaoFundRecordController { private final GaoFundRecordService service; + private final GaoOrderPaymentService orderPaymentService; private final GaoStoreScopeService storeScopeService; private final AttachmentService attachmentService; private final UserService userService; @@ -57,8 +60,7 @@ public class GaoFundRecordController { @RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ) @PostMapping("/detail") public GaoFundRecord detail(@RequestParam String id) { - GaoFundRecord result = service.get(id); - TimiException.required(result, "not found fund record"); + GaoFundRecord result = service.detail(id); storeScopeService.checkStoreId(result.getStoreId()); fillTransient(List.of(result)); return result; @@ -69,7 +71,14 @@ public class GaoFundRecordController { @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE) @PostMapping("/revoke") public GaoFundRecord revoke(@RequestBody GaoFundRevokeRequest request) { - GaoFundRecord result = service.revoke(request); + GaoOrderPaymentRevokeRequest orderRequest = new GaoOrderPaymentRevokeRequest(); + orderRequest.setRequestNo(request.getRequestNo()); + orderRequest.setFundRecordId(request.getFundRecordId()); + orderRequest.setOccurredAt(request.getOccurredAt()); + orderRequest.setReason(request.getReason()); + orderRequest.setAttachmentList(request.getAttachmentList()); + orderRequest.setRemark(request.getRemark()); + GaoFundRecord result = orderPaymentService.revoke(orderRequest); fillTransient(List.of(result)); return result; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java new file mode 100644 index 0000000..940c519 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java @@ -0,0 +1,163 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/// GAO 统一履约接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/order/fulfillment") +public class GaoOrderFulfillmentController { + + private final GaoOrderFulfillmentService service; + private final GaoStoreScopeService storeScopeService; + private final AttachmentService attachmentService; + private final GaoCustomerService customerService; + private final UserService userService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoOrderFulfillmentPage page) { + TimiException.required(page, "not found fulfillment page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + PageResult result = service.pageByQuery(page); + fillTransient(result.getList()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_READ) + @PostMapping("/detail") + public GaoOrderFulfillment detail(@RequestParam String id) { + GaoOrderFulfillment result = service.detail(id); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_CREATE) + @PostMapping("/create") + public GaoOrderFulfillment create(@RequestBody GaoOrderFulfillmentCreateRequest request) { + GaoOrderFulfillment result = service.create(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY) + @PostMapping("/prepare-stock") + public GaoOrderFulfillment prepareStock(@RequestBody GaoOrderFulfillmentPrepareStockRequest request) { + GaoOrderFulfillment result = service.prepareStock(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY) + @PostMapping("/verify") + public GaoOrderFulfillment verify(@RequestBody GaoOrderFulfillmentActionRequest request) { + GaoOrderFulfillment result = service.verify(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY) + @PostMapping("/reject") + public GaoOrderFulfillment reject(@RequestBody GaoOrderFulfillmentActionRequest request) { + GaoOrderFulfillment result = service.reject(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_CREATE) + @PostMapping("/cancel") + public GaoOrderFulfillment cancel(@RequestBody GaoOrderFulfillmentActionRequest request) { + GaoOrderFulfillment result = service.cancel(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_REVOKE) + @PostMapping("/revoke") + public GaoOrderFulfillment revoke(@RequestBody GaoOrderFulfillmentRevokeRequest request) { + GaoOrderFulfillment result = service.revoke(request); + fillTransient(List.of(result)); + return result; + } + + private void fillTransient(List fulfillmentList) { + if (fulfillmentList == null || fulfillmentList.isEmpty()) { + return; + } + List idList = fulfillmentList.stream().map(GaoOrderFulfillment::getId).toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_ORDER_FULFILLMENT, idList); + List customerIdList = fulfillmentList.stream() + .map(GaoOrderFulfillment::getCustomerId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map customerMap = customerIdList.isEmpty() ? Map.of() : customerService.mapByIdList(customerIdList); + Map> customerAttachmentMap = customerIdList.isEmpty() + ? Map.of() + : attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name()); + List userIdList = fulfillmentList.stream() + .flatMap(item -> Stream.of(item.getOperatorUserId(), item.getVerifierUserId())) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoOrderFulfillment fulfillment : fulfillmentList) { + fulfillment.setAttachmentList(attachmentMap.getOrDefault(fulfillment.getId(), List.of())); + GaoCustomer customer = customerMap.get(fulfillment.getCustomerId()); + if (customer != null) { + customer.setAttachmentList(customerAttachmentMap.getOrDefault(customer.getId(), List.of())); + } + fulfillment.setCustomer(customer); + fulfillment.setOperator(userMap.get(fulfillment.getOperatorUserId())); + fulfillment.setVerifier(userMap.get(fulfillment.getVerifierUserId())); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java index a638a19..840152d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java @@ -4,7 +4,10 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.service.GaoEventService; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; import com.imyeyu.api.modules.user.entity.User; @@ -33,6 +36,8 @@ import java.util.Map; public class GaoPackageConfigController { private final GaoPackageConfigService service; + private final GaoEventService eventService; + private final GaoProductSkuService skuService; private final GaoStoreScopeService storeScopeService; private final UserService userService; @@ -44,7 +49,7 @@ public class GaoPackageConfigController { TimiException.required(page, "not found page"); page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); PageResult result = service.pageByQuery(page); - fillTransient(result.getList()); + fillTransient(result.getList(), false); return result; } @@ -54,7 +59,7 @@ public class GaoPackageConfigController { @PostMapping("/list/valid") public List listValid(@RequestParam String storeId) { List result = service.listValid(storeScopeService.resolveStoreId(storeId)); - fillTransient(result); + fillTransient(result, false); return result; } @@ -66,11 +71,11 @@ public class GaoPackageConfigController { GaoPackageConfig config = service.get(id); TimiException.required(config, "not found package config"); storeScopeService.checkStoreId(config.getStoreId()); - fillTransient(List.of(config)); + fillTransient(List.of(config), true); return config; } - private void fillTransient(List configList) { + private void fillTransient(List configList, boolean includeProductDetail) { if (configList == null || configList.isEmpty()) { return; } @@ -81,6 +86,28 @@ public class GaoPackageConfigController { config.setItemList(itemMap.getOrDefault(config.getId(), List.of())); config.setUpdater(updaterMap.get(config.getUpdatedBy())); } + if (includeProductDetail) { + fillProductDetails(configList); + } + } + + private void fillProductDetails(List configList) { + List skuIdList = configList.stream() + .flatMap(config -> config.getItemList().stream()) + .map(GaoPackageConfigItem::getSkuId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (skuIdList.isEmpty()) { + return; + } + Map skuMap = skuIdList.stream() + .collect(java.util.stream.Collectors.toMap(item -> item, skuService::detail)); + for (GaoPackageConfig config : configList) { + for (GaoPackageConfigItem item : config.getItemList()) { + item.setSku(skuMap.get(item.getSkuId())); + } + } } @AOPLog @@ -116,6 +143,7 @@ public class GaoPackageConfigController { GaoPackageConfig config = service.get(id); TimiException.required(config, "not found package config"); storeScopeService.checkStoreId(config.getStoreId()); + TimiException.requiredTrue(0 == eventService.countByPackageConfigId(id), "套餐配置已绑定购买事件,不能删除"); service.delete(id); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java deleted file mode 100644 index 716e04b..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.imyeyu.api.modules.gao.controller; - -import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.common.service.AttachmentService; -import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; -import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; -import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; -import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest; -import com.imyeyu.api.modules.user.entity.User; -import com.imyeyu.api.modules.user.service.UserService; -import com.imyeyu.java.bean.timi.TimiException; -import com.imyeyu.spring.annotation.AOPLog; -import com.imyeyu.spring.annotation.RequestRateLimit; -import com.imyeyu.spring.bean.PageResult; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -/// GAO 套餐发放接口 -/// -/// @author Codex -/// @since 2026-08-31 -@RestController -@RequiredArgsConstructor -@RequestMapping("/gao/package/delivery") -public class GaoPackageDeliveryController { - - private final GaoPackageDeliveryService service; - private final GaoStoreScopeService storeScopeService; - private final AttachmentService attachmentService; - private final UserService userService; - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ) - @PostMapping("/list") - public PageResult list(@RequestBody GaoPackageDeliveryPage page) { - TimiException.required(page, "not found page"); - page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); - PageResult result = service.pageByQuery(page); - fillTransient(result.getList()); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ) - @PostMapping("/detail") - public GaoPackageDelivery detail(@RequestParam String id) { - GaoPackageDelivery result = service.get(id); - TimiException.required(result, "not found delivery"); - storeScopeService.checkStoreId(result.getStoreId()); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE) - @PostMapping("/create") - public GaoPackageDelivery create(@RequestBody GaoPackageDeliveryCreateRequest request) { - GaoPackageDelivery result = service.createDelivery(request); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY) - @PostMapping("/verify") - public GaoPackageDelivery verify(@RequestParam String id) { - GaoPackageDelivery result = service.verify(id); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY) - @PostMapping("/reject") - public GaoPackageDelivery reject(@RequestBody GaoPackageDeliveryActionRequest request) { - GaoPackageDelivery result = service.reject(request); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE) - @PostMapping("/cancel") - public GaoPackageDelivery cancel(@RequestBody GaoPackageDeliveryActionRequest request) { - GaoPackageDelivery result = service.cancel(request); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_REVOKE) - @PostMapping("/revoke") - public GaoPackageDelivery revoke(@RequestBody GaoPackageDeliveryRevokeRequest request) { - GaoPackageDelivery result = service.revoke(request); - fillTransient(List.of(result)); - return result; - } - - private void fillTransient(List deliveryList) { - if (deliveryList == null || deliveryList.isEmpty()) { - return; - } - List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList(); - Map> itemMap = service.mapItemListByDeliveryIdList(idList); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList); - List userIdList = deliveryList.stream() - .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId())) - .filter(value -> value != null && !value.isBlank()) - .distinct() - .toList(); - Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); - for (GaoPackageDelivery delivery : deliveryList) { - delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of())); - delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of())); - delivery.setOperator(userMap.get(delivery.getOperatorUserId())); - delivery.setVerifier(userMap.get(delivery.getVerifierUserId())); - } - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductChartController.java new file mode 100644 index 0000000..5381646 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductChartController.java @@ -0,0 +1,126 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.service.GaoProductChartService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/// GAO 产品图表接口 +/// +/// @author Codex +/// @since 2026-09-06 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product/chart") +public class GaoProductChartController { + + private final GaoProductChartService service; + private final GaoStoreScopeService storeScopeService; + + /// 查询产品每日销售统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/daily") + public List salesDaily(@RequestBody GaoProductChartReq req) { + return service.salesDaily(normalize(req)); + } + + /// 查询产品销售排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/rank") + public List salesRank(@RequestBody GaoProductChartReq req) { + return service.salesRank(normalize(req)); + } + + /// 查询产品每日履约统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/daily") + public List fulfillmentDaily(@RequestBody GaoProductChartReq req) { + return service.fulfillmentDaily(normalize(req)); + } + + /// 查询产品履约进度排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/rank") + public List fulfillmentRank(@RequestBody GaoProductChartReq req) { + return service.fulfillmentRank(normalize(req)); + } + + /// 查询产品每日库存变动统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/movement/daily") + public List stockMovementDaily(@RequestBody GaoProductChartReq req) { + return service.stockMovementDaily(normalize(req)); + } + + /// 查询产品库存健康度 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/health") + public List stockHealth(@RequestBody GaoProductChartReq req) { + return service.stockHealth(normalize(req)); + } + + /// 查询产品类型统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/structure/type") + public List type(@RequestBody GaoProductChartReq req) { + return service.type(normalize(req)); + } + + /// 查询产品分类统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/structure/category") + public List category(@RequestBody GaoProductChartReq req) { + return service.category(normalize(req)); + } + + /// 查询产品销售单位排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/unit/rank") + public List unitSalesRank(@RequestBody GaoProductChartReq req) { + return service.unitSalesRank(normalize(req)); + } + + private GaoProductChartReq normalize(GaoProductChartReq req) { + TimiException.required(req, "not found product chart req"); + req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId())); + return req; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java new file mode 100644 index 0000000..101003b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java @@ -0,0 +1,248 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; +import com.imyeyu.api.modules.gao.service.GaoProductSpecService; +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/// GAO 产品库接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product") +public class GaoProductController { + + private final GaoProductCategoryService categoryService; + private final GaoProductService productService; + private final GaoProductSkuService skuService; + private final GaoProductSpecService specService; + private final GaoStoreScopeService storeScopeService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_READ) + @PostMapping("/category/list") + public PageResult categoryList(@RequestBody GaoProductCategoryPage page) { + TimiException.required(page, "not found category page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return categoryService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_CREATE) + @PostMapping("/category/create") + public String categoryCreate(@RequestBody GaoProductCategory category) { + TimiException.required(category, "not found category"); + category.setStoreId(storeScopeService.resolveStoreId(category.getStoreId())); + categoryService.create(category); + return category.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE) + @PostMapping("/category/update") + public void categoryUpdate(@RequestBody GaoProductCategory request) { + TimiException.required(request, "not found category"); + TimiException.required(request.getId(), "not found category.id"); + GaoProductCategory category = categoryService.get(request.getId()); + TimiException.required(category, "not found category"); + storeScopeService.checkStoreId(category.getStoreId()); + request.setStoreId(category.getStoreId()); + categoryService.update(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE) + @PostMapping("/category/sort") + public void categorySort(@RequestBody List idList) { + categoryService.sort(idList); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE) + @PostMapping("/category/disable") + public void categoryDisable(@RequestParam String id) { + categoryService.disable(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoProductPage page) { + TimiException.required(page, "not found product page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return productService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/detail") + public GaoProduct detail(@RequestParam String id) { + GaoProduct result = productService.detail(id); + storeScopeService.checkStoreId(result.getStoreId()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CREATE) + @PostMapping("/create") + public String create(@RequestBody GaoProduct product) { + TimiException.required(product, "not found product"); + product.setStoreId(storeScopeService.resolveStoreId(product.getStoreId())); + productService.create(product); + return product.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoProduct request) { + TimiException.required(request, "not found product"); + TimiException.required(request.getId(), "not found product.id"); + GaoProduct product = productService.get(request.getId()); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + request.setStoreId(product.getStoreId()); + productService.update(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/disable") + public void disable(@RequestParam String id) { + GaoProduct product = productService.get(id); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + productService.disable(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/sku/detail") + public GaoProductSku skuDetail(@RequestParam String id) { + GaoProductSku result = skuService.detail(id); + storeScopeService.checkStoreId(result.getStoreId()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CREATE) + @PostMapping("/sku/create") + public String skuCreate(@RequestBody GaoProductSku sku) { + TimiException.required(sku, "not found product sku"); + GaoProduct product = productService.get(sku.getProductId()); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + skuService.create(sku); + return sku.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/sku/update") + public void skuUpdate(@RequestBody GaoProductSku request) { + TimiException.required(request, "not found product sku"); + TimiException.required(request.getId(), "not found product sku.id"); + GaoProductSku sku = skuService.get(request.getId()); + TimiException.required(sku, "not found product sku"); + storeScopeService.checkStoreId(sku.getStoreId()); + request.setProductId(sku.getProductId()); + skuService.update(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/sku/disable") + public void skuDisable(@RequestParam String id) { + GaoProductSku sku = skuService.get(id); + TimiException.required(sku, "not found product sku"); + storeScopeService.checkStoreId(sku.getStoreId()); + skuService.disable(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/spec/list") + public List specList(@RequestParam String productId) { + GaoProduct product = productService.get(productId); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + return specService.listByProductId(productId); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/spec/create") + public String specCreate(@RequestBody GaoProductSpec spec) { + TimiException.required(spec, "not found product spec"); + GaoProduct product = productService.get(spec.getProductId()); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + specService.createSpec(spec); + return spec.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/spec/value/create") + public String specValueCreate(@RequestBody GaoProductSpecValue value) { + TimiException.required(value, "not found product spec value"); + String productId = specService.getProductIdBySpecId(value.getSpecId()); + TimiException.required(productId, "规格项不存在"); + GaoProduct product = productService.get(productId); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + specService.createValue(value); + return value.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/sellable/list") + public List sellableList(@RequestBody GaoProductPage page) { + TimiException.required(page, "not found product page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return productService.listSellable(page); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductSkuChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductSkuChartController.java new file mode 100644 index 0000000..7f6d42c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductSkuChartController.java @@ -0,0 +1,106 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.service.GaoProductSkuChartService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/// GAO SKU 图表接口 +/// +/// @author Codex +/// @since 2026-09-07 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product/sku/chart") +public class GaoProductSkuChartController { + + private final GaoProductSkuChartService service; + private final GaoStoreScopeService storeScopeService; + + /// 查询 SKU 每日销售统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/daily") + public List salesDaily(@RequestBody GaoProductChartReq req) { + return service.salesDaily(normalize(req)); + } + + /// 查询 SKU 销售排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/rank") + public List salesRank(@RequestBody GaoProductChartReq req) { + return service.salesRank(normalize(req)); + } + + /// 查询 SKU 单位销售排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/unit/rank") + public List unitSalesRank(@RequestBody GaoProductChartReq req) { + return service.unitSalesRank(normalize(req)); + } + + /// 查询 SKU 每日履约统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/daily") + public List fulfillmentDaily(@RequestBody GaoProductChartReq req) { + return service.fulfillmentDaily(normalize(req)); + } + + /// 查询 SKU 履约进度排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/rank") + public List fulfillmentRank(@RequestBody GaoProductChartReq req) { + return service.fulfillmentRank(normalize(req)); + } + + /// 查询 SKU 每日库存变动统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/movement/daily") + public List stockMovementDaily(@RequestBody GaoProductChartReq req) { + return service.stockMovementDaily(normalize(req)); + } + + /// 查询 SKU 库存健康度 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/health") + public List stockHealth(@RequestBody GaoProductChartReq req) { + return service.stockHealth(normalize(req)); + } + + private GaoProductChartReq normalize(GaoProductChartReq req) { + TimiException.required(req, "SKU 图表请求不能为空"); + req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId())); + return req; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java new file mode 100644 index 0000000..b8e4777 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java @@ -0,0 +1,140 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.service.GaoProductStockService; +import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/// GAO 产品库存接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product/stock") +public class GaoProductStockController { + + private final GaoProductStockService stockService; + private final GaoProductStockRecordService recordService; + private final GaoStoreScopeService storeScopeService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoProductStockPage page) { + TimiException.required(page, "not found stock page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return stockService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/product-list") + public PageResult productList(@RequestBody GaoProductStockPage page) { + TimiException.required(page, "not found stock product page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return stockService.pageProductByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/detail") + public GaoProductStock detail(@RequestParam(required = false) String storeId, @RequestParam String skuId) { + return stockService.detail(storeScopeService.resolveStoreId(storeId), skuId); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/initial-in") + public GaoProductStock initialIn(@RequestBody GaoProductStockChangeRequest request) { + return stockService.initialIn(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/manual-in") + public GaoProductStock manualIn(@RequestBody GaoProductStockChangeRequest request) { + return stockService.manualIn(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/manual-out") + public GaoProductStock manualOut(@RequestBody GaoProductStockChangeRequest request) { + return stockService.manualOut(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_MANUAL_REVOKE) + @PostMapping("/manual-revoke") + public GaoProductStock manualRevoke(@RequestBody GaoProductStockManualRevokeRequest request) { + return stockService.manualRevoke(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/stocktake-gain") + public GaoProductStock stocktakeGain(@RequestBody GaoProductStockChangeRequest request) { + return stockService.stocktakeGain(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/stocktake-loss") + public GaoProductStock stocktakeLoss(@RequestBody GaoProductStockChangeRequest request) { + return stockService.stocktakeLoss(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/record/list") + public PageResult recordList(@RequestBody GaoProductStockRecordPage page) { + TimiException.required(page, "not found stock record page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return recordService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/record/detail") + public GaoProductStockRecord recordDetail(@RequestParam String id) { + GaoProductStockRecord result = recordService.detail(id); + storeScopeService.checkStoreId(result.getStoreId()); + return result; + } + + private GaoProductStockChangeRequest normalize(GaoProductStockChangeRequest request) { + TimiException.required(request, "not found stock change request"); + request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId())); + return request; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java new file mode 100644 index 0000000..a01ec67 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java @@ -0,0 +1,156 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderChartService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderSkuRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/// GAO 销售订单图表接口 +/// +/// @author Codex +/// @since 2026-09-06 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/sales/order/chart") +public class GaoSalesOrderChartController { + + private final GaoSalesOrderChartService service; + private final GaoStoreScopeService storeScopeService; + + /// 查询销售订单每日统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/daily") + public List daily(@RequestBody GaoSalesOrderChartReq req) { + return service.daily(normalize(req)); + } + + /// 查询销售订单类型统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/type") + public List type(@RequestBody GaoSalesOrderChartReq req) { + return service.type(normalize(req)); + } + + /// 查询销售订单付款状态统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/payment") + public List payment(@RequestBody GaoSalesOrderChartReq req) { + return service.payment(normalize(req)); + } + + /// 查询客户订单排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/rank") + public List customerRank(@RequestBody GaoSalesOrderChartReq req) { + return service.customerRank(normalize(req)); + } + + /// 查询指定客户订单产品排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/product/rank") + public List customerProductRank(@RequestBody GaoSalesOrderChartReq req) { + return service.customerProductRank(normalize(req)); + } + + /// 查询指定客户订单 SKU 排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/sku/rank") + public List customerSkuRank(@RequestBody GaoSalesOrderChartReq req) { + return service.customerSkuRank(normalize(req)); + } + + /// 查询销售订单状态统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/status") + public List status(@RequestBody GaoSalesOrderChartReq req) { + return service.status(normalize(req)); + } + + /// 查询销售订单应收统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/receivable") + public GaoSalesOrderReceivableStatView receivable(@RequestBody GaoSalesOrderChartReq req) { + return service.receivable(normalize(req)); + } + + /// 查询客户每日有效消费统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/consumption/daily") + public List consumptionDaily(@RequestBody GaoSalesOrderChartReq req) { + return service.consumptionDaily(normalize(req)); + } + + /// 查询客户订单日历统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/calendar") + public List calendar(@RequestBody GaoSalesOrderChartReq req) { + return service.calendar(normalize(req)); + } + + /// 查询客户每日复购统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/repeat/daily") + public List repeatDaily(@RequestBody GaoSalesOrderChartReq req) { + return service.repeatDaily(normalize(req)); + } + + /// 查询客户复购间隔统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/repeat/interval") + public List repeatInterval(@RequestBody GaoSalesOrderChartReq req) { + return service.repeatInterval(normalize(req)); + } + + private GaoSalesOrderChartReq normalize(GaoSalesOrderChartReq req) { + TimiException.required(req, "not found sales order chart req"); + req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId())); + return req; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java new file mode 100644 index 0000000..1701140 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java @@ -0,0 +1,144 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderUpdateRequest; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/// GAO 销售订单和资金接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/sales/order") +public class GaoSalesOrderController { + + private final GaoSalesOrderService service; + private final GaoOrderPaymentService paymentService; + private final GaoStoreScopeService storeScopeService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoSalesOrderPage page) { + TimiException.required(page, "not found sales order page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return service.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ) + @PostMapping("/detail") + public GaoSalesOrder detail(@RequestParam String id) { + return service.detail(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_CREATE) + @PostMapping("/direct-product/create") + public GaoSalesOrder createDirectProduct(@RequestBody GaoDirectProductOrderCreateRequest request) { + TimiException.required(request, "not found direct product order request"); + request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId())); + return service.createDirectProductOrder(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_UPDATE) + @PostMapping("/update") + public GaoSalesOrder update(@RequestBody GaoSalesOrderUpdateRequest request) { + TimiException.required(request, "not found sales order update request"); + TimiException.required(request.getId(), "not found sales order.id"); + return service.updateRemarkAndAttachments(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_CANCEL) + @PostMapping("/cancel") + public GaoSalesOrder cancel(@RequestBody GaoSalesOrderCancelRequest request) { + return service.cancel(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) + @PostMapping("/payment/receive") + public GaoFundRecord receive(@RequestBody GaoOrderPaymentReceiveRequest request) { + return paymentService.receive(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) + @PostMapping("/payment/verify") + public GaoFundRecord verify(@RequestParam String id) { + return paymentService.verify(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) + @PostMapping("/payment/reject") + public GaoFundRecord reject(@RequestBody GaoOrderPaymentActionRequest request) { + return paymentService.reject(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) + @PostMapping("/payment/cancel") + public GaoFundRecord cancelPayment(@RequestBody GaoOrderPaymentActionRequest request) { + return paymentService.cancel(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) + @PostMapping("/refund") + public GaoFundRecord refund(@RequestBody GaoOrderPaymentRefundRequest request) { + return paymentService.refund(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) + @PostMapping("/refund/confirm-none") + public GaoFundRecord confirmNoRefund(@RequestBody GaoOrderPaymentNoRefundRequest request) { + return paymentService.confirmNoRefund(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE) + @PostMapping("/fund/revoke") + public GaoFundRecord revoke(@RequestBody GaoOrderPaymentRevokeRequest request) { + return paymentService.revoke(request); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java index 85616cc..bf1086d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java @@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.controller; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreService; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerConsumptionRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerConsumptionRecord.java new file mode 100644 index 0000000..74d3ff0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerConsumptionRecord.java @@ -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; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java index facd096..2425485 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java @@ -16,100 +16,38 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public class GaoCustomerPackage extends UUIDEntity { - /// 客户套餐状态 - public enum Status { - - /// 履约中 - ACTIVE, - - /// 已全部履约 - FULFILLED, - - /// 已取消 - CANCELLED - } - - /// 付款状态 - public enum PaymentStatus { - - /// 未付款 - UNPAID, - - /// 部分付款 - PARTIALLY_PAID, - - /// 已付清 - PAID - } - - /// 退款状态 - public enum RefundStatus { - - /// 无需退款 - NOT_REQUIRED, - - /// 待退款或待确认无需退款 - PENDING, - - /// 已完成退款处理 - REFUNDED - } - /// 门店 ID private String storeId; + /// 销售订单 ID + private String salesOrderId; + /// 客户 ID private String customerId; /// 来源套餐配置 ID private String packageConfigId; - /// 购买事件登记 ID - private String purchaseEventRecordId; - - /// 购买幂等请求号 - private String requestNo; - /// 套餐名称快照 private String packageNameSnapshot; /// 标准价快照,单位分 private Long standardAmountSnapshot; - /// 实际成交价,单位分 - private Long saleAmount; + /// 订单投影状态 + @Transient + private GaoSalesOrder.Status status; - /// 待核销收款合计,单位分 - private Long pendingPaidAmount; - - /// 累计有效收款,单位分 - private Long paidAmount; - - /// 累计有效退款,单位分 - private Long refundedAmount; - - /// 套餐状态 - private Status status; - - /// 付款状态 - private PaymentStatus paymentStatus; - - /// 退款状态 - private RefundStatus refundStatus; - - /// 购买时间 - private Long purchasedAt; - - /// 购买登记人 - private String operatorUserId; - - /// 取消时间 + /// 订单投影取消时间 + @Transient private Long cancelledAt; - /// 取消操作人 + /// 订单投影取消操作人 ID + @Transient private String cancelOperatorUserId; - /// 取消原因 + /// 订单投影取消原因 + @Transient private String cancelReason; /// 备注 @@ -119,27 +57,19 @@ public class GaoCustomerPackage extends UUIDEntity { @Transient private GaoCustomer customer; - /// 购买事件登记 - @Transient - private GaoEventRecord purchaseEventRecord; - - /// 购买操作人 - @Transient - private User operator; - /// 取消操作人 @Transient private User cancelOperator; - /// 产品权益列表 + /// 关联销售订单 @Transient - private List itemList; + private GaoSalesOrder salesOrder; - /// 发放历史 + /// 关联订单可履约明细投影 @Transient - private List deliveryList; + private List itemList; - /// 资金历史 + /// 统一履约历史 @Transient - private List fundRecordList; + private List fulfillmentList; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java deleted file mode 100644 index 450f06a..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.imyeyu.api.modules.gao.entity; - -import com.imyeyu.spring.annotation.table.Transient; -import com.imyeyu.spring.entity.UUIDEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.math.BigDecimal; - -/// GAO 客户套餐产品权益快照 -/// -/// @author Codex -/// @since 2026-08-31 -@Data -@EqualsAndHashCode(callSuper = true) -public class GaoCustomerPackageItem extends UUIDEntity { - - /// 客户套餐 ID - private String customerPackageId; - - /// 产品名称快照 - private String productNameSnapshot; - - /// 产品单位快照 - private String unitSnapshot; - - /// 总权益数量 - private BigDecimal totalQuantity; - - /// 待核销占用数量 - private BigDecimal reservedQuantity; - - /// 已核销净发放数量 - private BigDecimal deliveredQuantity; - - /// 排序值 - private Integer sort; - - /// 备注 - private String remark; - - /// 待核销数量 - @Transient - private BigDecimal pendingQuantity; - - /// 当前可发数量 - @Transient - private BigDecimal availableQuantity; - - /// 权益剩余数量 - @Transient - private BigDecimal remainingQuantity; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java index c48b270..f9c087b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java @@ -25,7 +25,10 @@ public class GaoEvent extends UUIDEntity { GENERAL, /// 购买产品套餐 - PACKAGE_PURCHASE + PACKAGE_PURCHASE, + + /// 已退役的历史消费事件,仅保留归档和溯源 + LEGACY_CONSUMPTION } /// diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java index 7292ca0..20390d0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java @@ -9,7 +9,7 @@ import lombok.EqualsAndHashCode; import java.util.List; -/// GAO 通用资金操作流水 +/// GAO 销售订单资金操作流水 /// /// @author Codex /// @since 2026-08-31 @@ -20,8 +20,8 @@ public class GaoFundRecord extends UUIDEntity { /// 资金业务类型 public enum BizType { - /// 客户套餐 - CUSTOMER_PACKAGE + /// 销售订单 + SALES_ORDER } /// 资金操作类型 @@ -101,6 +101,17 @@ public class GaoFundRecord extends UUIDEntity { /// 备注 private String remark; + /// 关联的冲正流水 ID + @Transient + private String revokeRecordId; + + /// 关联销售订单总价,单位分 + @Transient + private Long salesOrderAmount; + + /// 流水发生时关联销售订单剩余待收款金额快照,单位分 + private Long salesOrderUnpaidAmountSnapshot; + /// 操作人 @Transient private User operator; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java similarity index 53% rename from src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java rename to src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java index 34f2b6e..48f260d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java @@ -2,34 +2,29 @@ package com.imyeyu.api.modules.gao.entity; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; import com.imyeyu.spring.annotation.table.Transient; -import com.imyeyu.spring.entity.UUIDEntity; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import com.imyeyu.spring.entity.Updatable; import lombok.Data; -import lombok.EqualsAndHashCode; import java.util.List; -/// GAO 套餐发放操作 +/// GAO 统一履约单 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -@EqualsAndHashCode(callSuper = true) -public class GaoPackageDelivery extends UUIDEntity { +public class GaoOrderFulfillment implements IDEntity, Creatable, Updatable { - /// 发放操作类型 - public enum OperationType { - - /// 正常发放 - DELIVER, - - /// 整单冲正 - REVOKE - } - - /// 发放状态 + /// 履约状态 public enum Status { + /// 等待库存 + WAITING_STOCK, + /// 待核销 PENDING, @@ -42,12 +37,16 @@ public class GaoPackageDelivery extends UUIDEntity { /// 提交人已撤销 CANCELLED, - /// 已核销后被整单冲正 + /// 已核销后冲正 REVOKED } - /// 客户套餐 ID - private String customerPackageId; + @Id + @AutoUUID + private String id; + + /// 销售订单 ID + private String salesOrderId; /// 门店 ID private String storeId; @@ -55,10 +54,7 @@ public class GaoPackageDelivery extends UUIDEntity { /// 客户 ID private String customerId; - /// 操作类型 - private OperationType operationType; - - /// 发放状态 + /// 履约状态 private Status status; /// 客户端幂等请求号 @@ -67,7 +63,7 @@ public class GaoPackageDelivery extends UUIDEntity { /// 提交人 private String operatorUserId; - /// 实际发放时间 + /// 实际履约时间 private Long deliveredAt; /// 核销或拒绝操作人 @@ -76,8 +72,8 @@ public class GaoPackageDelivery extends UUIDEntity { /// 核销或拒绝时间 private Long verifiedAt; - /// 冲正单指向原发放单 - private String sourceDeliveryId; + /// 冲正单指向原履约单 + private String sourceFulfillmentId; /// 拒绝、撤销或冲正原因 private String reason; @@ -85,21 +81,33 @@ public class GaoPackageDelivery extends UUIDEntity { /// 备注 private String remark; - /// 操作人 + private Long createdAt; + + private Long updatedAt; + + /// 提交人 @Transient private User operator; + /// 客户 + @Transient + private GaoCustomer customer; + /// 核销或拒绝操作人 @Transient private User verifier; - /// 原发放单 + /// 原履约单 @Transient - private GaoPackageDelivery sourceDelivery; + private GaoOrderFulfillment sourceFulfillment; - /// 发放明细 + /// 履约明细 @Transient - private List itemList; + private List itemList; + + /// 库存处理流水 + @Transient + private List stockRecordList; /// 附件 @Transient diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java new file mode 100644 index 0000000..645efbf --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java @@ -0,0 +1,85 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 统一履约明细 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentItem implements IDEntity, Creatable { + + @Id + @AutoUUID + private String id; + + /// 履约单 ID + private String fulfillmentId; + + /// SKU ID + private String skuId; + + /// SKU 单位 ID + private String skuUnitId; + + /// 产品图片附件 + @Transient + private List attachmentList; + + /// 来源销售订单明细 ID + private String sourceOrderItemId; + + /// 本次履约数量 + private BigDecimal quantity; + + /// 基础单位数量 + private BigDecimal baseQuantity; + + /// 操作前待履约数量 + private BigDecimal reservedBefore; + + /// 操作后待履约数量 + private BigDecimal reservedAfter; + + /// 操作前已履约数量 + private BigDecimal fulfilledBefore; + + /// 操作后已履约数量 + private BigDecimal fulfilledAfter; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + + /// SKU 规格结构快照,数组元素包含 specId、specName、specValueId、specValueName + private JsonNode skuSpecSnapshot; + + /// 单位快照 + private String unitSnapshot; + + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; + + private Long createdAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java index 205d250..cb40d94 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.entity; +import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; import lombok.EqualsAndHashCode; @@ -17,11 +18,32 @@ public class GaoPackageConfigItem extends UUIDEntity { /// 套餐配置 ID private String packageConfigId; - /// 产品名称 - private String productName; + /// SKU ID + private String skuId; - /// 产品单位 - private String unit; + /// SKU 单位 ID + private String skuUnitId; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + + /// 单位快照 + private String unitSnapshot; + + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; /// 默认数量 private BigDecimal defaultQuantity; @@ -31,4 +53,8 @@ public class GaoPackageConfigItem extends UUIDEntity { /// 备注 private String remark; + + /// SKU 详情 + @Transient + private GaoProductSku sku; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java deleted file mode 100644 index f1fab81..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.imyeyu.api.modules.gao.entity; - -import com.imyeyu.spring.entity.UUIDEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.math.BigDecimal; - -/// GAO 套餐发放产品明细 -/// -/// @author Codex -/// @since 2026-08-31 -@Data -@EqualsAndHashCode(callSuper = true) -public class GaoPackageDeliveryItem extends UUIDEntity { - - /// 发放单 ID - private String deliveryId; - - /// 客户套餐产品 ID - private String customerPackageItemId; - - /// 本次数量 - private BigDecimal quantity; - - /// 操作前占用数量快照 - private BigDecimal reservedBefore; - - /// 操作后占用数量快照 - private BigDecimal reservedAfter; - - /// 操作前已发数量快照 - private BigDecimal deliveredBefore; - - /// 操作后已发数量快照 - private BigDecimal deliveredAfter; - - /// 产品名称快照 - private String productNameSnapshot; - - /// 产品单位快照 - private String unitSnapshot; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java index f0e6826..97a385c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java @@ -17,13 +17,13 @@ public class GaoPointLedger extends UUIDEntity { /// 流水变化类型 public enum ChangeType { - /// 登记事件发放积分 + /// 规则发放积分 EARN, - /// 登记事件消费积分 + /// 规则扣减积分 CONSUME, - /// 删除登记记录撤销积分 + /// 撤销积分 REVOKE, /// 人工调整积分 @@ -40,6 +40,16 @@ public class GaoPointLedger extends UUIDEntity { DECREASE } + /// 直接触发来源类型 + public enum TriggerSourceType { + + /// 事件登记 + EVENT_RECORD, + + /// 订单首次全额实收 + SALES_ORDER_PAID + } + /// 客户积分账户 ID private String accountId; @@ -67,11 +77,20 @@ public class GaoPointLedger extends UUIDEntity { /// 命中的积分规则 ID private String ruleId; - /// 登记事件 ID - private String eventId; + /// 直接触发来源类型 + private TriggerSourceType triggerSourceType; - /// 登记记录 ID - private String eventRecordId; + /// 直接触发来源 ID + private String triggerSourceId; + + /// 关联销售订单 ID + private String salesOrderId; + + /// 实际执行 Groovy 脚本版本 ID,简单规则为空 + private String groovyScriptVersionId; + + /// 命中时的规则快照 + private JsonNode ruleSnapshot; /// 来源快照 private JsonNode sourcePayload; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java index a17a27c..eba636a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java @@ -1,6 +1,5 @@ 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; @@ -29,43 +28,47 @@ public class GaoPointRule extends UUIDEntity { DELETED } - /// 触发类型 - public enum TriggerType { + /// 规则模式 + public enum RuleMode { - /// 每登记一次触发一次 - EVERY_TIME, + /// 简单规则,固定积分值 + SIMPLE, - /// 客户首次登记该事件触发一次 - FIRST_TIME, - - /// 客户累计登记达到 N 次后每次触发 - AFTER_COUNT_EVERY_TIME, - - /// 使用登记接口提交的积分值触发 - SOURCE_VALUE, - - /// 按来源事件数值累计分档触发 - CUSTOMER_EVENT_VALUE_TIER + /// Groovy 脚本规则 + GROOVY } - /// 积分值模式 - public enum PointMode { + /// 直接触发来源类型 + public enum TriggerSourceType { - /// 使用规则固定积分值 - FIXED, + /// 事件登记 + EVENT_RECORD, - /// 使用登记接口提交的积分值 - SOURCE_VALUE, + /// 订单首次全额实收 + SALES_ORDER_PAID + } - /// 使用条件配置中的分档积分值 - TIER + /// 触发对象筛选类型 + public enum TriggerRefType { + + /// GAO 登记事件 + EVENT } /// 门店 ID private String storeId; - /// 登记事件 ID - private String eventId; + /// 规则模式 + private RuleMode ruleMode; + + /// 直接触发来源类型 + private TriggerSourceType triggerSourceType; + + /// 触发对象筛选类型 + private TriggerRefType triggerRefType; + + /// 触发对象筛选 ID + private String triggerRefId; /// 规则名称 private String name; @@ -73,17 +76,11 @@ public class GaoPointRule extends UUIDEntity { /// 规则状态 private Status status; - /// 触发类型 - private TriggerType triggerType; - - /// 积分值模式 - private PointMode pointMode; - - /// 固定积分值 + /// 固定积分值,简单规则使用 private Long pointValue; - /// 条件配置 - private JsonNode conditionJson; + /// Groovy 脚本 ID,GROOVY 模式使用 + private String groovyScriptId; /// 生效开始时间戳,毫秒,空为不限制 private Long beginAt; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java new file mode 100644 index 0000000..6186823 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java @@ -0,0 +1,97 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 产品 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProduct extends UUIDEntity { + + /// 产品类型 + public enum ProductType { + + /// 服务,不管理库存 + SERVICE, + + /// 实物,默认管理库存 + GOODS, + + /// 消耗品,可选择是否管理库存 + CONSUMABLE + } + + /// 产品附件类型 + public enum AttachType { + + /// 产品图片 + PHOTO, + + /// 其他产品文件 + OTHER + } + + /// 产品状态 + public enum Status { + + /// 启用 + ACTIVE, + + /// 停用 + INACTIVE + } + + /// 门店 ID + private String storeId; + + /// 分类 ID + private String categoryId; + + /// 门店内产品编码 + private String code; + + /// 产品名称 + private String name; + + /// 产品类型 + private ProductType productType; + + /// true 为启用库存管理 + private Boolean stockEnabled; + + /// 状态 + private Status status; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; + + /// 最后修改用户 ID + private String updatedBy; + + /// 产品分类 + @Transient + private GaoProductCategory category; + + /// 产品规格列表 + @Transient + private List specList; + + /// 产品 SKU 列表 + @Transient + private List skuList; + + /// 产品附件列表 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductCategory.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductCategory.java new file mode 100644 index 0000000..f50546c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductCategory.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 产品分类 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductCategory extends UUIDEntity { + + /// 分类状态 + public enum Status { + + /// 启用 + ACTIVE, + + /// 停用 + INACTIVE + } + + /// 门店 ID + private String storeId; + + /// 分类名称 + private String name; + + /// 排序值 + private Integer sort; + + /// 状态 + private Status status; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSku.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSku.java new file mode 100644 index 0000000..732cc79 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSku.java @@ -0,0 +1,83 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 产品 SKU +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductSku extends UUIDEntity { + + /// SKU 状态 + public enum Status { + + /// 启用 + ACTIVE, + + /// 停用 + INACTIVE + } + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 门店内 SKU 编码 + private String code; + + /// 条码 + private String barcode; + + /// SKU 展示名称 + private String name; + + /// 规格摘要 + private String specSummary; + + /// 规格值组合键 + private String specValueKey; + + /// true 为启用库存管理 + private Boolean stockEnabled; + + /// true 为允许业务数量保留小数 + private Boolean allowDecimalQuantity; + + /// 最低库存数量,基础单位 + private BigDecimal minStockQuantity; + + /// 状态 + private Status status; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; + + /// 最后修改用户 ID + private String updatedBy; + + /// 规格值 ID 列表 + @Transient + private List specValueIdList; + + /// SKU 单位列表 + @Transient + private List unitList; + + /// SKU 附件列表 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuSpecValue.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuSpecValue.java new file mode 100644 index 0000000..fc5d847 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuSpecValue.java @@ -0,0 +1,29 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO SKU 规格值关联 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductSkuSpecValue implements IDEntity, Creatable { + + @Id + @AutoUUID + private String id; + + /// SKU ID + private String skuId; + + /// 规格值 ID + private String specValueId; + + private Long createdAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuUnit.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuUnit.java new file mode 100644 index 0000000..af09197 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuUnit.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 产品 SKU 业务单位 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductSkuUnit extends UUIDEntity { + + /// SKU ID + private String skuId; + + /// 单位名称 + private String name; + + /// 每个业务单位对应的基础单位数量 + private BigDecimal baseQuantityPerUnit; + + /// 该业务单位建议售价,单位分 + private Long salePrice; + + /// true 为基础库存单位 + private Boolean isBase; + + /// true 为允许采购入库 + private Boolean allowPurchase; + + /// true 为允许销售或套餐配置 + private Boolean allowSale; + + /// 排序值 + private Integer sort; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java new file mode 100644 index 0000000..947a1e9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 产品规格项 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductSpec extends UUIDEntity { + + /// 前端创建请求中的临时关联键 + @Transient + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String clientKey; + + /// 产品 ID + private String productId; + + /// 规格名称 + private String name; + + /// 排序值 + private Integer sort; + + /// 规格值列表 + @Transient + private List valueList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java new file mode 100644 index 0000000..e44ac55 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 产品规格值 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductSpecValue extends UUIDEntity { + + /// 前端创建请求中的临时关联键 + @Transient + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String clientKey; + + /// 规格项 ID + private String specId; + + /// 规格值名称 + private String name; + + /// true 为启用 + private Boolean enabled; + + /// 排序值 + private Integer sort; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java new file mode 100644 index 0000000..df352b6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 产品库存余额 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductStock extends UUIDEntity { + + /// 门店 ID + private String storeId; + + /// SKU ID + private String skuId; + + /// 实际库存,基础单位 + private BigDecimal onHandQuantity; + + /// 已预占库存,基础单位 + private BigDecimal reservedQuantity; + + /// 最后操作人 + private String updatedBy; + + /// SKU + @Transient + private GaoProductSku sku; + + /// 可用库存,基础单位 + @Transient + private BigDecimal availableQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java new file mode 100644 index 0000000..e1f0ded --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java @@ -0,0 +1,165 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 库存变更流水 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoProductStockRecord implements IDEntity, Creatable { + + /// 库存流水业务类型 + public enum BizType { + + /// 人工库存操作 + PRODUCT_STOCK, + + /// 订单履约库存操作 + ORDER_FULFILLMENT + } + + /// 库存操作类型 + public enum OperationType { + + /// 期初入库 + INITIAL_IN, + + /// 手工入库 + MANUAL_IN, + + /// 手工出库 + MANUAL_OUT, + + /// 手工流水冲正 + MANUAL_REVOKE, + + /// 履约预占 + ORDER_RESERVE, + + /// 履约释放预占 + ORDER_RELEASE, + + /// 履约出库 + ORDER_OUT, + + /// 履约冲正入库 + ORDER_REVOKE, + + /// 盘盈 + STOCKTAKE_GAIN, + + /// 盘亏 + STOCKTAKE_LOSS + } + + @Id + @AutoUUID + private String id; + + /// 门店 ID + private String storeId; + + /// SKU ID + private String skuId; + + /// SKU 单位 ID + private String skuUnitId; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + + /// SKU 规格结构快照,数组元素包含 specId、specName、specValueId、specValueName + private JsonNode skuSpecSnapshot; + + /// 单位名称快照 + private String unitSnapshot; + + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; + + /// 业务数量 + private BigDecimal quantity; + + /// 基础单位数量 + private BigDecimal baseQuantity; + + /// 操作类型 + private OperationType operationType; + + /// 实际库存增量 + private BigDecimal onHandDelta; + + /// 预占库存增量 + private BigDecimal reservedDelta; + + /// 操作前实际库存 + private BigDecimal onHandBefore; + + /// 操作后实际库存 + private BigDecimal onHandAfter; + + /// 操作前预占库存 + private BigDecimal reservedBefore; + + /// 操作后预占库存 + private BigDecimal reservedAfter; + + /// 来源业务类型 + private BizType bizType; + + /// 来源业务 ID + private String bizId; + + /// 冲正来源流水 ID + private String sourceRecordId; + + /// 关联的冲正流水 ID + @Transient + private String revokeRecordId; + + /// 幂等请求号 + private String requestNo; + + /// 操作用户 ID + private String operatorUserId; + + /// 发生时间 + private Long occurredAt; + + /// 备注 + private String remark; + + private Long createdAt; + + /// 操作用户 + @Transient + private User operator; + + /// 附件 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java new file mode 100644 index 0000000..4e9e1f4 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java @@ -0,0 +1,160 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 销售订单 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoSalesOrder extends UUIDEntity { + + /// 订单类型 + public enum OrderType { + + /// 套餐订单 + PACKAGE, + + /// 直接产品订单 + DIRECT_PRODUCT + } + + /// 订单状态 + public enum Status { + + /// 进行中 + ACTIVE, + + /// 全部履约 + FULFILLED, + + /// 已取消 + CANCELLED + } + + /// 付款状态 + public enum PaymentStatus { + + /// 未付款 + UNPAID, + + /// 部分付款 + PARTIALLY_PAID, + + /// 已付清 + PAID + } + + /// 退款状态 + public enum RefundStatus { + + /// 无需退款 + NOT_REQUIRED, + + /// 待退款或待确认无需退款 + PENDING, + + /// 已完成退款 + REFUNDED + } + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 门店内订单号 + private String orderNo; + + /// 订单类型 + private OrderType orderType; + + /// 订单状态 + private Status status; + + /// 付款状态 + private PaymentStatus paymentStatus; + + /// 退款状态 + private RefundStatus refundStatus; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 首次全额实收时间 + private Long paidAt; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 来源事件记录 ID + private String sourceEventRecordId; + + /// 创建订单幂等号 + private String requestNo; + + /// 下单时间 + private Long purchasedAt; + + /// 下单操作人 + private String operatorUserId; + + /// 取消时间 + private Long cancelledAt; + + /// 取消操作人 + private String cancelOperatorUserId; + + /// 取消原因 + private String cancelReason; + + /// 备注 + private String remark; + + /// 客户 + @Transient + private GaoCustomer customer; + + /// 下单操作人 + @Transient + private User operator; + + /// 取消操作人 + @Transient + private User cancelOperator; + + /// 销售订单明细 + @Transient + private List itemList; + + /// 资金流水 + @Transient + private List fundRecordList; + + /// 来源事件记录 + @Transient + private GaoEventRecord sourceEventRecord; + + /// 未收金额,单位分 + @Transient + private Long unpaidAmount; + + /// 订单附件 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java new file mode 100644 index 0000000..db2166d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java @@ -0,0 +1,103 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 销售订单通用可履约明细 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoSalesOrderItem extends UUIDEntity { + + /// 订单项类型 + public enum ItemType { + + /// 直接产品 + DIRECT_PRODUCT, + + /// 套餐权益 + PACKAGE_ENTITLEMENT + } + + /// 销售订单 ID + private String salesOrderId; + + /// 订单项类型 + private ItemType itemType; + + /// SKU ID + private String skuId; + + /// SKU 单位 ID + private String skuUnitId; + + /// 产品图片附件 + @Transient + private List attachmentList; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + + /// SKU 规格结构快照,数组元素包含 specId、specName、specValueId、specValueName + private JsonNode skuSpecSnapshot; + + /// 单位快照 + private String unitSnapshot; + + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; + + /// 购买数量 + private BigDecimal quantity; + + /// 待履约数量 + private BigDecimal reservedQuantity; + + /// 已履约数量 + private BigDecimal fulfilledQuantity; + + /// 实际单价,单位分 + private Long unitPrice; + + /// 行金额,单位分 + private Long lineAmount; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; + + /// 待核销数量 + @Transient + private BigDecimal pendingQuantity; + + /// 当前可履约数量 + @Transient + private BigDecimal availableQuantity; + + /// 权益剩余数量 + @Transient + private BigDecimal remainingQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerConsumptionRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerConsumptionRecordMapper.java new file mode 100644 index 0000000..a9c7261 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerConsumptionRecordMapper.java @@ -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 selectBySource(@Param("sourceType") GaoCustomerConsumptionRecord.SourceType sourceType, @Param("sourceId") String sourceId); + + List 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); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java deleted file mode 100644 index 79f9ba4..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.imyeyu.api.modules.gao.mapper; - -import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; -import com.imyeyu.spring.mapper.BaseMapper; -import org.apache.ibatis.annotations.Param; - -import java.util.Collection; -import java.util.List; - -/// GAO 客户套餐产品权益 Mapper -/// -/// @author Codex -/// @since 2026-08-31 -public interface GaoCustomerPackageItemMapper extends BaseMapper { - - /// 按客户套餐查询产品权益 - List selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId); - - /// 批量查询客户套餐产品权益 - List selectByCustomerPackageIdList(@Param("customerPackageIdList") Collection customerPackageIdList); - - /// 锁定客户套餐产品 - GaoCustomerPackageItem selectForUpdate(@Param("id") String id); - - /// 调整待核销占用数量 - int updateReservedQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta); - - /// 调整已核销发放数量 - int updateDeliveredQuantity(@Param("id") String id, @Param("delta") java.math.BigDecimal delta); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java index 39ac742..1e8155b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageMapper.java @@ -11,17 +11,8 @@ import org.apache.ibatis.annotations.Param; /// @since 2026-08-31 public interface GaoCustomerPackageMapper extends BaseMapper { - /// 按门店和请求号查询购买结果 - GaoCustomerPackage selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); - - /// 按请求号查询购买结果,用于配置删除后的幂等重试 - GaoCustomerPackage selectByRequestNoAnyStore(@Param("requestNo") String requestNo); - - /// 锁定客户套餐 - GaoCustomerPackage selectForUpdate(@Param("id") String id); - - /// 按购买事件登记查询客户套餐 - GaoCustomerPackage selectByPurchaseEventRecordId(@Param("purchaseEventRecordId") String purchaseEventRecordId); + /// 按销售订单查询客户套餐 + GaoCustomerPackage selectBySalesOrderId(@Param("salesOrderId") String salesOrderId); /// 按条件统计客户套餐 long countByQuery(GaoCustomerPackagePage page); @@ -29,12 +20,4 @@ public interface GaoCustomerPackageMapper extends BaseMapper selectByQuery(GaoCustomerPackagePage page); - /// 原子调整待核销收款 - int updatePendingPaidAmountDelta(@Param("id") String id, @Param("delta") Long delta); - - /// 原子调整有效收款 - int updatePaidAmountDelta(@Param("id") String id, @Param("delta") Long delta); - - /// 带状态条件更新套餐状态 - int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoCustomerPackage.Status fromStatus, @Param("toStatus") GaoCustomerPackage.Status toStatus, @Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId, @Param("cancelReason") String cancelReason, @Param("refundStatus") GaoCustomerPackage.RefundStatus refundStatus); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java index 6b473da..a71e9af 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java @@ -29,6 +29,10 @@ public interface GaoEventRecordMapper extends BaseMapper @Select("SELECT COUNT(*) FROM `gao_event_record` WHERE `event_id` = #{eventId} AND `deleted_at` IS NULL") long countByEventId(String eventId); + /// 查询事件记录是否被订单引用 + @Select("SELECT COUNT(*) FROM `gao_sales_order` WHERE `source_event_record_id` = #{recordId}") + long countBySourceEventRecordId(@Param("recordId") String recordId); + Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt); /// 查询事件中已携带数值的登记记录数量 diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java index 6777a3e..ce14ce9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java @@ -26,7 +26,7 @@ public class GaoEventSQLProvider extends SQLProvider { private String appendWithoutDeleted(String sql) { return sql.replace( "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()) ); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java index 4f130b0..31d1289 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java @@ -19,6 +19,9 @@ public interface GaoFundRecordMapper extends BaseMapper { /// 锁定资金记录 GaoFundRecord selectForUpdate(@Param("id") String id); + /// 查询资金记录及关联销售订单资金摘要 + GaoFundRecord selectWithSalesOrderSummary(@Param("id") String id); + /// 查询资金冲正记录 GaoFundRecord selectRevokeBySourceRecordId(@Param("sourceRecordId") String sourceRecordId); diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java new file mode 100644 index 0000000..513d47b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillmentItem; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 统一履约明细 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderFulfillmentItemMapper extends BaseMapper { + + /// 按履约单查询明细 + List selectByFulfillmentId(@Param("fulfillmentId") String fulfillmentId); + + /// 批量查询履约明细 + List selectByFulfillmentIdList(@Param("fulfillmentIdList") List fulfillmentIdList); + + /// 锁定履约明细 + List selectByFulfillmentIdForUpdate(@Param("fulfillmentId") String fulfillmentId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentMapper.java new file mode 100644 index 0000000..55fc95c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentMapper.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 统一履约 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderFulfillmentMapper extends BaseMapper { + + /// 按门店和幂等号查询履约单 + GaoOrderFulfillment selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 按 ID 查询履约单 + GaoOrderFulfillment selectById(@Param("id") String id); + + /// 锁定履约单 + GaoOrderFulfillment selectForUpdate(@Param("id") String id); + + /// 按订单查询履约单 + List selectBySalesOrderId(@Param("salesOrderId") String salesOrderId); + + /// 按冲正来源查询履约单 + GaoOrderFulfillment selectBySourceFulfillmentId(@Param("sourceFulfillmentId") String sourceFulfillmentId); + + /// 统计订单中仍占用权益的履约单 + long countPendingBySalesOrderId(@Param("salesOrderId") String salesOrderId); + + /// 按条件统计履约单 + long countByQuery(GaoOrderFulfillmentPage page); + + /// 按条件查询履约单 + List selectByQuery(GaoOrderFulfillmentPage page); + + /// 更新履约状态 + int updateStatus(@Param("id") String id, @Param("status") GaoOrderFulfillment.Status status, + @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, + @Param("reason") String reason); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderReminderMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderReminderMapper.java new file mode 100644 index 0000000..4a05dd2 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderReminderMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.vo.GaoOrderReminderCustomer; + +import java.util.List; + +/// GAO 订单提醒 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoOrderReminderMapper { + + /// 查询需要收款的订单客户 + /// + /// @return 订单客户列表 + List selectOrderReceiveCustomers(); + + /// 查询待核销收款客户 + /// + /// @return 收款客户列表 + List selectReceiveVerifyCustomers(); + + /// 查询待核销履约客户 + /// + /// @return 履约客户列表 + List selectFulfillmentVerifyCustomers(); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java deleted file mode 100644 index ebee2fb..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.imyeyu.api.modules.gao.mapper; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; -import com.imyeyu.spring.mapper.BaseMapper; -import org.apache.ibatis.annotations.Param; - -import java.util.Collection; -import java.util.List; - -/// GAO 套餐发放明细 Mapper -/// -/// @author Codex -/// @since 2026-08-31 -public interface GaoPackageDeliveryItemMapper extends BaseMapper { - - /// 按发放单查询明细 - List selectByDeliveryId(@Param("deliveryId") String deliveryId); - - /// 批量查询发放明细 - List selectByDeliveryIdList(@Param("deliveryIdList") Collection deliveryIdList); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java deleted file mode 100644 index 7ee4923..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.imyeyu.api.modules.gao.mapper; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; -import com.imyeyu.spring.mapper.BaseMapper; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/// GAO 套餐发放 Mapper -/// -/// @author Codex -/// @since 2026-08-31 -public interface GaoPackageDeliveryMapper extends BaseMapper { - - /// 按门店和请求号查询发放结果 - GaoPackageDelivery selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); - - /// 锁定发放单 - GaoPackageDelivery selectForUpdate(@Param("id") String id); - - /// 查询原发放单的冲正记录 - GaoPackageDelivery selectRevokeBySourceDeliveryId(@Param("sourceDeliveryId") String sourceDeliveryId); - - /// 查询客户套餐待核销发放数量 - long countPendingByCustomerPackageId(@Param("customerPackageId") String customerPackageId); - - /// 查询客户套餐已核销正常发放数量 - long countVerifiedByCustomerPackageId(@Param("customerPackageId") String customerPackageId); - - /// 查询客户套餐发放历史 - java.util.List selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId); - - /// 按条件统计发放单 - long countByQuery(GaoPackageDeliveryPage page); - - /// 按条件查询发放单 - List selectByQuery(GaoPackageDeliveryPage page); - - /// 带状态条件更新发放单 - int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoPackageDelivery.Status fromStatus, @Param("toStatus") GaoPackageDelivery.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerConsumptionRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerConsumptionRecordMapper.java new file mode 100644 index 0000000..9e486ed --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerConsumptionRecordMapper.java @@ -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 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); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java index 197349d..4a8e7c8 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java @@ -19,11 +19,18 @@ public interface GaoPointLedgerMapper extends BaseMapper /// @return 流水 GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey); - /// 查询登记记录产生的原始规则积分流水 + /// 查询触发来源产生的原始规则积分流水 /// - /// @param eventRecordId 登记记录 ID + /// @param triggerSourceType 直接触发来源类型 + /// @param triggerSourceId 直接触发来源 ID /// @return 原始规则积分流水 - List selectRuleChangeByEventRecordId(@Param("eventRecordId") String eventRecordId); + List selectRuleChangeByTriggerSource(@Param("triggerSourceType") GaoPointLedger.TriggerSourceType triggerSourceType, @Param("triggerSourceId") String triggerSourceId); + + /// 查询依赖指定消费事实的原始规则积分流水 + /// + /// @param consumptionRecordId 消费事实 ID + /// @return 原始规则积分流水 + List selectRuleChangeByConsumptionRecordId(@Param("consumptionRecordId") String consumptionRecordId); /// 批量查询幂等键对应的流水 /// diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java index 97a1fe1..af230c4 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java @@ -18,8 +18,9 @@ public interface GaoPointRuleMapper extends BaseMapper { /// 查询当前时间可匹配的启用规则 /// /// @param storeId 门店 ID - /// @param eventId 登记事件 ID + /// @param triggerSourceType 直接触发来源类型 + /// @param triggerRefId 触发对象筛选 ID,空为不限 /// @param now 当前时间戳 /// @return 按优先级排序的规则列表 - List selectActiveByStoreIdAndEventId(@Param("storeId") String storeId, @Param("eventId") String eventId, @Param("now") long now); + List selectActiveByTrigger(@Param("storeId") String storeId, @Param("triggerSourceType") GaoPointRule.TriggerSourceType triggerSourceType, @Param("triggerRefId") String triggerRefId, @Param("now") long now); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductCategoryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductCategoryMapper.java new file mode 100644 index 0000000..8b83fdc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductCategoryMapper.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品分类 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductCategoryMapper extends BaseMapper { + + /// 查询分类 + GaoProductCategory selectById(@Param("id") String id); + + /// 按 ID 批量查询分类 + List selectByIdList(@Param("idList") Collection idList); + + /// 一次性更新分类排序 + void updateSort(@Param("idList") List idList, @Param("updatedAt") long updatedAt); + + /// 按条件统计产品分类 + long countByQuery(GaoProductCategoryPage page); + + /// 按条件查询产品分类 + List selectByQuery(GaoProductCategoryPage page); + + /// 查询门店分类 + List selectByStoreId(@Param("storeId") String storeId); + + /// 查询门店内同名分类 + GaoProductCategory selectByStoreIdAndName(@Param("storeId") String storeId, @Param("name") String name, @Param("excludeId") String excludeId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductChartMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductChartMapper.java new file mode 100644 index 0000000..f5e0924 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductChartMapper.java @@ -0,0 +1,48 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; + +import java.util.List; + +/// GAO 产品图表 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductChartMapper { + + /// 查询产品每日销售统计 + List selectSalesDaily(GaoProductChartReq req); + + /// 查询产品销售排行 + List selectSalesRank(GaoProductChartReq req); + + /// 查询产品每日履约统计 + List selectFulfillmentDaily(GaoProductChartReq req); + + /// 查询产品履约进度排行 + List selectFulfillmentRank(GaoProductChartReq req); + + /// 查询产品每日库存变动统计 + List selectStockMovementDaily(GaoProductChartReq req); + + /// 查询产品库存健康度 + List selectStockHealth(GaoProductChartReq req); + + /// 查询产品类型统计 + List selectType(GaoProductChartReq req); + + /// 查询产品分类统计 + List selectCategory(GaoProductChartReq req); + + /// 查询产品销售单位排行 + List selectUnitSalesRank(GaoProductChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java new file mode 100644 index 0000000..5841a47 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductMapper extends BaseMapper { + + /// 按条件统计产品 + long countByQuery(GaoProductPage page); + + /// 按条件查询产品 + List selectByQuery(GaoProductPage page); + + /// 批量查询产品 + List selectByIdList(@Param("idList") Collection idList); + + /// 查询产品编码 + GaoProduct selectByStoreIdAndCode(@Param("storeId") String storeId, @Param("code") String code, @Param("excludeId") String excludeId); + + /// 锁定产品 + GaoProduct selectForUpdate(@Param("id") String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuChartMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuChartMapper.java new file mode 100644 index 0000000..171750a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuChartMapper.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; + +import java.util.List; + +/// GAO SKU 图表 Mapper +/// +/// @author Codex +/// @since 2026-09-07 +public interface GaoProductSkuChartMapper { + + /// 查询 SKU 每日销售统计 + List selectSalesDaily(GaoProductChartReq req); + + /// 查询 SKU 销售排行 + List selectSalesRank(GaoProductChartReq req); + + /// 查询 SKU 单位销售排行 + List selectUnitSalesRank(GaoProductChartReq req); + + /// 查询 SKU 每日履约统计 + List selectFulfillmentDaily(GaoProductChartReq req); + + /// 查询 SKU 履约进度排行 + List selectFulfillmentRank(GaoProductChartReq req); + + /// 查询 SKU 每日库存变动统计 + List selectStockMovementDaily(GaoProductChartReq req); + + /// 查询 SKU 库存健康度 + List selectStockHealth(GaoProductChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuMapper.java new file mode 100644 index 0000000..a14e681 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuMapper.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品 SKU Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuMapper extends BaseMapper { + + /// 按产品查询 SKU + List selectByProductId(@Param("productId") String productId); + + /// 批量按产品查询 SKU + List selectByProductIdList(@Param("productIdList") Collection productIdList); + + /// 查询 SKU 编码 + GaoProductSku selectByStoreIdAndCode(@Param("storeId") String storeId, @Param("code") String code, @Param("excludeId") String excludeId); + + /// 查询 SKU 条码 + GaoProductSku selectByStoreIdAndBarcode(@Param("storeId") String storeId, @Param("barcode") String barcode, @Param("excludeId") String excludeId); + + /// 查询产品下相同规格值组合的 SKU + GaoProductSku selectByProductIdAndSpecValueKey(@Param("productId") String productId, @Param("specValueKey") String specValueKey, @Param("excludeId") String excludeId); + + /// 锁定 SKU + GaoProductSku selectForUpdate(@Param("id") String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuSpecValueMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuSpecValueMapper.java new file mode 100644 index 0000000..ed78800 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuSpecValueMapper.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSkuSpecValue; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO SKU 规格值关联 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuSpecValueMapper extends BaseMapper { + + /// 批量查询 SKU 规格值关联 + List selectBySkuIdList(@Param("skuIdList") Collection skuIdList); + + /// 删除 SKU 的规格值关联 + void deleteBySkuId(@Param("skuId") String skuId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuUnitMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuUnitMapper.java new file mode 100644 index 0000000..4163550 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuUnitMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品 SKU 单位 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuUnitMapper extends BaseMapper { + + /// 按 SKU 查询有效单位 + List selectBySkuId(@Param("skuId") String skuId); + + /// 批量查询 SKU 有效单位 + List selectBySkuIdList(@Param("skuIdList") Collection skuIdList); + + /// 锁定单位 + GaoProductSkuUnit selectForUpdate(@Param("id") String id); + + /// 更新有效单位 + int updateUnit(GaoProductSkuUnit unit); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecMapper.java new file mode 100644 index 0000000..bc5eef0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品规格项 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSpecMapper extends BaseMapper { + + /// 按产品查询规格项 + List selectByProductId(@Param("productId") String productId); + + /// 批量按产品查询规格项 + List selectByProductIdList(@Param("productIdList") Collection productIdList); + + /// 查询规格值所属产品 + String selectProductIdBySpecValueId(@Param("specValueId") String specValueId); + + /// 查询规格项所属产品 + String selectProductIdBySpecId(@Param("specId") String specId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecValueMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecValueMapper.java new file mode 100644 index 0000000..0caba09 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecValueMapper.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品规格值 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSpecValueMapper extends BaseMapper { + + /// 按 ID 查询有效规格值 + GaoProductSpecValue selectById(@Param("id") String id); + + /// 按规格项查询规格值 + List selectBySpecIdList(@Param("specIdList") Collection specIdList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java new file mode 100644 index 0000000..f9f8f7f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java @@ -0,0 +1,46 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 产品库存 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductStockMapper extends BaseMapper { + + /// 锁定库存余额 + GaoProductStock selectForUpdate(@Param("storeId") String storeId, @Param("skuId") String skuId); + + /// 按条件统计库存余额 + long countByQuery(GaoProductStockPage page); + + /// 按条件查询库存余额 + List selectByQuery(GaoProductStockPage page); + + /// 按产品汇总库存数量 + long countProductByQuery(GaoProductStockPage page); + + /// 按产品分页查询库存汇总 + List selectProductByQuery(GaoProductStockPage page); + + /// 按门店和产品查询库存余额 + GaoProductStock selectByStoreIdAndSkuId(@Param("storeId") String storeId, @Param("skuId") String skuId); + + /// 条件更新库存余额 + int updateQuantity(@Param("id") String id, @Param("onHandDelta") java.math.BigDecimal onHandDelta, @Param("reservedDelta") java.math.BigDecimal reservedDelta, @Param("updatedBy") String updatedBy); + + /// 条件增加预占库存 + int reserve(@Param("id") String id, @Param("quantity") java.math.BigDecimal quantity, @Param("updatedBy") String updatedBy); + + /// 条件释放预占库存 + int release(@Param("id") String id, @Param("quantity") java.math.BigDecimal quantity, @Param("updatedBy") String updatedBy); + + /// 插入库存余额,已存在时忽略 + int insertIgnore(GaoProductStock stock); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java new file mode 100644 index 0000000..bd5ea37 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 库存流水 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductStockRecordMapper extends BaseMapper { + + /// 按门店和请求号查询库存流水 + List selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 锁定库存流水 + GaoProductStockRecord selectForUpdate(@Param("id") String id); + + /// 查询库存冲正流水 + GaoProductStockRecord selectRevokeBySourceRecordId(@Param("sourceRecordId") String sourceRecordId); + + /// 按业务查询库存流水 + List selectByBiz(@Param("bizType") GaoProductStockRecord.BizType bizType, @Param("bizId") String bizId); + + /// 查询指定业务和产品的库存流水 + GaoProductStockRecord selectByBizSku(@Param("bizType") GaoProductStockRecord.BizType bizType, @Param("bizId") String bizId, @Param("skuId") String skuId, @Param("operationType") GaoProductStockRecord.OperationType operationType); + + /// 按条件统计库存流水 + long countByQuery(GaoProductStockRecordPage page); + + /// 按条件查询库存流水 + List selectByQuery(GaoProductStockRecordPage page); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java new file mode 100644 index 0000000..43b831c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java @@ -0,0 +1,60 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderSkuRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; + +import java.util.List; + +/// GAO 销售订单图表 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoSalesOrderChartMapper { + + /// 查询销售订单每日统计 + List selectDaily(GaoSalesOrderChartReq req); + + /// 查询销售订单类型统计 + List selectType(GaoSalesOrderChartReq req); + + /// 查询销售订单付款状态统计 + List selectPayment(GaoSalesOrderChartReq req); + + /// 查询客户订单排行 + List selectCustomerRank(GaoSalesOrderChartReq req); + + /// 查询客户订单产品排行 + List selectCustomerProductRank(GaoSalesOrderChartReq req); + + /// 查询客户订单 SKU 排行 + List selectCustomerSkuRank(GaoSalesOrderChartReq req); + + /// 查询销售订单状态统计 + List selectStatus(GaoSalesOrderChartReq req); + + /// 查询销售订单应收统计 + GaoSalesOrderReceivableStatView selectReceivable(GaoSalesOrderChartReq req); + + /// 查询客户每日有效消费统计 + List selectConsumptionDaily(GaoSalesOrderChartReq req); + + /// 查询客户订单日历统计 + List selectCalendar(GaoSalesOrderChartReq req); + + /// 查询客户每日复购统计 + List selectRepeatDaily(GaoSalesOrderChartReq req); + + /// 查询客户复购间隔统计 + List selectRepeatInterval(GaoSalesOrderChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java new file mode 100644 index 0000000..a51eb31 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java @@ -0,0 +1,30 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 销售订单明细 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoSalesOrderItemMapper extends BaseMapper { + + /// 查询订单明细 + List selectBySalesOrderId(@Param("salesOrderId") String salesOrderId); + + /// 批量查询订单明细 + List selectBySalesOrderIdList(@Param("salesOrderIdList") List salesOrderIdList); + + /// 锁定订单明细 + GaoSalesOrderItem selectForUpdate(@Param("id") String id); + + /// 原子调整待履约数量 + int updateReservedQuantity(@Param("id") String id, @Param("delta") BigDecimal delta); + + /// 原子调整已履约数量 + int updateFulfilledQuantity(@Param("id") String id, @Param("delta") BigDecimal delta); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java new file mode 100644 index 0000000..f5f14f0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java @@ -0,0 +1,47 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Collection; + +/// GAO 销售订单 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoSalesOrderMapper extends BaseMapper { + + /// 按门店和幂等号查询订单 + GaoSalesOrder selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 锁定订单 + GaoSalesOrder selectForUpdate(@Param("id") String id); + + /// 批量查询订单 + List selectByIdList(@Param("idList") Collection idList); + + /// 按条件统计订单 + long countByQuery(GaoSalesOrderPage page); + + /// 按条件查询订单 + List selectByQuery(GaoSalesOrderPage page); + + /// 更新订单付款汇总 + int updatePayment(@Param("id") String id, @Param("pendingPaidAmount") Long pendingPaidAmount, @Param("paidAmount") Long paidAmount, + @Param("paidAt") Long paidAt, @Param("refundedAmount") Long refundedAmount, @Param("paymentStatus") GaoSalesOrder.PaymentStatus paymentStatus, + @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus); + + /// 带状态条件取消订单 + int cancelIf(@Param("id") String id, @Param("fromStatus") GaoSalesOrder.Status fromStatus, @Param("toStatus") GaoSalesOrder.Status toStatus, + @Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId, + @Param("cancelReason") String cancelReason, @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus); + + /// 更新订单履约状态 + int updateStatus(@Param("id") String id, @Param("status") GaoSalesOrder.Status status); + + /// 更新订单备注 + int updateRemark(@Param("id") String id, @Param("remark") String remark); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java index 9ad780e..81f9bf9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java @@ -15,6 +15,12 @@ import java.util.List; /// @since 2026-07-30 public interface GaoUserMapper extends BaseMapper { + /// 查询门店启用中的店长 + /// + /// @param storeId 门店 ID + /// @return 店长列表 + List selectEnabledStoreManagers(@Param("storeId") String storeId); + List selectByUserIdList(Collection userIdList); List selectRecipientPage(@Param("page") Page page); diff --git a/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointDecision.java b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointDecision.java new file mode 100644 index 0000000..0465bfc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointDecision.java @@ -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 conditionSnapshot, List consumptionRecordIdList) { +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java new file mode 100644 index 0000000..944b7bd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java @@ -0,0 +1,132 @@ +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 lombok.Getter; + +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; + + @Getter + private final String storeId; + @Getter + private final String customerId; + @Getter + private final String triggerSourceType; + @Getter + private final String triggerRefType; + @Getter + private final String triggerRefId; + @Getter + private final long occurredAt; + + /// 参与过金额计算的消费事实,ID 到金额摘要 + private final Map 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; + } + + /// 查询触发时间往前滚动一年的有效消费金额,窗口按 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 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 conditionSnapshot, List consumptionRecordIdList) { + TimiException.requiredTrue(pointDelta != 0, "脚本积分变化不能为 0"); + Map snapshot = conditionSnapshot == null ? new LinkedHashMap<>() : new LinkedHashMap<>(conditionSnapshot); + List> consumptionSummary = new ArrayList<>(); + for (Map.Entry entry : consumptionFactMap.entrySet()) { + Map 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 deps = new ArrayList<>(consumptionFactMap.keySet()); + for (String id : consumptionRecordIdList == null ? List.of() : consumptionRecordIdList) { + if (id != null && !deps.contains(id)) { + deps.add(id); + } + } + return new GaoPointDecision(pointDelta, reason, snapshot, deps); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerConsumptionRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerConsumptionRecordService.java new file mode 100644 index 0000000..8ec9e2e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerConsumptionRecordService.java @@ -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 { + + /// 写入销售订单消费事实 + /// + /// 幂等,重复执行返回已有事实 + /// + /// @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 listActiveByWindow(String storeId, String customerId, long beginAt, long endAt); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java index 55ba3b2..7c0a800 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java @@ -1,17 +1,12 @@ package com.imyeyu.api.modules.gao.service; import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; -import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; -import java.math.BigDecimal; -import java.util.Collection; -import java.util.List; -import java.util.Map; /// GAO 客户套餐服务 /// @@ -22,66 +17,12 @@ public interface GaoCustomerPackageService extends BaseService pageByQuery(GaoCustomerPackagePage page); - /// 创建客户套餐及购买事件登记 + /// 创建客户套餐及套餐订单 GaoCustomerPackage purchase(GaoCustomerPackagePurchaseRequest request); /// 取消客户套餐 GaoCustomerPackage cancel(GaoCustomerPackageCancelRequest request); - /// 锁定客户套餐 - /// - /// @param id 客户套餐 ID - /// @return 已锁定的客户套餐,不存在时为空 - GaoCustomerPackage getForUpdate(String id); - - /// 查询客户套餐产品权益 - /// - /// @param customerPackageId 客户套餐 ID - /// @return 产品权益列表 - List listItemByCustomerPackageId(String customerPackageId); - - /// 批量查询客户套餐产品权益 - /// - /// @param customerPackageIdList 客户套餐 ID 列表 - /// @return 按客户套餐 ID 分组的产品权益 - Map> mapItemListByCustomerPackageIdList(Collection customerPackageIdList); - - /// 锁定客户套餐产品权益 - /// - /// @param itemId 客户套餐产品权益 ID - /// @return 已锁定的产品权益,不存在时为空 - GaoCustomerPackageItem getItemForUpdate(String itemId); - - /// 原子调整待核销发放占用数量 - /// - /// @param itemId 客户套餐产品权益 ID - /// @param delta 调整量 - /// @return 受影响行数 - int updateItemReservedQuantity(String itemId, BigDecimal delta); - - /// 原子调整已核销发放数量 - /// - /// @param itemId 客户套餐产品权益 ID - /// @param delta 调整量 - /// @return 受影响行数 - int updateItemDeliveredQuantity(String itemId, BigDecimal delta); - - /// 原子调整待核销收款金额 - /// - /// @param id 客户套餐 ID - /// @param delta 调整量 - /// @return 受影响行数 - int updatePendingPaidAmountDelta(String id, Long delta); - - /// 原子调整有效收款金额 - /// - /// @param id 客户套餐 ID - /// @param delta 调整量 - /// @return 受影响行数 - int updatePaidAmountDelta(String id, Long delta); - - /// 保存客户套餐聚合状态 - /// - /// @param packageEntity 客户套餐聚合 - void updateAggregateState(GaoCustomerPackage packageEntity); + /// 按销售订单查询客户套餐 + GaoCustomerPackage getBySalesOrderId(String salesOrderId); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java index eeeed06..a798883 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java @@ -2,10 +2,15 @@ package com.imyeyu.api.modules.gao.service; import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordNumberValueDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq; import com.imyeyu.java.TimiJava; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; @@ -25,6 +30,58 @@ public interface GaoEventRecordService extends BaseService listByCustomerId(String customerId); + /// 查询指定事件的登记数量 + /// + /// @param eventId 事件 ID + /// @return 登记数量 + long countByEventId(String eventId); + + /// 查询指定事件中已携带数值的登记数量 + /// + /// @param eventId 事件 ID + /// @return 登记数量 + long countByEventIdAndNumberValue(String eventId); + + /// 按门店和登记日期查询登记数量 + /// + /// @param storeId 门店 ID + /// @param registeredDateValue 登记日期值 + /// @return 登记数量 + long countByStoreIdAndRegisteredDateValue(String storeId, Integer registeredDateValue); + + /// 同步登记记录营业日序号快照 + /// + /// @param storeId 门店 ID + /// @param registeredDateValue 登记日期值 + /// @param businessDayNo 营业日序号 + void updateBusinessDayNo(String storeId, Integer registeredDateValue, long businessDayNo); + + /// 按条件统计登记记录数量 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param eventId 事件 ID + /// @param beginAt 起始时间,包含 + /// @param endAt 结束时间,不包含 + /// @return 登记数量 + long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt); + + /// 汇总客户指定事件登记数值 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param eventId 事件 ID + /// @param beginAt 起始时间,包含 + /// @param endAt 结束时间,不包含 + /// @return 数值总和 + long sumNumberValue(String storeId, String customerId, String eventId, Long beginAt, Long endAt); + + /// 回写登记记录获得的积分快照 + /// + /// @param id 登记记录 ID + /// @param earnedPointValue 获得积分 + void updateEarnedPointValue(String id, long earnedPointValue); + /// 为登记记录附加已选择的选项组 /// /// @param recordList 登记记录列表 @@ -47,11 +104,10 @@ public interface GaoEventRecordService extends BaseService pageByRange(GaoEventRecordPage req); + /// 按时间范围统计门店登记数 + /// + /// @param req 门店报表查询请求 + /// @return 每日登记数 + List stateDailyByRange(GaoStoreChartReq req); + + /// 按时间范围统计门店登记数值 + /// + /// @param req 门店报表查询请求 + /// @return 每日登记数值 + List stateNumberValueDailyByRange(GaoStoreChartReq req); + + /// 按时间范围查询门店登记日历 + /// + /// @param req 门店报表查询请求 + /// @return 登记日历 + List calendarByStoreRange(GaoStoreChartReq req); + + /// 按客户和时间范围统计登记数 + /// + /// @param req 客户报表查询请求 + /// @return 每日登记数 + List stateCustomerDailyByRange(GaoCustomerChartReq req); + + /// 按客户和时间范围查询登记日历 + /// + /// @param req 客户报表查询请求 + /// @return 登记日历 + List calendarByCustomerRange(GaoCustomerChartReq req); + /// 按登记记录列表条件汇总数值 /// /// @param req 登记记录范围查询请求 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java index cef97d2..0fce4b1 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java @@ -25,6 +25,12 @@ public interface GaoEventService extends BaseService { Map mapByIdList(Collection idList); + /// 查询绑定指定套餐配置的事件数量 + /// + /// @param packageConfigId 套餐配置 ID + /// @return 事件数量 + long countByPackageConfigId(String packageConfigId); + Map> mapRoleCodeListByEventIdList(Collection eventIdList); /// 查询启用的登记事件 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java index b90fb98..7263186 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java @@ -1,18 +1,13 @@ package com.imyeyu.api.modules.gao.service; import com.imyeyu.api.modules.gao.entity.GaoFundRecord; -import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest; import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; -import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; import java.util.List; -/// GAO 通用资金流水服务 +/// GAO 销售订单资金流水服务 /// /// @author Codex /// @since 2026-08-31 @@ -21,33 +16,47 @@ public interface GaoFundRecordService extends BaseService /// 按条件查询资金流水 PageResult pageByQuery(GaoFundRecordPage page); - /// 查询客户套餐资金历史 - List listByCustomerPackageId(String customerPackageId); + /// 查询资金流水详情 + GaoFundRecord detail(String id); - /// 登记线下收款 - GaoFundRecord receive(GaoFundReceiveRequest request); + /// 按销售订单查询资金流水 + List listByBizId(String salesOrderId); - /// 核销收款 - GaoFundRecord verify(String fundRecordId); - - /// 拒绝待核销收款 - GaoFundRecord reject(GaoFundActionRequest request); - - /// 撤销自己提交的待核销收款 - GaoFundRecord cancel(GaoFundActionRequest request); - - /// 登记退款 - GaoFundRecord refund(GaoFundRefundRequest request); - - /// 确认无需退款 - GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request); - - /// 冲正资金流水 - GaoFundRecord revoke(GaoFundRevokeRequest request); - - /// 统计客户套餐待核销收款数量 + /// 由销售订单资金服务创建资金流水 /// - /// @param bizId 业务 ID - /// @return 待核销收款数量 - long countPendingReceiveByBizId(String bizId); + /// @param record 资金记录 + void createByOrderPayment(GaoFundRecord record); + + /// 查询销售订单待核销收款数量 + long countPendingReceiveByBizId(String salesOrderId); + + /// 按门店和请求号查询资金记录 + /// + /// @param storeId 门店 ID + /// @param requestNo 请求号 + /// @return 资金记录 + GaoFundRecord getByRequestNo(String storeId, String requestNo); + + /// 锁定资金记录 + /// + /// @param id 资金记录 ID + /// @return 资金记录 + GaoFundRecord getForUpdate(String id); + + /// 查询资金冲正记录 + /// + /// @param sourceRecordId 原资金记录 ID + /// @return 冲正记录 + GaoFundRecord getRevokeBySourceRecordId(String sourceRecordId); + + /// 按状态条件更新资金记录 + /// + /// @param id 资金记录 ID + /// @param fromStatus 原状态 + /// @param toStatus 目标状态 + /// @param verifierUserId 核销用户 ID + /// @param verifiedAt 核销时间 + /// @param reason 原因 + /// @return 更新行数 + int updateStatusIf(String id, GaoFundRecord.Status fromStatus, GaoFundRecord.Status toStatus, String verifierUserId, Long verifiedAt, String reason); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderFulfillmentService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderFulfillmentService.java new file mode 100644 index 0000000..d80f6a7 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderFulfillmentService.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 统一履约服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderFulfillmentService extends BaseService { + + /// 按条件分页查询履约单 + PageResult pageByQuery(GaoOrderFulfillmentPage page); + + /// 查询履约单详情 + GaoOrderFulfillment detail(String id); + + /// 按销售订单查询履约历史 + List listBySalesOrderId(String salesOrderId); + + /// 查询销售订单仍占用权益的履约单数量 + long countPendingBySalesOrderId(String salesOrderId); + + /// 创建履约单 + GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request); + + /// 为库存不足履约单准备库存 + GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request); + + /// 核销履约单 + GaoOrderFulfillment verify(GaoOrderFulfillmentActionRequest request); + + /// 拒绝履约单 + GaoOrderFulfillment reject(GaoOrderFulfillmentActionRequest request); + + /// 撤销待核销履约单 + GaoOrderFulfillment cancel(GaoOrderFulfillmentActionRequest request); + + /// 冲正已核销履约单 + GaoOrderFulfillment revoke(GaoOrderFulfillmentRevokeRequest request); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderPaymentService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderPaymentService.java new file mode 100644 index 0000000..71f51ec --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderPaymentService.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; + +/// GAO 销售订单资金服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderPaymentService { + + /// 登记收款 + GaoFundRecord receive(GaoOrderPaymentReceiveRequest request); + + /// 核销收款 + GaoFundRecord verify(String fundRecordId); + + /// 拒绝收款 + GaoFundRecord reject(GaoOrderPaymentActionRequest request); + + /// 撤销自己提交的收款 + GaoFundRecord cancel(GaoOrderPaymentActionRequest request); + + /// 登记退款 + GaoFundRecord refund(GaoOrderPaymentRefundRequest request); + + /// 确认无需退款 + GaoFundRecord confirmNoRefund(GaoOrderPaymentNoRefundRequest request); + + /// 冲正资金流水 + GaoFundRecord revoke(GaoOrderPaymentRevokeRequest request); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderReminderService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderReminderService.java new file mode 100644 index 0000000..ead6e45 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderReminderService.java @@ -0,0 +1,14 @@ +package com.imyeyu.api.modules.gao.service; + +/// GAO 订单提醒服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoOrderReminderService { + + /// 发送订单收款提醒 + void sendOrderReceiveReminder(); + + /// 发送核销提醒 + void sendVerifyReminder(); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java deleted file mode 100644 index 078e2c5..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.imyeyu.api.modules.gao.service; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest; -import com.imyeyu.spring.bean.PageResult; -import com.imyeyu.spring.service.BaseService; - -import java.util.Collection; -import java.util.List; -import java.util.Map; - -/// GAO 套餐发放服务 -/// -/// @author Codex -/// @since 2026-08-31 -public interface GaoPackageDeliveryService extends BaseService { - - /// 按条件查询发放单 - PageResult pageByQuery(GaoPackageDeliveryPage page); - - /// 查询客户套餐发放历史 - List listByCustomerPackageId(String customerPackageId); - - /// 批量查询发放明细 - /// - /// @param deliveryIdList 发放单 ID 列表 - /// @return 按发放单 ID 分组的发放明细 - Map> mapItemListByDeliveryIdList(Collection deliveryIdList); - - /// 创建待核销发放单 - GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request); - - /// 核销发放单 - GaoPackageDelivery verify(String deliveryId); - - /// 拒绝待核销发放单 - GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request); - - /// 撤销自己提交的待核销发放单 - GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request); - - /// 整单冲正已核销发放单 - GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request); - - /// 统计客户套餐待核销发放单数量 - /// - /// @param customerPackageId 客户套餐 ID - /// @return 待核销发放单数量 - long countPendingByCustomerPackageId(String customerPackageId); - - /// 统计客户套餐已核销正常发放单数量 - /// - /// @param customerPackageId 客户套餐 ID - /// @return 已核销正常发放单数量 - long countVerifiedByCustomerPackageId(String customerPackageId); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java index 442d9b9..78708b1 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java @@ -1,35 +1,50 @@ 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.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.GaoPointLedgerPage; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; +import java.util.List; + /// GAO 客户积分流水服务 /// /// @author Codex /// @since 2026-08-11 public interface GaoPointLedgerService extends BaseService { - /// 写入登记规则积分流水 + /// 写入规则发放或扣减积分流水 + /// + /// 幂等,重复触发返回已有流水;积分流水只追加,不修改不删除 /// - /// @param record 登记记录 /// @param rule 命中规则 - /// @param pointDelta 积分变化 - /// @param sourcePayload 来源快照 + /// @param decision 积分决策 + /// @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 积分流水 - 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 consumptionRecordIdList); - /// 写入删除登记记录的撤销流水 + /// 追加撤销流水 + /// + /// 撤销流水以原流水为基准反向冲抵,幂等键按原流水设计 /// /// @param originLedger 原始规则积分流水 - /// @param record 登记记录 + /// @param reason 撤销原因 /// @return 撤销流水 - GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record); + GaoPointLedger revoke(GaoPointLedger originLedger, String reason); /// 写入人工调整流水 /// @@ -38,6 +53,25 @@ public interface GaoPointLedgerService extends BaseService listRuleChangeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId); + + /// 查询依赖指定消费事实的原始规则积分流水 + /// + /// @param consumptionRecordId 消费事实 ID + /// @return 原始积分流水 + List listRuleChangeByConsumptionRecordId(String consumptionRecordId); + + /// 按幂等键批量查询积分流水 + /// + /// @param idempotencyKeyList 幂等键列表 + /// @return 已存在流水 + List listByIdempotencyKeyList(List idempotencyKeyList); + /// 分页查询积分流水 /// /// @param page 查询条件 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java index 9bae0b8..8a5d24d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java @@ -13,11 +13,14 @@ public interface GaoPointRuleService extends BaseService { /// 查询当前时间可匹配的启用规则 /// + /// 触发对象筛选 ID 为空时匹配未设置触发对象引用的规则 + /// /// @param storeId 门店 ID - /// @param eventId 登记事件 ID + /// @param triggerSourceType 直接触发来源类型 + /// @param triggerRefId 触发对象筛选 ID,可空 /// @param now 当前时间戳 - /// @return 规则列表 - List listActive(String storeId, String eventId, long now); + /// @return 按优先级排序的规则列表 + List listActive(String storeId, GaoPointRule.TriggerSourceType triggerSourceType, String triggerRefId, long now); /// 查询规则 ID 列表 /// diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java index b2e47db..0b7fea9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java @@ -1,7 +1,8 @@ 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.GaoPointLedger; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest; /// GAO 客户积分触发服务 @@ -15,11 +16,22 @@ public interface GaoPointTriggerService { /// @param record 登记记录 void onEventRecordCreated(GaoEventRecord record); - /// 撤销登记记录产生的全部获得积分 + /// 撤销登记记录触发的全部获得积分,并撤销迁移消费事实及其依赖积分 /// /// @param record 登记记录 void revokeByEventRecord(GaoEventRecord record); + /// 在订单首次全额实收时写入消费事实并触发订单积分 + /// + /// @param order 销售订单 + void onSalesOrderPaid(GaoSalesOrder order); + + /// 订单取消或全额实收被冲正时撤销消费事实及其依赖积分 + /// + /// @param order 销售订单 + /// @param reason 撤销原因 + void revokeBySalesOrder(GaoSalesOrder order, String reason); + /// 执行人工调整 /// /// @param request 人工调整请求 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductCategoryService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductCategoryService.java new file mode 100644 index 0000000..68c2842 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductCategoryService.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 产品分类服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductCategoryService extends BaseService { + + /// 按条件分页查询分类 + PageResult pageByQuery(GaoProductCategoryPage page); + + /// 查询门店分类 + List listByStore(String storeId); + + /// 批量更新分类排序 + void sort(List idList); + + /// 停用分类 + void disable(String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductChartService.java new file mode 100644 index 0000000..174784c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductChartService.java @@ -0,0 +1,48 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; + +import java.util.List; + +/// GAO 产品图表服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductChartService { + + /// 查询产品每日销售统计 + List salesDaily(GaoProductChartReq req); + + /// 查询产品销售排行 + List salesRank(GaoProductChartReq req); + + /// 查询产品每日履约统计 + List fulfillmentDaily(GaoProductChartReq req); + + /// 查询产品履约进度排行 + List fulfillmentRank(GaoProductChartReq req); + + /// 查询产品每日库存变动统计 + List stockMovementDaily(GaoProductChartReq req); + + /// 查询产品库存健康度 + List stockHealth(GaoProductChartReq req); + + /// 查询产品类型统计 + List type(GaoProductChartReq req); + + /// 查询产品分类统计 + List category(GaoProductChartReq req); + + /// 查询产品销售单位排行 + List unitSalesRank(GaoProductChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java new file mode 100644 index 0000000..f5a8070 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductService extends BaseService { + + /// 按条件分页查询产品 + PageResult pageByQuery(GaoProductPage page); + + /// 查询产品详情单位 + GaoProduct detail(String id); + + /// 批量查询产品详情 + List listByIdList(Collection idList); + + /// 查询启用且允许销售的产品单位 + List listSellable(GaoProductPage page); + + /// 停用产品 + void disable(String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuChartService.java new file mode 100644 index 0000000..a86930a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuChartService.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; + +import java.util.List; + +/// GAO SKU 图表服务 +/// +/// @author Codex +/// @since 2026-09-07 +public interface GaoProductSkuChartService { + + /// 查询 SKU 每日销售统计 + List salesDaily(GaoProductChartReq req); + + /// 查询 SKU 销售排行 + List salesRank(GaoProductChartReq req); + + /// 查询 SKU 单位销售排行 + List unitSalesRank(GaoProductChartReq req); + + /// 查询 SKU 每日履约统计 + List fulfillmentDaily(GaoProductChartReq req); + + /// 查询 SKU 履约进度排行 + List fulfillmentRank(GaoProductChartReq req); + + /// 查询 SKU 每日库存变动统计 + List stockMovementDaily(GaoProductChartReq req); + + /// 查询 SKU 库存健康度 + List stockHealth(GaoProductChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java new file mode 100644 index 0000000..f7d4d21 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java @@ -0,0 +1,50 @@ +package com.imyeyu.api.modules.gao.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; +import com.imyeyu.spring.service.BaseService; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品 SKU 服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuService extends BaseService { + + /// 查询 SKU 详情 + GaoProductSku detail(String id); + + /// 按产品查询 SKU + List listByProductId(String productId); + + /// 批量按产品查询 SKU + List listByProductIdList(Collection productIdList); + + /// 查询并校验可销售 SKU 单位 + GaoProductSkuUnitSelection getSellableSkuUnit(String storeId, String skuId, String skuUnitId); + + /// 查询并校验 SKU 单位 + GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase); + + /// 查询 SKU 单位 + List listUnitBySkuId(String skuId); + + /// 查询 SKU 规格结构快照 + /// + /// @param skuId SKU ID + /// @return SKU 规格结构快照;无规格时返回空数组 + JsonNode getSkuSpecSnapshot(String skuId); + + /// 根据已加载 SKU 查询规格结构快照 + /// + /// @param sku 已加载 SKU + /// @return SKU 规格结构快照;无规格时返回空数组 + JsonNode getSkuSpecSnapshot(GaoProductSku sku); + + /// 停用 SKU + void disable(String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSpecService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSpecService.java new file mode 100644 index 0000000..353ccbc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSpecService.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; + +import java.util.List; + +/// GAO 产品规格服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSpecService { + + /// 查询产品规格 + List listByProductId(String productId); + + /// 查询规格项所属产品 ID + String getProductIdBySpecId(String specId); + + /// 创建规格项 + void createSpec(GaoProductSpec spec); + + /// 创建规格值 + void createValue(GaoProductSpecValue value); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java new file mode 100644 index 0000000..3686785 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java @@ -0,0 +1,41 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 库存流水服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductStockRecordService extends BaseService { + + /// 按条件查询库存流水 + PageResult pageByQuery(GaoProductStockRecordPage page); + + /// 查询库存流水详情 + GaoProductStockRecord detail(String id); + + /// 按来源业务查询库存流水 + List listByBiz(GaoProductStockRecord.BizType bizType, String bizId); + + /// 按门店和请求号查询库存流水 + List listByRequestNo(String storeId, String requestNo); + + /// 锁定库存流水 + GaoProductStockRecord getForUpdate(String id); + + /// 查询库存冲正流水 + GaoProductStockRecord getRevokeBySourceRecordId(String sourceRecordId); + + /// 查询指定业务和产品的库存流水 + GaoProductStockRecord getByBizSku(GaoProductStockRecord.BizType bizType, String bizId, String skuId, + GaoProductStockRecord.OperationType operationType); + + /// 保存库存操作流水及附件 + void createOperationRecord(GaoProductStockRecord record, List attachmentList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java new file mode 100644 index 0000000..43a9999 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java @@ -0,0 +1,60 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView; +import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; +import com.imyeyu.spring.bean.PageResult; + +import java.util.List; + +/// GAO 产品库存服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductStockService { + + /// 查询库存余额 + PageResult pageByQuery(GaoProductStockPage page); + + /// 按产品汇总查询库存 + PageResult pageProductByQuery(GaoProductStockPage page); + + /// 查询库存余额详情 + GaoProductStock detail(String storeId, String skuId); + + /// 期初入库 + GaoProductStock initialIn(GaoProductStockChangeRequest request); + + /// 手工入库 + GaoProductStock manualIn(GaoProductStockChangeRequest request); + + /// 手工出库 + GaoProductStock manualOut(GaoProductStockChangeRequest request); + + /// 手工库存流水冲正 + GaoProductStock manualRevoke(GaoProductStockManualRevokeRequest request); + + /// 盘盈 + GaoProductStock stocktakeGain(GaoProductStockChangeRequest request); + + /// 盘亏 + GaoProductStock stocktakeLoss(GaoProductStockChangeRequest request); + + /// 为履约预占库存 + boolean hasSufficientForFulfillment(String storeId, List items); + + /// 为履约预占库存 + List reserveForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); + + /// 释放履约预占库存 + List releaseForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); + + /// 履约出库 + List outForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); + + /// 履约库存冲正入库 + List revokeForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java new file mode 100644 index 0000000..c95c273 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java @@ -0,0 +1,60 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderSkuRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; + +import java.util.List; + +/// GAO 销售订单图表服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoSalesOrderChartService { + + /// 查询销售订单每日统计 + List daily(GaoSalesOrderChartReq req); + + /// 查询销售订单类型统计 + List type(GaoSalesOrderChartReq req); + + /// 查询销售订单付款状态统计 + List payment(GaoSalesOrderChartReq req); + + /// 查询客户订单排行 + List customerRank(GaoSalesOrderChartReq req); + + /// 查询客户订单产品排行 + List customerProductRank(GaoSalesOrderChartReq req); + + /// 查询客户订单 SKU 排行 + List customerSkuRank(GaoSalesOrderChartReq req); + + /// 查询销售订单状态统计 + List status(GaoSalesOrderChartReq req); + + /// 查询销售订单应收统计 + GaoSalesOrderReceivableStatView receivable(GaoSalesOrderChartReq req); + + /// 查询客户每日有效消费统计 + List consumptionDaily(GaoSalesOrderChartReq req); + + /// 查询客户订单日历统计 + List calendar(GaoSalesOrderChartReq req); + + /// 查询客户每日复购统计 + List repeatDaily(GaoSalesOrderChartReq req); + + /// 查询客户复购间隔统计 + List repeatInterval(GaoSalesOrderChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java new file mode 100644 index 0000000..02bb4eb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java @@ -0,0 +1,81 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderUpdateRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/// GAO 销售订单服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoSalesOrderService extends BaseService { + + /// 按条件分页查询订单 + PageResult pageByQuery(GaoSalesOrderPage page); + + /// 查询订单详情 + GaoSalesOrder detail(String id); + + /// 创建直接产品订单 + GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request); + + /// 只更新订单备注和附件 + GaoSalesOrder updateRemarkAndAttachments(GaoSalesOrderUpdateRequest request); + + /// 按门店和幂等号查询订单 + /// + /// @param storeId 门店 ID + /// @param requestNo 幂等号 + /// @return 销售订单,不存在时为空 + GaoSalesOrder getByStoreIdAndRequestNo(String storeId, String requestNo); + + /// 由套餐购买流程创建套餐订单 + GaoSalesOrder createPackageOrder(GaoPackageOrderCreateData data); + + /// 锁定订单 + GaoSalesOrder getForUpdate(String id); + + /// 取消订单 + GaoSalesOrder cancel(GaoSalesOrderCancelRequest request); + + /// 更新订单付款汇总 + void updatePayment(GaoSalesOrder order); + + /// 重算付款状态 + void recomputePaymentStatus(GaoSalesOrder order); + + /// 检查履约权限 + void checkFulfillmentPermission(GaoSalesOrder order); + + /// 原子调整订单项待履约数量 + int updateItemReservedQuantity(String itemId, BigDecimal delta); + + /// 原子调整订单项已履约数量 + int updateItemFulfilledQuantity(String itemId, BigDecimal delta); + + /// 锁定订单明细 + GaoSalesOrderItem getItemForUpdate(String itemId); + + /// 根据订单明细刷新订单履约状态 + void refreshFulfillmentStatus(GaoSalesOrder order); + + /// 写入销售订单履约状态 + void updateFulfillmentStatus(GaoSalesOrder order, boolean fulfilled); + + /// 查询订单明细 + List listItemByOrderId(String orderId); + + /// 批量查询订单及其明细 + Map mapByIdList(Collection idList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java index e2e1d32..f1341d0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java @@ -1,7 +1,7 @@ package com.imyeyu.api.modules.gao.service.implement; -import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.service.GaoCustomerChartService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; @@ -19,18 +19,18 @@ import java.util.List; @RequiredArgsConstructor public class GaoCustomerChartServiceImplement implements GaoCustomerChartService { - private final GaoEventRecordMapper eventRecordMapper; + private final GaoEventRecordService eventRecordService; @Override public List eventRecordDailyStat(GaoCustomerChartReq req) { checkRangeReq(req); - return eventRecordMapper.stateCustomerDailyByRange(req); + return eventRecordService.stateCustomerDailyByRange(req); } @Override public List eventRecordCalendar(GaoCustomerChartReq req) { checkRangeReq(req); - return eventRecordMapper.selectCustomerCalendarByRange(req); + return eventRecordService.calendarByCustomerRange(req); } private void checkRangeReq(GaoCustomerChartReq req) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerConsumptionRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerConsumptionRecordServiceImplement.java new file mode 100644 index 0000000..73f9917 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerConsumptionRecordServiceImplement.java @@ -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 implements GaoCustomerConsumptionRecordService { + + private final GaoCustomerConsumptionRecordMapper mapper; + + @Override + protected BaseMapper 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 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); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java index 78f8b6d..d63d3e9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java @@ -2,27 +2,27 @@ package com.imyeyu.api.modules.gao.service.implement; import com.imyeyu.api.bean.ModuleCode; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.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.GaoCustomerPackage; -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.GaoPackageConfigItem; -import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageItemMapper; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; import com.imyeyu.api.modules.gao.service.GaoCustomerService; 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.GaoFundRecordService; -import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem; +import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.user.service.PermissionChecker; @@ -34,12 +34,10 @@ import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -55,17 +53,13 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class GaoCustomerPackageServiceImplement extends AbstractEntityService implements GaoCustomerPackageService { - private static final BigDecimal ZERO = BigDecimal.ZERO; - private final GaoCustomerPackageMapper mapper; - private final GaoCustomerPackageItemMapper itemMapper; - private final ObjectProvider deliveryServiceProvider; - private final ObjectProvider fundRecordServiceProvider; private final GaoPackageConfigService packageConfigService; + private final GaoProductSkuService skuService; private final GaoCustomerService customerService; - private final GaoEventService eventService; - private final GaoEventRecordService eventRecordService; + private final GaoSalesOrderService salesOrderService; private final GaoStoreScopeService storeScopeService; + private final AttachmentService attachmentService; private final UserLoginService userLoginService; private final PermissionChecker permissionChecker; private final LoggerService loggerService; @@ -76,43 +70,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService listItemByCustomerPackageId(String customerPackageId) { - return itemMapper.selectByCustomerPackageId(customerPackageId); - } - - @Override - public GaoCustomerPackageItem getItemForUpdate(String itemId) { - return itemMapper.selectForUpdate(itemId); - } - - @Override - public int updateItemReservedQuantity(String itemId, BigDecimal delta) { - return itemMapper.updateReservedQuantity(itemId, delta); - } - - @Override - public int updateItemDeliveredQuantity(String itemId, BigDecimal delta) { - return itemMapper.updateDeliveredQuantity(itemId, delta); - } - - @Override - public int updatePendingPaidAmountDelta(String id, Long delta) { - return mapper.updatePendingPaidAmountDelta(id, delta); - } - - @Override - public int updatePaidAmountDelta(String id, Long delta) { - return mapper.updatePaidAmountDelta(id, delta); - } - - @Override - public void updateAggregateState(GaoCustomerPackage packageEntity) { - mapper.update(packageEntity); + public GaoCustomerPackage getBySalesOrderId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + return mapper.selectBySalesOrderId(salesOrderId); } /// 客户套餐必须通过购买流程创建 @@ -171,14 +131,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService configItemList = packageConfigService.listItemByPackageConfigId(config.getId()); request.setSaleAmount(TimiJava.defaultIfNull(request.getSaleAmount(), config.getStandardAmount())); TimiException.requiredTrue(0 <= request.getSaleAmount(), "成交价不能为负数"); @@ -190,11 +148,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService orderItemList = itemList.stream().map(this::toOrderItem).toList(); + GaoPackageOrderCreateData orderData = new GaoPackageOrderCreateData(); + orderData.setStoreId(config.getStoreId()); + orderData.setCustomerId(customer.getId()); + orderData.setPackageConfigId(config.getId()); + orderData.setPackageNameSnapshot(config.getName()); + orderData.setStandardAmountSnapshot(config.getStandardAmount()); + orderData.setSaleAmount(request.getSaleAmount()); + orderData.setRequestNo(request.getRequestNo()); + orderData.setPurchasedAt(purchasedAt); + orderData.setOperatorUserId(userLoginService.getRequireLoginUserId()); + orderData.setRemark(request.getRemark()); + orderData.setItemList(orderItemList); + GaoSalesOrder salesOrder = salesOrderService.createPackageOrder(orderData); + attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, salesOrder.getId(), request.getAttachmentList()); GaoCustomerPackage result = new GaoCustomerPackage(); result.setStoreId(config.getStoreId()); + result.setSalesOrderId(salesOrder.getId()); result.setCustomerId(customer.getId()); result.setPackageConfigId(config.getId()); - result.setPurchaseEventRecordId(createdEventRecord.getId()); - result.setRequestNo(request.getRequestNo()); result.setPackageNameSnapshot(config.getName()); result.setStandardAmountSnapshot(config.getStandardAmount()); - result.setSaleAmount(request.getSaleAmount()); - result.setPendingPaidAmount(0L); - result.setPaidAmount(0L); - result.setRefundedAmount(0L); - result.setStatus(GaoCustomerPackage.Status.ACTIVE); - result.setPaymentStatus(request.getSaleAmount() == 0 ? GaoCustomerPackage.PaymentStatus.PAID : GaoCustomerPackage.PaymentStatus.UNPAID); - result.setRefundStatus(GaoCustomerPackage.RefundStatus.NOT_REQUIRED); - result.setPurchasedAt(purchasedAt); - result.setOperatorUserId(userLoginService.getRequireLoginUserId()); result.setRemark(request.getRemark()); super.create(result); - for (GaoCustomerPackagePurchaseItem source : itemList) { - GaoCustomerPackageItem item = new GaoCustomerPackageItem(); - item.setCustomerPackageId(result.getId()); - item.setProductNameSnapshot(source.getProductName().trim()); - item.setUnitSnapshot(source.getUnit().trim()); - item.setTotalQuantity(source.getTotalQuantity()); - item.setReservedQuantity(ZERO); - item.setDeliveredQuantity(ZERO); - item.setSort(source.getSort()); - item.setRemark(source.getRemark()); - itemMapper.insert(item); - } + result.setSalesOrder(salesOrder); + result.setItemList(salesOrder.getItemList()); + result.setStatus(salesOrder.getStatus()); return result; } @@ -264,31 +210,23 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList = itemMapper.selectByCustomerPackageId(packageEntity.getId()); - for (GaoCustomerPackageItem item : itemList) { - TimiException.requiredTrue(item.getDeliveredQuantity().compareTo(item.getTotalQuantity()) < 0, "已全部履约的套餐不能取消"); - } - packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED); - packageEntity.setCancelledAt(Time.now()); - packageEntity.setCancelOperatorUserId(userLoginService.getRequireLoginUserId()); - packageEntity.setCancelReason(reason); - packageEntity.setRefundStatus(0 < safeLong(packageEntity.getPaidAmount()) ? GaoCustomerPackage.RefundStatus.PENDING : GaoCustomerPackage.RefundStatus.NOT_REQUIRED); - mapper.update(packageEntity); + TimiException.required(packageEntity.getSalesOrderId(), "套餐未关联销售订单"); + GaoSalesOrderCancelRequest orderCancelRequest = new GaoSalesOrderCancelRequest(); + orderCancelRequest.setSalesOrderId(packageEntity.getSalesOrderId()); + orderCancelRequest.setReason(reason); + GaoSalesOrder order = salesOrderService.cancel(orderCancelRequest); + packageEntity.setSalesOrder(order); + packageEntity.setStatus(order.getStatus()); + packageEntity.setCancelledAt(order.getCancelledAt()); + packageEntity.setCancelOperatorUserId(order.getCancelOperatorUserId()); + packageEntity.setCancelReason(order.getCancelReason()); + packageEntity.setItemList(salesOrderService.listItemByOrderId(order.getId())); return packageEntity; } - private GaoEventRecord eventRecordWithAttachments(GaoEventRecord record, GaoCustomerPackagePurchaseRequest request) { - record.setAttachmentList(request.getAttachmentList()); - return record; - } - private GaoCustomer validateCustomer(String customerId, String storeId) { GaoCustomer customer = customerService.get(customerId); TimiException.required(customer, "not found customer"); @@ -296,19 +234,18 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService defaultPurchaseItems(List configItemList) { return configItemList.stream().map(configItem -> { GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem(); - item.setProductName(configItem.getProductName()); - item.setUnit(configItem.getUnit()); + item.setSkuId(configItem.getSkuId()); + item.setSkuUnitId(configItem.getSkuUnitId()); + item.setProductNameSnapshot(configItem.getProductNameSnapshot()); + item.setProductIdSnapshot(configItem.getProductIdSnapshot()); + item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot()); + item.setSkuNameSnapshot(configItem.getSkuNameSnapshot()); + item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot()); + item.setUnitSnapshot(configItem.getUnitSnapshot()); + item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); item.setTotalQuantity(configItem.getDefaultQuantity()); item.setSort(configItem.getSort()); item.setRemark(configItem.getRemark()); @@ -316,83 +253,104 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList) { TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品"); Set keySet = new HashSet<>(); for (int i = 0; i < itemList.size(); i++) { GaoCustomerPackagePurchaseItem item = itemList.get(i); TimiException.required(item, "购买产品明细不能为空"); - TimiException.required(item.getProductName(), "购买产品名称不能为空"); - TimiException.required(item.getUnit(), "购买产品单位不能为空"); + TimiException.required(item.getSkuId(), "购买 SKU 不能为空"); + TimiException.required(item.getSkuUnitId(), "购买 SKU 单位不能为空"); TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空"); TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0"); - TimiException.requiredTrue(3 >= item.getTotalQuantity().scale(), "购买产品数量最多保留三位小数"); - String key = item.getProductName().trim() + "|" + item.getUnit().trim(); - TimiException.requiredTrue(keySet.add(key), "购买套餐内产品名称和单位不能重复"); - item.setProductName(item.getProductName().trim()); - item.setUnit(item.getUnit().trim()); + TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数"); + String key = item.getSkuId() + "|" + item.getSkuUnitId(); + TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复"); item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); } } + private void verifyConfigSellableItems(String storeId, List itemList) { + for (GaoPackageConfigItem item : itemList) { + TimiException.required(item.getSkuId(), "套餐配置缺少 SKU"); + TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位"); + // 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态 + skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId()); + } + } + private void verifySameStructureAsConfig(List itemList, List configItemList) { - TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致"); - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity())); + TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致"); + Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit()); - TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致"); + GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId()); + TimiException.required(item, "套餐权益项目必须与套餐配置一致"); + item.setProductNameSnapshot(configItem.getProductNameSnapshot()); + item.setProductIdSnapshot(configItem.getProductIdSnapshot()); + item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot()); + item.setSkuNameSnapshot(configItem.getSkuNameSnapshot()); + item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot()); + item.setUnitSnapshot(configItem.getUnitSnapshot()); + item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); TimiException.requiredTrue(item.getSort().equals(configItem.getSort()) && sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改"); } } private void verifySameQuantityAsConfig(List itemList, List configItemList) { - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity())); + Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit()); + GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId()); TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额"); } } - private void verifyPurchaseIdempotency(GaoCustomerPackage existing, GaoCustomerPackagePurchaseRequest request) { - TimiException.requiredTrue(existing.getCustomerId().equals(request.getCustomerId()) - && existing.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用"); - GaoEventRecord eventRecord = eventRecordService.get(existing.getPurchaseEventRecordId()); - TimiException.required(eventRecord, "购买事件登记不存在"); - TimiException.requiredTrue(eventRecord.getEventId().equals(request.getEventId()) - && (request.getPurchasedAt() == null || eventRecord.getRegisteredAt().equals(request.getPurchasedAt())) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致"); - List existingItemList = itemMapper.selectByCustomerPackageId(existing.getId()); + private void verifyPurchaseIdempotency(GaoSalesOrder existingOrder, GaoCustomerPackage existingPackage, GaoCustomerPackagePurchaseRequest request) { + TimiException.required(existingPackage, "幂等订单未找到客户套餐权益"); + TimiException.requiredTrue(existingOrder.getOrderType() == GaoSalesOrder.OrderType.PACKAGE + && existingOrder.getCustomerId().equals(request.getCustomerId()) + && existingOrder.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用"); + TimiException.requiredTrue(existingPackage.getPackageConfigId().equals(request.getPackageConfigId()) + && (request.getPurchasedAt() == null || existingOrder.getPurchasedAt().equals(request.getPurchasedAt())) + && sameText(existingOrder.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致"); + List existingItemList = salesOrderService.listItemByOrderId(existingOrder.getId()); List requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of()); verifyPurchaseItems(requestItemList); TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致"); - Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductNameSnapshot() + "|" + item.getUnitSnapshot(), Function.identity())); + Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); for (GaoCustomerPackagePurchaseItem item : requestItemList) { - GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductName() + "|" + item.getUnit()); + GaoSalesOrderItem existingItem = existingItemMap.get(item.getSkuId() + "|" + item.getSkuUnitId()); TimiException.required(existingItem, "requestNo 对应购买请求内容不一致"); - TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0 + TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT + && existingItem.getQuantity().compareTo(item.getTotalQuantity()) == 0 && existingItem.getSort().equals(item.getSort()) && sameText(existingItem.getRemark(), item.getRemark()), "requestNo 对应购买请求内容不一致"); } } - @Override - public Map> mapItemListByCustomerPackageIdList(Collection customerPackageIdList) { - if (customerPackageIdList == null || customerPackageIdList.isEmpty()) { - return Map.of(); - } - return itemMapper.selectByCustomerPackageIdList(customerPackageIdList).stream() - .collect(Collectors.groupingBy(GaoCustomerPackageItem::getCustomerPackageId)); - } - - private Long pendingAmount(GaoCustomerPackage packageEntity) { - return safeLong(packageEntity.getPendingPaidAmount()); - } - - private long safeLong(Long value) { - return value == null ? 0L : value; - } - private String requiredReason(String reason, String message) { TimiException.required(reason, message); String result = reason.trim(); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java index 043cb7a..f90b65c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java @@ -21,9 +21,14 @@ import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.api.modules.gao.util.EventRecordXSSFBuilder; import com.imyeyu.api.modules.gao.vo.GaoCustomerEventRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; import com.imyeyu.api.modules.gao.vo.GaoEventOptionStatView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoEventRecordNumberValueDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordPage; import com.imyeyu.api.modules.gao.vo.GaoEventRecordRankPage; +import com.imyeyu.api.modules.gao.vo.GaoStoreChartReq; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.user.service.RoleChecker; @@ -94,6 +99,54 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService recordList) { List recordIdList = new ArrayList<>(); @@ -161,22 +214,22 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList()); verifyRecordValue(event, record.getNumberValue()); @@ -302,6 +351,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = null; if (record.getSelectedOptionGroupList() != null) { @@ -351,9 +402,11 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService stateDailyByRange(GaoStoreChartReq req) { + return mapper.stateDailyByRange(req); + } + + @Override + public List stateNumberValueDailyByRange(GaoStoreChartReq req) { + return mapper.stateNumberValueDailyByRange(req); + } + + @Override + public List calendarByStoreRange(GaoStoreChartReq req) { + return mapper.selectCalendarByRange(req); + } + + @Override + public List stateCustomerDailyByRange(GaoCustomerChartReq req) { + return mapper.stateCustomerDailyByRange(req); + } + + @Override + public List calendarByCustomerRange(GaoCustomerChartReq req) { + return mapper.selectCustomerCalendarByRange(req); + } + @Override public Long sumNumberValueByRange(GaoEventRecordPage page) { TimiException.required(page.getStoreId(), "not found page.storeId"); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java index e099af8..0581c70 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java @@ -6,14 +6,12 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoEventOption; import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation; -import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.mapper.GaoEventMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventOptionGroupMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventOptionMapper; -import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventRoleRelationMapper; +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.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.java.TimiJava; @@ -26,6 +24,7 @@ import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -57,9 +56,8 @@ public class GaoEventServiceImplement extends AbstractEntityService eventRecordServiceProvider; @Override protected BaseMapper mapper() { @@ -87,6 +85,7 @@ public class GaoEventServiceImplement extends AbstractEntityService(idList)).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity())); } + @Override + public long countByPackageConfigId(String packageConfigId) { + TimiException.required(packageConfigId, "not found packageConfigId"); + return mapper.countByPackageConfigId(packageConfigId); + } + @Override public Map> mapRoleCodeListByEventIdList(Collection eventIdList) { return roleRelationMapper.selectAllByEventIdList(new HashSet<>(eventIdList)) @@ -325,9 +334,6 @@ public class GaoEventServiceImplement extends AbstractEntityService implements GaoFundRecordService { private final GaoFundRecordMapper mapper; - private final GaoCustomerPackageService customerPackageService; - private final GaoPackageDeliveryService deliveryService; - private final AttachmentService attachmentService; - private final GaoStoreScopeService storeScopeService; - private final UserLoginService userLoginService; - private final LoggerService loggerService; @Override protected BaseMapper mapper() { return mapper; } - /// 资金流水必须通过专用操作创建 + /// 资金流水必须通过销售订单资金接口创建 /// /// @param record 资金流水 @Override public void create(GaoFundRecord record) { - TimiException.requiredTrue(false, "资金流水必须通过专用接口创建"); + TimiException.requiredTrue(false, "资金流水必须通过销售订单资金接口创建"); + } + + @Override + public void createByOrderPayment(GaoFundRecord record) { + TimiException.required(record, "not found fund record"); + super.create(record); } /// 资金流水不允许通用修改 @@ -94,296 +76,51 @@ public class GaoFundRecordServiceImplement extends AbstractEntityService listByCustomerPackageId(String customerPackageId) { - TimiException.required(customerPackageId, "not found customerPackageId"); - return mapper.selectByBizId(customerPackageId); - } - - @Override - public long countPendingReceiveByBizId(String bizId) { - return mapper.countPendingReceiveByBizId(bizId); - } - - @Override - public GaoFundRecord get(String id) { - return super.get(id); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord receive(GaoFundReceiveRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CREATE", request, () -> receiveInternal(request)); - } - - private GaoFundRecord receiveInternal(GaoFundReceiveRequest request) { - TimiException.required(request, "not found receive request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - TimiException.required(request.getAmount(), "not found amount"); - TimiException.requiredTrue(0 < request.getAmount(), "收款金额必须大于 0"); - GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); - GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) - && existing.getOperationType() == GaoFundRecord.OperationType.RECEIVE - && existing.getAmount().equals(request.getAmount()) - && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "收款必须上传照片"); - checkPackageReceivable(packageEntity); - TimiException.requiredTrue(safeLong(packageEntity.getPaidAmount()) + safeLong(packageEntity.getPendingPaidAmount()) + request.getAmount() <= safeLong(packageEntity.getSaleAmount()), "累计收款不能超过成交价"); - GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.RECEIVE, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark()); - record.setStatus(GaoFundRecord.Status.PENDING); - super.create(record); - TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), request.getAmount()), "待核销收款更新失败"); - attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord verify(String fundRecordId) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_VERIFY", fundRecordId, () -> verifyInternal(fundRecordId)); - } - - private GaoFundRecord verifyInternal(String fundRecordId) { - GaoFundRecord record = mapper.selectForUpdate(fundRecordId); - TimiException.required(record, "not found fund record"); - storeScopeService.checkStoreId(record.getStoreId()); - TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); - GaoCustomerPackage packageEntity = lockPackage(record.getBizId()); - TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败"); - TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), record.getAmount()), "有效收款更新失败"); - String userId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化"); - updatePaymentStatus(packageEntity, safeLong(packageEntity.getPaidAmount()) + record.getAmount(), safeLong(packageEntity.getPendingPaidAmount()) - record.getAmount()); - record.setStatus(GaoFundRecord.Status.VERIFIED); - record.setVerifierUserId(userId); - record.setVerifiedAt(Time.now()); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord reject(GaoFundActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_REJECT", request, () -> releasePending(request, GaoFundRecord.Status.REJECTED)); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord cancel(GaoFundActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CANCEL", request, () -> { - TimiException.required(request, "not found fund action request"); - TimiException.required(request.getFundRecordId(), "not found fundRecordId"); - GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId()); - TimiException.required(record, "not found fund record"); - TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(record.getOperatorUserId()), "只能撤销自己提交的收款"); - return releasePending(request, GaoFundRecord.Status.CANCELLED); - }); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord refund(GaoFundRefundRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REFUND", request, () -> refundInternal(request)); - } - - private GaoFundRecord refundInternal(GaoFundRefundRequest request) { - TimiException.required(request, "not found refund request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - TimiException.required(request.getAmount(), "not found amount"); - TimiException.requiredTrue(0 < request.getAmount(), "退款金额必须大于 0"); - TimiException.requiredTrue(hasReason(request.getReason()), "退款原因不能为空"); - GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); - GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) - && existing.getOperationType() == GaoFundRecord.OperationType.REFUND - && existing.getAmount().equals(request.getAmount()) - && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) - && sameText(existing.getReason(), request.getReason().trim()) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "退款必须上传照片"); - checkRefundable(packageEntity); - TimiException.requiredTrue(request.getAmount() <= safeLong(packageEntity.getPaidAmount()), "退款金额不能超过累计有效收款"); - GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.REFUND, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark()); - record.setStatus(GaoFundRecord.Status.VERIFIED); - record.setReason(request.getReason().trim()); - super.create(record); - packageEntity.setRefundedAmount(request.getAmount()); - packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED); - customerPackageService.updateAggregateState(packageEntity); - attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_CONFIRM_NO_REFUND", request, () -> confirmNoRefundInternal(request)); - } - - private GaoFundRecord confirmNoRefundInternal(GaoFundConfirmNoRefundRequest request) { - TimiException.required(request, "not found no refund request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - TimiException.requiredTrue(hasReason(request.getReason()), "确认无需退款原因不能为空"); - GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); - GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) - && existing.getOperationType() == GaoFundRecord.OperationType.CONFIRM_NO_REFUND - && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) - && sameText(existing.getReason(), request.getReason().trim()) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - checkRefundable(packageEntity); - GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.CONFIRM_NO_REFUND, request.getRequestNo(), 0L, request.getOccurredAt(), request.getRemark()); - record.setStatus(GaoFundRecord.Status.VERIFIED); - record.setReason(request.getReason().trim()); - super.create(record); - packageEntity.setRefundedAmount(0L); - packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED); - customerPackageService.updateAggregateState(packageEntity); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord revoke(GaoFundRevokeRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REVOKE", request, () -> revokeInternal(request)); - } - - private GaoFundRecord revokeInternal(GaoFundRevokeRequest request) { - TimiException.required(request, "not found fund revoke request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getFundRecordId(), "not found fundRecordId"); - TimiException.requiredTrue(hasReason(request.getReason()), "资金冲正原因不能为空"); - GaoFundRecord source = mapper.selectForUpdate(request.getFundRecordId()); - TimiException.required(source, "not found fund record"); - storeScopeService.checkStoreId(source.getStoreId()); - GaoFundRecord existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE - && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "资金冲正必须上传照片"); - TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正"); - TimiException.requiredNull(mapper.selectRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正"); - GaoCustomerPackage packageEntity = lockPackage(source.getBizId()); - if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) { - TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "退款完成后不能冲正相关收款"); - long paidAfter = safeLong(packageEntity.getPaidAmount()) - source.getAmount(); - if (0 < deliveryService.countVerifiedByCustomerPackageId(packageEntity.getId())) { - TimiException.requiredTrue(packageEntity.getSaleAmount() <= paidAfter, "存在已核销发放,冲正收款后套餐仍须付清"); - } - TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), -source.getAmount()), "有效收款冲正失败"); - packageEntity.setPaidAmount(paidAfter); - updatePaymentStatus(packageEntity, paidAfter, packageEntity.getPendingPaidAmount()); - } else { - TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.REFUNDED, "资金记录退款状态无效"); - packageEntity.setRefundedAmount(0L); - packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.PENDING); - customerPackageService.updateAggregateState(packageEntity); - } - GaoFundRecord revoke = newRecord(packageEntity, GaoFundRecord.OperationType.REVOKE, request.getRequestNo(), source.getAmount(), request.getOccurredAt(), request.getRemark()); - revoke.setStatus(GaoFundRecord.Status.VERIFIED); - revoke.setSourceRecordId(source.getId()); - revoke.setReason(request.getReason().trim()); - super.create(revoke); - attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList()); - return revoke; - } - - private GaoFundRecord releasePending(GaoFundActionRequest request, GaoFundRecord.Status targetStatus) { - TimiException.required(request, "not found fund action request"); - TimiException.required(request.getFundRecordId(), "not found fundRecordId"); - TimiException.requiredTrue(hasReason(request.getReason()), "拒绝或撤销原因不能为空"); - GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId()); - TimiException.required(record, "not found fund record"); - storeScopeService.checkStoreId(record.getStoreId()); - TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); - GaoCustomerPackage packageEntity = lockPackage(record.getBizId()); - TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败"); - String userId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, targetStatus, userId, Time.now(), request.getReason().trim()), "收款记录状态已变化"); - record.setStatus(targetStatus); - record.setVerifierUserId(userId); - record.setVerifiedAt(Time.now()); - record.setReason(request.getReason().trim()); - return record; - } - - private GaoCustomerPackage lockPackage(String id) { - GaoCustomerPackage result = customerPackageService.getForUpdate(id); - TimiException.required(result, "not found customer package"); - storeScopeService.checkStoreId(result.getStoreId()); + public GaoFundRecord detail(String id) { + TimiException.required(id, "not found fund record id"); + GaoFundRecord result = mapper.selectWithSalesOrderSummary(id); + TimiException.required(result, "not found fund record"); + GaoFundRecord revoke = mapper.selectRevokeBySourceRecordId(result.getId()); + result.setRevokeRecordId(revoke == null ? null : revoke.getId()); return result; } - private void checkPackageReceivable(GaoCustomerPackage packageEntity) { - TimiException.requiredTrue(packageEntity.getStatus() != GaoCustomerPackage.Status.CANCELLED, "已取消套餐不能收款"); - TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "已完成退款套餐不能收款"); + @Override + public List listByBizId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + return mapper.selectByBizId(salesOrderId); } - private void checkRefundable(GaoCustomerPackage packageEntity) { - TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.CANCELLED, "只有已取消套餐可以退款"); - TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.PENDING, "当前套餐不在待退款状态"); + @Override + public long countPendingReceiveByBizId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + return mapper.countPendingReceiveByBizId(salesOrderId); } - private GaoFundRecord newRecord(GaoCustomerPackage packageEntity, GaoFundRecord.OperationType operationType, String requestNo, Long amount, Long occurredAt, String remark) { - GaoFundRecord record = new GaoFundRecord(); - record.setStoreId(packageEntity.getStoreId()); - record.setCustomerId(packageEntity.getCustomerId()); - record.setBizType(GaoFundRecord.BizType.CUSTOMER_PACKAGE); - record.setBizId(packageEntity.getId()); - record.setOperationType(operationType); - record.setAmount(amount); - record.setRequestNo(requestNo); - record.setOperatorUserId(userLoginService.getRequireLoginUserId()); - record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now())); - record.setRemark(remark); - return record; + @Override + public GaoFundRecord getByRequestNo(String storeId, String requestNo) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(requestNo, "not found requestNo"); + return mapper.selectByRequestNo(storeId, requestNo); } - private void updatePaymentStatus(GaoCustomerPackage packageEntity, long paidAmount, long pendingAmount) { - packageEntity.setPaidAmount(paidAmount); - packageEntity.setPendingPaidAmount(pendingAmount); - packageEntity.setPaymentStatus(safeLong(packageEntity.getSaleAmount()) == 0 || paidAmount >= safeLong(packageEntity.getSaleAmount()) - ? GaoCustomerPackage.PaymentStatus.PAID - : (0 < paidAmount ? GaoCustomerPackage.PaymentStatus.PARTIALLY_PAID : GaoCustomerPackage.PaymentStatus.UNPAID)); - customerPackageService.updateAggregateState(packageEntity); + @Override + public GaoFundRecord getForUpdate(String id) { + TimiException.required(id, "not found fundRecordId"); + return mapper.selectForUpdate(id); } - private boolean hasAttachment(List attachmentList) { - for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { - if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { - return true; - } - } - return false; + @Override + public GaoFundRecord getRevokeBySourceRecordId(String sourceRecordId) { + TimiException.required(sourceRecordId, "not found sourceRecordId"); + return mapper.selectRevokeBySourceRecordId(sourceRecordId); } - private boolean hasReason(String reason) { - return reason != null && !reason.trim().isEmpty(); - } - - private boolean sameText(String left, String right) { - return left == null ? right == null : left.equals(right); - } - - private long safeLong(Long value) { - return value == null ? 0L : value; + @Override + public int updateStatusIf(String id, GaoFundRecord.Status fromStatus, GaoFundRecord.Status toStatus, String verifierUserId, Long verifiedAt, String reason) { + TimiException.required(id, "not found fundRecordId"); + TimiException.required(fromStatus, "not found fromStatus"); + TimiException.required(toStatus, "not found toStatus"); + return mapper.updateStatusIf(id, fromStatus, toStatus, verifierUserId, verifiedAt, reason); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java new file mode 100644 index 0000000..631c9dc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java @@ -0,0 +1,704 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.bean.ModuleCode; +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.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillmentItem; +import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentItemMapper; +import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentMapper; +import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductStockService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; +import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/// GAO 统一履约服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService implements GaoOrderFulfillmentService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoOrderFulfillmentMapper mapper; + private final GaoOrderFulfillmentItemMapper itemMapper; + private final GaoSalesOrderService salesOrderService; + private final GaoProductService productService; + private final GaoProductSkuService skuService; + private final GaoProductStockService productStockService; + private final GaoProductStockRecordService stockRecordService; + private final GaoStoreScopeService storeScopeService; + private final RoleChecker roleChecker; + private final UserLoginService userLoginService; + private final AttachmentService attachmentService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + /// 履约单必须通过专用流程创建 + @Override + public void create(GaoOrderFulfillment fulfillment) { + throw new TimiException(TimiCode.ERROR, "履约单必须通过专用接口创建"); + } + + /// 履约单不允许通用修改 + @Override + public void update(GaoOrderFulfillment fulfillment) { + throw new TimiException(TimiCode.ERROR, "履约单不允许通用修改"); + } + + /// 履约单不允许删除 + @Override + public void delete(String id) { + throw new TimiException(TimiCode.ERROR, "履约单不允许删除"); + } + + /// 履约单不允许物理删除 + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "履约单不允许物理删除"); + } + + @Override + public PageResult pageByQuery(GaoOrderFulfillmentPage page) { + TimiException.required(page, "not found fulfillment page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + List list = mapper.selectByQuery(page); + fillItemList(list); + result.setList(list); + return result; + } + + @Override + public GaoOrderFulfillment detail(String id) { + GaoOrderFulfillment fulfillment = mapper.selectById(id); + TimiException.required(fulfillment, "not found fulfillment"); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + List itemList = itemMapper.selectByFulfillmentId(fulfillment.getId()); + fillItemAttachments(itemList); + fulfillment.setItemList(itemList); + fulfillment.setStockRecordList(stockRecordService.listByBiz(GaoProductStockRecord.BizType.ORDER_FULFILLMENT, fulfillment.getId())); + return fulfillment; + } + + @Override + public List listBySalesOrderId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + List list = mapper.selectBySalesOrderId(salesOrderId); + fillItemList(list); + return list; + } + + @Override + public long countPendingBySalesOrderId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + return mapper.countPendingBySalesOrderId(salesOrderId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CREATE", request, () -> createInternal(request)); + } + + private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) { + TimiException.required(request, "not found fulfillment request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + GaoSalesOrder order = salesOrderService.getForUpdate(request.getSalesOrderId()); + GaoOrderFulfillment existing = mapper.selectByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + verifyIdempotency(existing, request); + return detail(existing.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "履约必须上传照片"); + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能履约"); + salesOrderService.checkFulfillmentPermission(order); + List sourceList = loadSources(order, request.getItemList(), true); + List stockItems = buildStockItems(order.getStoreId(), sourceList); + boolean stockReady = stockItems.isEmpty() || productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems); + String operatorUserId = userLoginService.getRequireLoginUserId(); + GaoOrderFulfillment fulfillment = newFulfillment(order, + stockReady ? GaoOrderFulfillment.Status.PENDING : GaoOrderFulfillment.Status.WAITING_STOCK, + requestNo, operatorUserId, TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()), request.getRemark()); + super.create(fulfillment); + insertSourceReservations(fulfillment, sourceList); + if (stockReady && !stockItems.isEmpty()) { + productStockService.reserveForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "RESERVE"), operatorUserId); + } + attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, fulfillment.getId(), request.getAttachmentList()); + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_PREPARE_STOCK", request, () -> prepareStockInternal(request)); + } + + private GaoOrderFulfillment prepareStockInternal(GaoOrderFulfillmentPrepareStockRequest request) { + TimiException.required(request, "not found prepare stock request"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name()); + GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId()); + TimiException.required(snapshot, "not found fulfillment"); + GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId()); + List sourceList = loadSources(order, itemRequests(snapshot.getId()), false); + GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态"); + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存"); + salesOrderService.checkFulfillmentPermission(order); + List fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); + TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); + List stockItems = buildStockItems(order.getStoreId(), sourceList); + applySkuSpecSnapshots(stockItems, fulfillmentItemList); + TimiException.requiredTrue(!stockItems.isEmpty(), "履约单没有需要占用的实物库存"); + TimiException.requiredTrue(productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems), "库存仍然不足,不能准备履约"); + String operatorUserId = userLoginService.getRequireLoginUserId(); + productStockService.reserveForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "RESERVE"), operatorUserId); + TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), GaoOrderFulfillment.Status.PENDING, null, null, request.getReason()), "履约状态已变化"); + if (request.getAttachmentList() != null && !request.getAttachmentList().isEmpty()) { + attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, fulfillment.getId(), request.getAttachmentList()); + } + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment verify(GaoOrderFulfillmentActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_VERIFY", request, () -> verifyInternal(request)); + } + + private GaoOrderFulfillment verifyInternal(GaoOrderFulfillmentActionRequest request) { + TimiException.required(request, "not found verify request"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId()); + TimiException.required(snapshot, "not found fulfillment"); + GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId()); + List sourceList = loadSources(order, itemRequests(snapshot.getId()), false); + GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态"); + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约"); + salesOrderService.checkFulfillmentPermission(order); + List fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); + TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); + assertReserved(sourceList); + for (SourceEntry source : sourceList) { + decreaseReserved(source); + increaseFulfilled(source); + } + List stockItems = buildStockItems(order.getStoreId(), sourceList); + applySkuSpecSnapshots(stockItems, fulfillmentItemList); + String verifierUserId = userLoginService.getRequireLoginUserId(); + if (!stockItems.isEmpty()) { + productStockService.outForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "OUT"), verifierUserId); + } + TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), GaoOrderFulfillment.Status.VERIFIED, + verifierUserId, Time.now(), request.getReason()), "履约状态已变化"); + salesOrderService.refreshFulfillmentStatus(order); + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment reject(GaoOrderFulfillmentActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REJECT", request, () -> releaseInternal(request, GaoOrderFulfillment.Status.REJECTED)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment cancel(GaoOrderFulfillmentActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CANCEL", request, () -> cancelInternal(request)); + } + + private GaoOrderFulfillment cancelInternal(GaoOrderFulfillmentActionRequest request) { + TimiException.required(request, "not found cancel request"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + GaoOrderFulfillment fulfillment = mapper.selectById(request.getFulfillmentId()); + TimiException.required(fulfillment, "not found fulfillment"); + String loginUserId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(loginUserId.equals(fulfillment.getOperatorUserId()), "只能撤销自己提交的履约"); + return releaseInternal(request, GaoOrderFulfillment.Status.CANCELLED); + } + + private GaoOrderFulfillment releaseInternal(GaoOrderFulfillmentActionRequest request, GaoOrderFulfillment.Status targetStatus) { + TimiException.required(request, "not found action request"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + String reason = requiredText(request.getReason(), "拒绝或撤销原因不能为空"); + GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId()); + TimiException.required(snapshot, "not found fulfillment"); + GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId()); + List sourceList = loadSources(order, itemRequests(snapshot.getId()), false); + GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK + || fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是可释放状态"); + List fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); + TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); + if (fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING) { + List stockItems = buildStockItems(order.getStoreId(), sourceList); + applySkuSpecSnapshots(stockItems, fulfillmentItemList); + if (!stockItems.isEmpty()) { + productStockService.releaseForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "RELEASE"), userLoginService.getRequireLoginUserId()); + } + } + for (SourceEntry source : sourceList) { + decreaseReserved(source); + } + String operatorUserId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), targetStatus, operatorUserId, Time.now(), reason), "履约状态已变化"); + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment revoke(GaoOrderFulfillmentRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REVOKE", request, () -> revokeInternal(request)); + } + + private GaoOrderFulfillment revokeInternal(GaoOrderFulfillmentRevokeRequest request) { + TimiException.required(request, "not found revoke request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + String reason = requiredText(request.getReason(), "冲正原因不能为空"); + GaoOrderFulfillment sourceSnapshot = mapper.selectById(request.getFulfillmentId()); + TimiException.required(sourceSnapshot, "not found fulfillment"); + GaoOrderFulfillment existing = mapper.selectByRequestNo(sourceSnapshot.getStoreId(), requestNo); + if (existing != null) { + TimiException.requiredTrue(sourceSnapshot.getId().equals(existing.getSourceFulfillmentId()), "requestNo 已被其他履约操作使用"); + return detail(existing.getId()); + } + GaoSalesOrder order = salesOrderService.getForUpdate(sourceSnapshot.getSalesOrderId()); + List sourceItemList = itemMapper.selectByFulfillmentId(sourceSnapshot.getId()); + List sourceList = loadSources(order, copyRequestItems(sourceItemList), false); + GaoOrderFulfillment source = lockFulfillment(sourceSnapshot.getId(), order); + storeScopeService.checkStoreId(source.getStoreId()); + TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正"); + TimiException.requiredNull(mapper.selectBySourceFulfillmentId(source.getId()), "该履约单已经冲正"); + sourceItemList = itemMapper.selectByFulfillmentIdForUpdate(source.getId()); + TimiException.requiredTrue(!sourceItemList.isEmpty(), "履约单没有明细"); + List stockItems = buildStockItems(order.getStoreId(), sourceList); + if (!stockItems.isEmpty()) { + TimiException.requiredTrue(Boolean.TRUE.equals(request.getReturnedToStore()), "实物履约冲正必须确认已退回门店"); + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "实物履约冲正必须上传退回照片"); + } + String operatorUserId = userLoginService.getRequireLoginUserId(); + GaoOrderFulfillment reverse = newFulfillment(order, GaoOrderFulfillment.Status.VERIFIED, + requestNo, operatorUserId, TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()), request.getRemark()); + reverse.setVerifierUserId(operatorUserId); + reverse.setVerifiedAt(Time.now()); + reverse.setSourceFulfillmentId(source.getId()); + reverse.setReason(reason); + super.create(reverse); + Map sourceItemMap = new HashMap<>(); + for (GaoOrderFulfillmentItem sourceItem : sourceItemList) { + sourceItemMap.put(sourceItem.getSourceOrderItemId(), sourceItem); + } + for (SourceEntry sourceEntry : sourceList) { + decreaseFulfilled(sourceEntry); + GaoOrderFulfillmentItem sourceItem = sourceItemMap.get(sourceEntry.sourceId()); + TimiException.required(sourceItem, "履约明细来源订单项不存在"); + GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem); + reverseItem.setFulfillmentId(reverse.getId()); + reverseItem.setReservedBefore(sourceEntry.reservedBefore()); + reverseItem.setReservedAfter(sourceEntry.reservedBefore()); + reverseItem.setFulfilledBefore(sourceEntry.fulfilledBefore()); + reverseItem.setFulfilledAfter(sourceEntry.fulfilledBefore().subtract(sourceEntry.quantity())); + itemMapper.insert(reverseItem); + } + if (!stockItems.isEmpty()) { + for (GaoStockChangeItem item : stockItems) { + GaoProductStockRecord stockRecord = stockRecordService.getByBizSku(GaoProductStockRecord.BizType.ORDER_FULFILLMENT, + source.getId(), item.getSkuId(), GaoProductStockRecord.OperationType.ORDER_OUT); + if (stockRecord != null) { + item.setSourceRecordId(stockRecord.getId()); + item.setSkuSpecSnapshot(stockRecord.getSkuSpecSnapshot()); + } + } + productStockService.revokeForFulfillment(order.getStoreId(), reverse.getId(), stockItems, + stockRequestNo(reverse, "REVOKE"), operatorUserId); + } + TimiException.requiredTrue(1 == mapper.updateStatus(source.getId(), GaoOrderFulfillment.Status.REVOKED, + operatorUserId, Time.now(), reason), "原履约状态已变化"); + attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, reverse.getId(), request.getAttachmentList()); + salesOrderService.refreshFulfillmentStatus(order); + return detail(reverse.getId()); + } + + private GaoOrderFulfillment newFulfillment(GaoSalesOrder order, GaoOrderFulfillment.Status status, + String requestNo, String operatorUserId, Long deliveredAt, String remark) { + GaoOrderFulfillment result = new GaoOrderFulfillment(); + result.setSalesOrderId(order.getId()); + result.setStoreId(order.getStoreId()); + result.setCustomerId(order.getCustomerId()); + result.setStatus(status); + result.setRequestNo(requestNo); + result.setOperatorUserId(operatorUserId); + result.setDeliveredAt(deliveredAt); + result.setRemark(remark); + return result; + } + + private void insertSourceReservations(GaoOrderFulfillment fulfillment, List sourceList) { + for (SourceEntry source : sourceList) { + TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "订单明细占用失败"); + GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId()); + itemMapper.insert(item); + } + } + + private void decreaseReserved(SourceEntry source) { + TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "订单明细占用释放失败"); + } + + private void increaseFulfilled(SourceEntry source) { + TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "订单明细履约失败"); + } + + private void decreaseFulfilled(SourceEntry source) { + TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正"); + TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败"); + } + + private void assertReserved(List sourceList) { + for (SourceEntry source : sourceList) { + TimiException.requiredTrue(0 <= source.reservedBefore().compareTo(source.quantity()), "履约待占用数量不足"); + } + } + + private List loadSources(GaoSalesOrder order, List requestItems, boolean checkAvailable) { + TimiException.requiredTrue(requestItems != null && !requestItems.isEmpty(), "履约至少需要一个产品"); + List sortedRequestItems = new ArrayList<>(requestItems); + Set sourceIdSet = new HashSet<>(); + for (GaoOrderFulfillmentItemRequest requestItem : sortedRequestItems) { + TimiException.required(requestItem, "履约明细不能为空"); + String sourceId = requiredText(requestItem.getSourceOrderItemId(), "履约明细来源订单项不能为空"); + requestItem.setSourceOrderItemId(sourceId); + TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细"); + TimiException.required(requestItem.getQuantity(), "履约数量不能为空"); + TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0"); + TimiException.requiredTrue(3 >= requestItem.getQuantity().stripTrailingZeros().scale(), "履约数量最多保留三位小数"); + } + sortedRequestItems.sort(Comparator.comparing(GaoOrderFulfillmentItemRequest::getSourceOrderItemId)); + List result = new ArrayList<>(); + for (GaoOrderFulfillmentItemRequest requestItem : sortedRequestItems) { + GaoSalesOrderItem source = salesOrderService.getItemForUpdate(requestItem.getSourceOrderItemId()); + TimiException.required(source, "not found sales order item"); + TimiException.requiredTrue(order.getId().equals(source.getSalesOrderId()), "订单明细不属于当前订单"); + GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE + ? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT; + TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配"); + result.add(toSource(order, source, requestItem.getQuantity(), checkAvailable)); + } + return result; + } + + private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) { + GaoProductSku sku = skuService.get(source.getSkuId()); + TimiException.required(sku, "履约来源 SKU 不存在"); + GaoProduct product = productService.get(sku.getProductId()); + TimiException.required(product, "履约来源产品不存在"); + TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店"); + BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot()); + GaoProductSkuUnit unit = snapshotUnit(source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit); + JsonNode skuSpecSnapshot = source.getSkuSpecSnapshot(); + return buildSource(order, source.getId(), source.getSkuId(), source.getSkuUnitId(), source.getProductIdSnapshot(), source.getProductNameSnapshot(), source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), skuSpecSnapshot, + source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(), + product, sku, unit, checkAvailable); + } + + private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String skuId, String unitId, String productId, + String productName, String skuCode, String skuName, String specSummary, JsonNode skuSpecSnapshot, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity, + BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit, + boolean checkAvailable) { + if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + } + BigDecimal reservedBefore = decimal(reservedQuantity); + BigDecimal fulfilledBefore = decimal(fulfilledQuantity); + if (checkAvailable) { + BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore); + TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足"); + } + return new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, skuSpecSnapshot, unitName, baseQuantityPerUnit, + quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit); + } + + private List buildStockItems(String storeId, List sourceList) { + Map itemMap = new LinkedHashMap<>(); + for (SourceEntry source : sourceList) { + if (!Boolean.TRUE.equals(source.sku().getStockEnabled())) { + continue; + } + GaoProductSkuUnit baseUnit = skuService.listUnitBySkuId(source.sku().getId()).stream() + .filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null); + TimiException.required(baseUnit, "实物产品缺少基础库存单位"); + BigDecimal baseQuantity = source.quantity().multiply(source.unit().getBaseQuantityPerUnit()); + GaoStockChangeItem item = itemMap.get(source.sku().getId()); + if (item == null) { + item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity); + item.setSkuSpecSnapshot(source.skuSpecSnapshot()); + itemMap.put(source.sku().getId(), item); + } else { + item.setQuantity(item.getQuantity().add(baseQuantity)); + item.setBaseQuantity(item.getBaseQuantity().add(baseQuantity)); + } + } + return itemMap.values().stream().sorted(Comparator.comparing(GaoStockChangeItem::getSkuId)).toList(); + } + + private GaoOrderFulfillment lockFulfillment(String fulfillmentId, GaoSalesOrder order) { + GaoOrderFulfillment result = mapper.selectForUpdate(fulfillmentId); + TimiException.required(result, "not found fulfillment"); + TimiException.requiredTrue(order.getId().equals(result.getSalesOrderId()), "履约单不属于当前订单"); + return result; + } + + private void applySkuSpecSnapshots(List stockItems, List fulfillmentItemList) { + if (stockItems == null || stockItems.isEmpty() || fulfillmentItemList == null || fulfillmentItemList.isEmpty()) { + return; + } + Map snapshotMap = new HashMap<>(); + for (GaoOrderFulfillmentItem item : fulfillmentItemList) { + if (item.getSkuId() != null) { + snapshotMap.put(item.getSkuId(), item.getSkuSpecSnapshot()); + } + } + for (GaoStockChangeItem item : stockItems) { + JsonNode snapshot = snapshotMap.get(item.getSkuId()); + if (snapshot != null) { + item.setSkuSpecSnapshot(snapshot); + } + } + } + + private List itemRequests(String fulfillmentId) { + return copyRequestItems(itemMapper.selectByFulfillmentId(fulfillmentId)); + } + + private List copyRequestItems(List itemList) { + List result = new ArrayList<>(); + for (GaoOrderFulfillmentItem item : TimiJava.safeIterable(itemList)) { + GaoOrderFulfillmentItemRequest request = new GaoOrderFulfillmentItemRequest(); + request.setSourceOrderItemId(item.getSourceOrderItemId()); + request.setQuantity(item.getQuantity()); + result.add(request); + } + return result; + } + + private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) { + GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); + result.setSkuId(source.getSkuId()); + result.setSkuUnitId(source.getSkuUnitId()); + result.setProductIdSnapshot(source.getProductIdSnapshot()); + result.setSourceOrderItemId(source.getSourceOrderItemId()); + result.setQuantity(source.getQuantity()); + result.setBaseQuantity(source.getBaseQuantity()); + result.setProductNameSnapshot(source.getProductNameSnapshot()); + result.setSkuCodeSnapshot(source.getSkuCodeSnapshot()); + result.setSkuNameSnapshot(source.getSkuNameSnapshot()); + result.setSpecSummarySnapshot(source.getSpecSummarySnapshot()); + result.setSkuSpecSnapshot(source.getSkuSpecSnapshot()); + result.setUnitSnapshot(source.getUnitSnapshot()); + result.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); + return result; + } + + private GaoProductSkuUnit snapshotUnit(String id, String name, BigDecimal baseQuantityPerUnit) { + GaoProductSkuUnit unit = new GaoProductSkuUnit(); + unit.setId(id); + unit.setName(name); + unit.setBaseQuantityPerUnit(baseQuantityPerUnit); + return unit; + } + + private BigDecimal requiredBaseQuantityPerUnit(BigDecimal value) { + TimiException.required(value, "履约来源缺少基础单位数量系数快照"); + TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效"); + return value; + } + + private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) { + TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId()) + && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) + && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应履约请求内容不一致"); + List existingItems = itemMapper.selectByFulfillmentId(existing.getId()); + List requestItems = request.getItemList(); + TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致"); + Map requestMap = new HashMap<>(); + for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) { + requestMap.put(item.getSourceOrderItemId(), item.getQuantity()); + } + for (GaoOrderFulfillmentItem item : existingItems) { + TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getSourceOrderItemId(), ZERO)) == 0, "requestNo 对应履约请求内容不一致"); + } + } + + private String stockRequestNo(GaoOrderFulfillment fulfillment, String operation) { + return "FULFILLMENT-%s-%s".formatted(fulfillment.getId(), operation); + } + + private boolean hasAttachment(List attachmentList) { + for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } + + private BigDecimal decimal(BigDecimal value) { + return value == null ? ZERO : value; + } + + private void fillItemList(List list) { + if (list == null || list.isEmpty()) { + return; + } + List fulfillmentIdList = list.stream() + .map(GaoOrderFulfillment::getId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (fulfillmentIdList.isEmpty()) { + for (GaoOrderFulfillment fulfillment : list) { + fulfillment.setItemList(List.of()); + } + return; + } + Map> itemMap = new HashMap<>(); + List queriedItemList = itemMapper.selectByFulfillmentIdList(fulfillmentIdList); + for (GaoOrderFulfillmentItem item : queriedItemList) { + itemMap.computeIfAbsent(item.getFulfillmentId(), key -> new ArrayList<>()).add(item); + } + List itemList = new ArrayList<>(); + for (GaoOrderFulfillment fulfillment : list) { + List fulfillmentItemList = itemMap.getOrDefault(fulfillment.getId(), List.of()); + fulfillment.setItemList(fulfillmentItemList); + itemList.addAll(fulfillmentItemList); + } + fillItemAttachments(itemList); + } + + private void fillItemAttachments(List itemList) { + if (itemList == null || itemList.isEmpty()) { + return; + } + List skuIdList = itemList.stream() + .map(GaoOrderFulfillmentItem::getSkuId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (skuIdList.isEmpty()) { + return; + } + Map> attachmentMap = attachmentService.mapByBizIdList( + Attachment.BizType.GAO_PRODUCT_SKU, + skuIdList); + for (GaoOrderFulfillmentItem item : itemList) { + item.setAttachmentList(attachmentMap.getOrDefault(item.getSkuId(), List.of())); + } + } + + private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot, + String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, JsonNode skuSpecSnapshot, + String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity, + BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, GaoProductSku sku, + GaoProductSkuUnit unit) { + + private String sourceId() { + return sourceOrderItemId; + } + + private GaoOrderFulfillmentItem toFulfillmentItem(String fulfillmentId) { + GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); + result.setFulfillmentId(fulfillmentId); + result.setSkuId(skuId); + result.setSkuUnitId(skuUnitId); + result.setProductIdSnapshot(productIdSnapshot); + result.setSourceOrderItemId(sourceOrderItemId); + result.setQuantity(quantity); + result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3)); + result.setReservedBefore(reservedBefore); + result.setReservedAfter(reservedBefore.add(quantity)); + result.setFulfilledBefore(fulfilledBefore); + result.setFulfilledAfter(fulfilledBefore); + result.setProductNameSnapshot(productNameSnapshot); + result.setSkuCodeSnapshot(skuCodeSnapshot); + result.setSkuNameSnapshot(skuNameSnapshot); + result.setSpecSummarySnapshot(specSummarySnapshot); + result.setSkuSpecSnapshot(skuSpecSnapshot); + result.setUnitSnapshot(unitSnapshot); + result.setBaseQuantityPerUnitSnapshot(baseQuantityPerUnitSnapshot); + return result; + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java new file mode 100644 index 0000000..c1dfd6b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java @@ -0,0 +1,293 @@ +package com.imyeyu.api.modules.gao.service.implement; + +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.GaoFundRecord; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +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.vo.GaoOrderPaymentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +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-02 +@Service +@RequiredArgsConstructor +public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService { + + private final GaoFundRecordService fundRecordService; + private final GaoSalesOrderService orderService; + private final GaoPointTriggerService pointTriggerService; + private final AttachmentService attachmentService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord receive(GaoOrderPaymentReceiveRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_RECEIVE", request, () -> receiveInternal(request)); + } + + private GaoFundRecord receiveInternal(GaoOrderPaymentReceiveRequest request) { + TimiException.required(request, "not found order payment request"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + TimiException.required(request.getAmount(), "收款金额不能为空"); + TimiException.requiredTrue(0 < request.getAmount(), "收款金额必须大于 0"); + GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId()); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + verifySameReceive(existing, order, request); + return existing; + } + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能收款"); + TimiException.requiredTrue(order.getRefundStatus() != GaoSalesOrder.RefundStatus.REFUNDED, "已完成退款订单不能收款"); + long pending = safe(order.getPendingPaidAmount()); + long paid = safe(order.getPaidAmount()); + TimiException.requiredTrue(pending + paid + request.getAmount() <= safe(order.getSaleAmount()), "累计收款不能超过成交价"); + TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "收款必须上传照片"); + GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.RECEIVE, requestNo, request.getAmount(), request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.PENDING); + fundRecordService.createByOrderPayment(record); + order.setPendingPaidAmount(pending + request.getAmount()); + orderService.recomputePaymentStatus(order); + orderService.updatePayment(order); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord verify(String fundRecordId) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_VERIFY", fundRecordId, () -> { + GaoFundRecord record = lockOrderRecord(fundRecordId); + TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); + GaoSalesOrder order = orderService.getForUpdate(record.getBizId()); + Long paidAtBefore = order.getPaidAt(); + order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount()); + order.setPaidAmount(safe(order.getPaidAmount()) + record.getAmount()); + orderService.recomputePaymentStatus(order); + String userId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化"); + orderService.updatePayment(order); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setVerifierUserId(userId); + record.setVerifiedAt(Time.now()); + if (paidAtBefore == null && order.getPaidAt() != null) { + // 订单首次全额实收,写入消费事实并触发订单积分 + pointTriggerService.onSalesOrderPaid(order); + } + return record; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord reject(GaoOrderPaymentActionRequest request) { + return releasePending(request, GaoFundRecord.Status.REJECTED, false); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord cancel(GaoOrderPaymentActionRequest request) { + return releasePending(request, GaoFundRecord.Status.CANCELLED, true); + } + + private GaoFundRecord releasePending(GaoOrderPaymentActionRequest request, GaoFundRecord.Status targetStatus, boolean onlyOperator) { + TimiException.required(request, "not found order payment action request"); + TimiException.required(request.getFundRecordId(), "not found fundRecordId"); + String reason = requiredText(request.getReason(), "资金操作原因不能为空"); + GaoFundRecord record = lockOrderRecord(request.getFundRecordId()); + TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); + if (onlyOperator) { + TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(record.getOperatorUserId()), "只能撤销自己提交的收款"); + } + GaoSalesOrder order = orderService.getForUpdate(record.getBizId()); + order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount()); + orderService.recomputePaymentStatus(order); + String userId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, targetStatus, userId, Time.now(), reason), "收款记录状态已变化"); + orderService.updatePayment(order); + record.setStatus(targetStatus); + record.setReason(reason); + record.setVerifierUserId(userId); + record.setVerifiedAt(Time.now()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord refund(GaoOrderPaymentRefundRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_REFUND", request, () -> refundInternal(request)); + } + + private GaoFundRecord refundInternal(GaoOrderPaymentRefundRequest request) { + TimiException.required(request, "not found refund request"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + TimiException.required(request.getAmount(), "退款金额不能为空"); + TimiException.requiredTrue(0 < request.getAmount(), "退款金额必须大于 0"); + String reason = requiredText(request.getReason(), "退款原因不能为空"); + GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId()); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getBizId().equals(order.getId()) && existing.getOperationType() == GaoFundRecord.OperationType.REFUND && existing.getAmount().equals(request.getAmount()), "requestNo 已被其他资金操作使用"); + return existing; + } + TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.CANCELLED, "只有已取消订单可以退款"); + TimiException.requiredTrue(order.getRefundStatus() == GaoSalesOrder.RefundStatus.PENDING, "当前订单不在待退款状态"); + long refundableAmount = safe(order.getPaidAmount()) - safe(order.getRefundedAmount()); + TimiException.requiredTrue(request.getAmount() <= refundableAmount, "退款金额不能超过有效收款余额"); + TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "退款必须上传照片"); + GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.REFUND, requestNo, request.getAmount(), request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setReason(reason); + fundRecordService.createByOrderPayment(record); + long refundedAmount = safe(order.getRefundedAmount()) + request.getAmount(); + order.setRefundedAmount(refundedAmount); + order.setRefundStatus(refundedAmount == safe(order.getPaidAmount()) + ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING); + orderService.updatePayment(order); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord confirmNoRefund(GaoOrderPaymentNoRefundRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CONFIRM_NO_REFUND", request, () -> { + TimiException.required(request, "not found no refund request"); + GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId()); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String reason = requiredText(request.getReason(), "确认无需退款原因不能为空"); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + return existing; + } + TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.CANCELLED + && order.getRefundStatus() == GaoSalesOrder.RefundStatus.PENDING + && safe(order.getRefundedAmount()) == 0, "当前订单不在待确认退款状态"); + GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.CONFIRM_NO_REFUND, requestNo, 0L, request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setReason(reason); + fundRecordService.createByOrderPayment(record); + order.setRefundStatus(GaoSalesOrder.RefundStatus.REFUNDED); + orderService.updatePayment(order); + return record; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord revoke(GaoOrderPaymentRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_FUND_REVOKE", request, () -> revokeInternal(request)); + } + + private GaoFundRecord revokeInternal(GaoOrderPaymentRevokeRequest request) { + TimiException.required(request, "not found fund revoke request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String reason = requiredText(request.getReason(), "资金冲正原因不能为空"); + GaoFundRecord source = lockOrderRecord(request.getFundRecordId()); + GaoSalesOrder order = orderService.getForUpdate(source.getBizId()); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用"); + return existing; + } + TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正"); + TimiException.requiredNull(fundRecordService.getRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正"); + Long paidAtBefore = order.getPaidAt(); + if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) { + order.setPaidAmount(safe(order.getPaidAmount()) - source.getAmount()); + orderService.recomputePaymentStatus(order); + } else if (source.getOperationType() == GaoFundRecord.OperationType.REFUND) { + order.setRefundedAmount(safe(order.getRefundedAmount()) - source.getAmount()); + order.setRefundStatus(GaoSalesOrder.RefundStatus.PENDING); + } else { + order.setRefundStatus(GaoSalesOrder.RefundStatus.PENDING); + } + GaoFundRecord revoke = newRecord(order, GaoFundRecord.OperationType.REVOKE, requestNo, source.getAmount(), request.getOccurredAt(), request.getRemark()); + revoke.setStatus(GaoFundRecord.Status.VERIFIED); + revoke.setSourceRecordId(source.getId()); + revoke.setReason(reason); + fundRecordService.createByOrderPayment(revoke); + orderService.updatePayment(order); + if (paidAtBefore != null && order.getPaymentStatus() != GaoSalesOrder.PaymentStatus.PAID) { + // 已全额实收的收款被冲正,撤销消费事实及其依赖积分 + pointTriggerService.revokeBySalesOrder(order, reason); + } + TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "资金冲正必须上传照片"); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList()); + return revoke; + } + + private GaoFundRecord lockOrderRecord(String id) { + GaoFundRecord record = fundRecordService.getForUpdate(id); + TimiException.required(record, "not found fund record"); + TimiException.requiredTrue(record.getBizType() == GaoFundRecord.BizType.SALES_ORDER, "资金记录不是销售订单资金"); + return record; + } + + private GaoFundRecord newRecord(GaoSalesOrder order, GaoFundRecord.OperationType operationType, String requestNo, Long amount, Long occurredAt, String remark) { + GaoFundRecord record = new GaoFundRecord(); + record.setStoreId(order.getStoreId()); + record.setCustomerId(order.getCustomerId()); + record.setBizType(GaoFundRecord.BizType.SALES_ORDER); + record.setBizId(order.getId()); + record.setOperationType(operationType); + record.setAmount(amount); + record.setSalesOrderUnpaidAmountSnapshot(safe(order.getSaleAmount()) - safe(order.getPaidAmount())); + record.setRequestNo(requestNo); + record.setOperatorUserId(userLoginService.getRequireLoginUserId()); + record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now())); + record.setRemark(remark); + return record; + } + + private boolean hasPhoto(List attachmentList) { + for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private void verifySameReceive(GaoFundRecord existing, GaoSalesOrder order, GaoOrderPaymentReceiveRequest request) { + TimiException.requiredTrue(existing.getBizType() == GaoFundRecord.BizType.SALES_ORDER + && existing.getBizId().equals(order.getId()) + && existing.getOperationType() == GaoFundRecord.OperationType.RECEIVE + && existing.getAmount().equals(request.getAmount()), "requestNo 已被其他资金操作使用"); + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private long safe(Long value) { + return value == null ? 0L : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderReminderServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderReminderServiceImplement.java new file mode 100644 index 0000000..0db3907 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderReminderServiceImplement.java @@ -0,0 +1,174 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.imyeyu.api.modules.common.entity.Notify; +import com.imyeyu.api.modules.common.entity.NotifyDetail; +import com.imyeyu.api.modules.common.service.NotifyService; +import com.imyeyu.api.modules.gao.entity.GaoUser; +import com.imyeyu.api.modules.gao.mapper.GaoOrderReminderMapper; +import com.imyeyu.api.modules.gao.mapper.GaoUserMapper; +import com.imyeyu.api.modules.gao.service.GaoOrderReminderService; +import com.imyeyu.api.modules.gao.vo.GaoOrderReminderCustomer; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/// GAO 订单提醒服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoOrderReminderServiceImplement implements GaoOrderReminderService { + + private static final String NOTIFY_BIZ_TYPE = "GAO_ORDER_REMINDER"; + private static final String SMS_TEMPLATE_ID = "2725097"; + private static final String ZONE_ID = "Asia/Shanghai"; + + private final GaoOrderReminderMapper mapper; + private final GaoUserMapper gaoUserMapper; + private final UserService userService; + private final NotifyService notifyService; + private final ObjectMapper jackson; + + @Override + public void sendOrderReceiveReminder() { + send(mapper.selectOrderReceiveCustomers(), ReminderType.ORDER_RECEIVE); + } + + @Override + public void sendVerifyReminder() { + Map> storeCustomerMap = new LinkedHashMap<>(); + Map> storeActionMap = new LinkedHashMap<>(); + mergeCustomers(storeCustomerMap, storeActionMap, mapper.selectReceiveVerifyCustomers(), "核销收款"); + mergeCustomers(storeCustomerMap, storeActionMap, mapper.selectFulfillmentVerifyCustomers(), "核销履约"); + for (Map.Entry> entry : storeCustomerMap.entrySet()) { + String action = String.join("、", storeActionMap.get(entry.getKey())); + sendStoreReminder(entry.getKey(), new ArrayList<>(entry.getValue().values()), ReminderType.VERIFY.name(), action); + } + } + + private void mergeCustomers(Map> storeCustomerMap, + Map> storeActionMap, List customerList, String action) { + if (customerList == null || customerList.isEmpty()) { + return; + } + for (GaoOrderReminderCustomer customer : customerList) { + storeCustomerMap.computeIfAbsent(customer.getStoreId(), key -> new LinkedHashMap<>()) + .putIfAbsent(customer.getCustomerId(), customer); + storeActionMap.computeIfAbsent(customer.getStoreId(), key -> new LinkedHashSet<>()).add(action); + } + } + + private void send(List customerList, ReminderType reminderType) { + if (customerList == null || customerList.isEmpty()) { + return; + } + Map> storeCustomerMap = customerList.stream() + .collect(Collectors.groupingBy(GaoOrderReminderCustomer::getStoreId, LinkedHashMap::new, Collectors.toList())); + for (Map.Entry> entry : storeCustomerMap.entrySet()) { + sendStoreReminder(entry.getKey(), entry.getValue(), reminderType.name(), reminderType.label); + } + } + + private void sendStoreReminder(String storeId, List customerList, String reminderType, + String action) { + String customerNames = formatCustomerNames(customerList); + String content = "您的客户 %s 的订单需要 %s,请留意订单状态。".formatted(customerNames, action); + List managerList = gaoUserMapper.selectEnabledStoreManagers(storeId); + if (managerList == null || managerList.isEmpty()) { + log.warn("GAO 订单提醒未找到启用中的店长: storeId[{}], reminderType[{}]", storeId, reminderType); + return; + } + List userIdList = managerList.stream().map(GaoUser::getUserId).distinct().toList(); + Map userMap = userService.mapByIdList(userIdList); + for (GaoUser manager : managerList) { + try { + User user = userMap.get(manager.getUserId()); + if (user == null) { + log.warn("GAO 订单提醒跳过不存在的店长用户: storeId[{}], userId[{}]", storeId, manager.getUserId()); + continue; + } + String remindedId = "%s:%s:%s:%s".formatted(LocalDate.now(ZoneId.of(ZONE_ID)), storeId, manager.getUserId(), reminderType); + if (notifyService.existsByBizReminded(NOTIFY_BIZ_TYPE, reminderType, remindedId)) { + continue; + } + long sendAt = Time.now(); + Notify notify = new Notify(); + notify.setBizType(NOTIFY_BIZ_TYPE); + notify.setBizRemindedType(reminderType); + notify.setBizRemindedId(remindedId); + notify.setDetailList(List.of( + createInternalDetail(manager.getUserId(), content, sendAt), + createSmsDetail(user.getPhoneNo(), customerNames, action, content, sendAt) + )); + notifyService.create(notify); + } catch (Exception e) { + log.error("GAO 订单提醒发送失败: storeId[{}], userId[{}], reminderType[{}]", storeId, manager.getUserId(), reminderType, e); + } + } + } + + private NotifyDetail createInternalDetail(String userId, String content, long sendAt) { + NotifyDetail detail = new NotifyDetail(); + detail.setMsgType(NotifyDetail.MsgType.INTERNAL); + detail.setTemplateId(SMS_TEMPLATE_ID); + detail.setSubject("订单状态提醒"); + detail.setContent(content); + detail.setContentType(NotifyDetail.ContentType.TEXT); + detail.setSendTo(userId); + detail.setSendAt(sendAt); + return detail; + } + + private NotifyDetail createSmsDetail(String phoneNo, String customerNames, String action, String content, long sendAt) { + NotifyDetail detail = new NotifyDetail(); + detail.setMsgType(NotifyDetail.MsgType.SMS); + detail.setTemplateId(SMS_TEMPLATE_ID); + detail.setContent(content); + detail.setArgs(jackson.valueToTree(List.of(customerNames, action))); + detail.setSendTo(phoneNo); + detail.setSendAt(sendAt); + return detail; + } + + private String formatCustomerNames(List customerList) { + List nameList = customerList.stream() + .map(GaoOrderReminderCustomer::getCustomerName) + .map(name -> name == null || name.isBlank() ? "未知客户" : name.trim()) + .toList(); + if (customerList.size() <= 2) { + return String.join("、", nameList); + } + return "%s、%s等 %d 位".formatted(nameList.getFirst(), nameList.get(1), customerList.size()); + } + + private enum ReminderType { + + /// 订单收款 + ORDER_RECEIVE("收款"), + + /// 统一核销 + VERIFY("核销收款、核销履约"); + + private final String label; + + ReminderType(String label) { + this.label = label; + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java index a36f0f6..66a06d9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java @@ -3,13 +3,17 @@ package com.imyeyu.api.modules.gao.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper; -import com.imyeyu.api.modules.gao.mapper.GaoEventMapper; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.timi.TimiException; @@ -38,7 +42,7 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService { GaoPackageConfig config = get(id); TimiException.required(config, "not found package config"); - TimiException.requiredTrue(0 == eventMapper.countByPackageConfigId(id), "套餐配置已绑定购买事件,不能删除"); super.delete(id); return id; }); @@ -169,26 +172,40 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService itemList) { + private void verifyItems(String storeId, List itemList) { TimiException.requiredTrue(!itemList.isEmpty(), "套餐至少配置一个产品"); + TimiException.required(storeId, "not found packageConfig.storeId"); Set keySet = new HashSet<>(); for (int i = 0; i < itemList.size(); i++) { GaoPackageConfigItem item = itemList.get(i); TimiException.required(item, "套餐产品明细不能为空"); - String name = trimRequired(item.getProductName(), "产品名称不能为空"); - String unit = trimRequired(item.getUnit(), "产品单位不能为空"); + TimiException.required(item.getSkuId(), "SKU 不能为空"); + TimiException.required(item.getSkuUnitId(), "SKU 单位不能为空"); + GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId()); + GaoProduct product = selection.getProduct(); + GaoProductSku sku = selection.getSku(); + GaoProductSkuUnit unitEntity = selection.getUnit(); BigDecimal quantity = item.getDefaultQuantity(); TimiException.required(quantity, "产品默认数量不能为空"); TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0"); - TimiException.requiredTrue(3 >= quantity.scale(), "产品数量最多保留三位小数"); - String key = name + "|" + unit; - TimiException.requiredTrue(keySet.add(key), "同一套餐内产品名称和单位不能重复"); - item.setProductName(name); - item.setUnit(unit); + TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "产品数量最多保留三位小数"); + if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该 SKU 不允许小数数量"); + } + quantity.multiply(unitEntity.getBaseQuantityPerUnit()); + String key = item.getSkuId() + "|" + item.getSkuUnitId(); + TimiException.requiredTrue(keySet.add(key), "同一套餐内 SKU 和单位不能重复"); + item.setProductNameSnapshot(product.getName()); + item.setProductIdSnapshot(product.getId()); + item.setSkuCodeSnapshot(sku.getCode()); + item.setSkuNameSnapshot(sku.getName()); + item.setSpecSummarySnapshot(sku.getSpecSummary()); + item.setUnitSnapshot(unitEntity.getName()); + item.setBaseQuantityPerUnitSnapshot(unitEntity.getBaseQuantityPerUnit()); item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); } } @@ -197,8 +214,15 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService implements GaoPackageDeliveryService { - - private static final BigDecimal ZERO = BigDecimal.ZERO; - - private final GaoPackageDeliveryMapper mapper; - private final GaoPackageDeliveryItemMapper itemMapper; - private final GaoCustomerPackageService customerPackageService; - private final AttachmentService attachmentService; - private final GaoStoreScopeService storeScopeService; - private final RoleChecker roleChecker; - private final UserLoginService userLoginService; - private final LoggerService loggerService; - - @Override - protected BaseMapper mapper() { - return mapper; - } - - /// 发放流水必须通过专用操作创建 - /// - /// @param delivery 发放流水 - @Override - public void create(GaoPackageDelivery delivery) { - throw new TimiException(TimiCode.ERROR, "发放流水必须通过专用接口创建"); - } - - /// 发放流水不允许通用修改 - /// - /// @param delivery 发放流水 - @Override - public void update(GaoPackageDelivery delivery) { - throw new TimiException(TimiCode.ERROR, "发放流水不允许通用修改"); - } - - /// 发放流水不允许删除 - /// - /// @param id 发放流水 ID - @Override - public void delete(String id) { - throw new TimiException(TimiCode.ERROR, "发放流水不允许删除"); - } - - /// 发放流水不允许物理删除 - /// - /// @param id 发放流水 ID - @Override - public void destroy(String id) { - throw new TimiException(TimiCode.ERROR, "发放流水不允许物理删除"); - } - - @Override - public PageResult pageByQuery(GaoPackageDeliveryPage page) { - TimiException.required(page, "not found page"); - PageResult result = new PageResult<>(); - result.setTotal(mapper.countByQuery(page)); - result.setList(mapper.selectByQuery(page)); - return result; - } - - @Override - public List listByCustomerPackageId(String customerPackageId) { - TimiException.required(customerPackageId, "not found customerPackageId"); - return mapper.selectByCustomerPackageId(customerPackageId); - } - - @Override - public Map> mapItemListByDeliveryIdList(Collection deliveryIdList) { - if (deliveryIdList == null || deliveryIdList.isEmpty()) { - return Map.of(); - } - return itemMapper.selectByDeliveryIdList(deliveryIdList).stream() - .collect(java.util.stream.Collectors.groupingBy(GaoPackageDeliveryItem::getDeliveryId)); - } - - @Override - public long countPendingByCustomerPackageId(String customerPackageId) { - return mapper.countPendingByCustomerPackageId(customerPackageId); - } - - @Override - public long countVerifiedByCustomerPackageId(String customerPackageId) { - return mapper.countVerifiedByCustomerPackageId(customerPackageId); - } - - @Override - public GaoPackageDelivery get(String id) { - return super.get(id); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CREATE", request, () -> createDeliveryInternal(request)); - } - - private GaoPackageDelivery createDeliveryInternal(GaoPackageDeliveryCreateRequest request) { - TimiException.required(request, "not found delivery request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(request.getCustomerPackageId()); - TimiException.required(packageEntity, "not found customer package"); - storeScopeService.checkStoreId(packageEntity.getStoreId()); - GaoPackageDelivery existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "requestNo 已被其他发放操作使用"); - verifyCreateIdempotency(existing, request); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放必须上传照片"); - TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可发放"); - checkPaymentForDelivery(packageEntity); - List requestItemList = validateRequestItems(request.getItemList()); - Map packageItemMap = new HashMap<>(); - for (GaoPackageDeliveryItem requestItem : requestItemList) { - GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(requestItem.getCustomerPackageItemId()); - TimiException.required(packageItem, "not found customer package item"); - TimiException.requiredTrue(packageEntity.getId().equals(packageItem.getCustomerPackageId()), "发放产品不属于当前套餐"); - packageItemMap.put(packageItem.getId(), packageItem); - } - - GaoPackageDelivery delivery = new GaoPackageDelivery(); - delivery.setCustomerPackageId(packageEntity.getId()); - delivery.setStoreId(packageEntity.getStoreId()); - delivery.setCustomerId(packageEntity.getCustomerId()); - delivery.setOperationType(GaoPackageDelivery.OperationType.DELIVER); - delivery.setStatus(GaoPackageDelivery.Status.PENDING); - delivery.setRequestNo(request.getRequestNo()); - delivery.setOperatorUserId(userLoginService.getRequireLoginUserId()); - delivery.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now())); - delivery.setRemark(request.getRemark()); - super.create(delivery); - for (GaoPackageDeliveryItem requestItem : requestItemList) { - GaoCustomerPackageItem packageItem = packageItemMap.get(requestItem.getCustomerPackageItemId()); - BigDecimal reservedBefore = decimal(packageItem.getReservedQuantity()); - BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity()); - BigDecimal available = decimal(packageItem.getTotalQuantity()).subtract(reservedBefore).subtract(deliveredBefore); - TimiException.requiredTrue(0 <= available.compareTo(requestItem.getQuantity()), "产品可发数量不足"); - int updated = customerPackageService.updateItemReservedQuantity(packageItem.getId(), requestItem.getQuantity()); - TimiException.requiredTrue(0 < updated, "产品可发数量不足"); - GaoPackageDeliveryItem savedItem = copyDeliveryItem(requestItem); - savedItem.setDeliveryId(delivery.getId()); - savedItem.setReservedBefore(reservedBefore); - savedItem.setReservedAfter(reservedBefore.add(requestItem.getQuantity())); - savedItem.setDeliveredBefore(deliveredBefore); - savedItem.setDeliveredAfter(deliveredBefore); - savedItem.setProductNameSnapshot(packageItem.getProductNameSnapshot()); - savedItem.setUnitSnapshot(packageItem.getUnitSnapshot()); - itemMapper.insert(savedItem); - } - attachmentService.updateByBizId(Attachment.BizType.GAO_PACKAGE_DELIVERY, delivery.getId(), request.getAttachmentList()); - return delivery; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery verify(String deliveryId) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_VERIFY", deliveryId, () -> verifyInternal(deliveryId)); - } - - private GaoPackageDelivery verifyInternal(String deliveryId) { - GaoPackageDelivery delivery = mapper.selectForUpdate(deliveryId); - TimiException.required(delivery, "not found delivery"); - storeScopeService.checkStoreId(delivery.getStoreId()); - TimiException.requiredTrue(delivery.getStatus() == GaoPackageDelivery.Status.PENDING && delivery.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "发放单不是待核销状态"); - GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(delivery.getCustomerPackageId()); - TimiException.required(packageEntity, "not found customer package"); - TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可核销发放"); - checkPaymentForDelivery(packageEntity); - List itemList = itemMapper.selectByDeliveryId(delivery.getId()); - for (GaoPackageDeliveryItem deliveryItem : itemList) { - GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(deliveryItem.getCustomerPackageItemId()); - TimiException.required(packageItem, "not found customer package item"); - TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(packageItem.getId(), deliveryItem.getQuantity().negate()), "发放占用数量异常"); - TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), deliveryItem.getQuantity()), "发放数量异常"); - } - TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, GaoPackageDelivery.Status.VERIFIED, userLoginService.getRequireLoginUserId(), Time.now(), delivery.getReason()), "发放单状态已变化"); - delivery.setStatus(GaoPackageDelivery.Status.VERIFIED); - delivery.setVerifierUserId(userLoginService.getRequireLoginUserId()); - delivery.setVerifiedAt(Time.now()); - refreshPackageStatus(packageEntity); - return delivery; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REJECT", request, () -> releasePending(request, GaoPackageDelivery.Status.REJECTED)); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CANCEL", request, () -> cancelInternal(request)); - } - - private GaoPackageDelivery cancelInternal(GaoPackageDeliveryActionRequest request) { - TimiException.required(request, "not found delivery request"); - GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId()); - TimiException.required(delivery, "not found delivery"); - String loginUserId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(loginUserId.equals(delivery.getOperatorUserId()), "只能撤销自己提交的发放"); - return releasePending(request, GaoPackageDelivery.Status.CANCELLED); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REVOKE", request, () -> revokeInternal(request)); - } - - private GaoPackageDelivery revokeInternal(GaoPackageDeliveryRevokeRequest request) { - TimiException.required(request, "not found revoke request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getDeliveryId(), "not found deliveryId"); - TimiException.required(hasReason(request.getReason()), "冲正原因不能为空"); - GaoPackageDelivery source = mapper.selectForUpdate(request.getDeliveryId()); - TimiException.required(source, "not found delivery"); - storeScopeService.checkStoreId(source.getStoreId()); - GaoPackageDelivery existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.REVOKE - && source.getId().equals(existing.getSourceDeliveryId()), "requestNo 已被其他发放操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放冲正必须上传照片"); - TimiException.requiredTrue(source.getOperationType() == GaoPackageDelivery.OperationType.DELIVER && source.getStatus() == GaoPackageDelivery.Status.VERIFIED, "只有已核销发放可以冲正"); - TimiException.requiredNull(mapper.selectRevokeBySourceDeliveryId(source.getId()), "该发放单已经冲正"); - GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(source.getCustomerPackageId()); - TimiException.required(packageEntity, "not found customer package"); - List sourceItemList = itemMapper.selectByDeliveryId(source.getId()); - GaoPackageDelivery revoke = new GaoPackageDelivery(); - revoke.setCustomerPackageId(source.getCustomerPackageId()); - revoke.setStoreId(source.getStoreId()); - revoke.setCustomerId(source.getCustomerId()); - revoke.setOperationType(GaoPackageDelivery.OperationType.REVOKE); - revoke.setStatus(GaoPackageDelivery.Status.VERIFIED); - revoke.setRequestNo(request.getRequestNo()); - revoke.setOperatorUserId(userLoginService.getRequireLoginUserId()); - revoke.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now())); - revoke.setVerifierUserId(userLoginService.getRequireLoginUserId()); - revoke.setVerifiedAt(Time.now()); - revoke.setSourceDeliveryId(source.getId()); - revoke.setReason(request.getReason().trim()); - revoke.setRemark(request.getRemark()); - super.create(revoke); - for (GaoPackageDeliveryItem sourceItem : sourceItemList) { - GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(sourceItem.getCustomerPackageItemId()); - TimiException.required(packageItem, "not found customer package item"); - BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity()); - TimiException.requiredTrue(0 < deliveredBefore.compareTo(sourceItem.getQuantity()) || deliveredBefore.compareTo(sourceItem.getQuantity()) == 0, "已发数量不足以冲正"); - TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), sourceItem.getQuantity().negate()), "发放数量冲正失败"); - GaoPackageDeliveryItem revokeItem = copyDeliveryItem(sourceItem); - revokeItem.setDeliveryId(revoke.getId()); - revokeItem.setReservedBefore(packageItem.getReservedQuantity()); - revokeItem.setReservedAfter(packageItem.getReservedQuantity()); - revokeItem.setDeliveredBefore(deliveredBefore); - revokeItem.setDeliveredAfter(deliveredBefore.subtract(sourceItem.getQuantity())); - itemMapper.insert(revokeItem); - } - TimiException.requiredTrue(0 < mapper.updateStatusIf(source.getId(), GaoPackageDelivery.Status.VERIFIED, GaoPackageDelivery.Status.REVOKED, source.getVerifierUserId(), source.getVerifiedAt(), source.getReason()), "发放单状态已变化"); - if (packageEntity.getStatus() == GaoCustomerPackage.Status.FULFILLED) { - packageEntity.setStatus(GaoCustomerPackage.Status.ACTIVE); - customerPackageService.updateAggregateState(packageEntity); - } - attachmentService.updateByBizId(Attachment.BizType.GAO_PACKAGE_DELIVERY, revoke.getId(), request.getAttachmentList()); - return revoke; - } - - private GaoPackageDelivery releasePending(GaoPackageDeliveryActionRequest request, GaoPackageDelivery.Status targetStatus) { - TimiException.required(request, "not found delivery request"); - TimiException.required(request.getDeliveryId(), "not found deliveryId"); - TimiException.required(hasReason(request.getReason()), "拒绝或撤销原因不能为空"); - GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId()); - TimiException.required(delivery, "not found delivery"); - storeScopeService.checkStoreId(delivery.getStoreId()); - TimiException.requiredTrue(delivery.getStatus() == GaoPackageDelivery.Status.PENDING && delivery.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "发放单不是待核销状态"); - for (GaoPackageDeliveryItem item : itemMapper.selectByDeliveryId(delivery.getId())) { - TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(item.getCustomerPackageItemId(), item.getQuantity().negate()), "发放占用数量异常"); - } - String loginUserId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, targetStatus, loginUserId, Time.now(), request.getReason().trim()), "发放单状态已变化"); - delivery.setStatus(targetStatus); - delivery.setVerifierUserId(loginUserId); - delivery.setVerifiedAt(Time.now()); - delivery.setReason(request.getReason().trim()); - return delivery; - } - - private void refreshPackageStatus(GaoCustomerPackage packageEntity) { - List itemList = customerPackageService.listItemByCustomerPackageId(packageEntity.getId()); - boolean fulfilled = !itemList.isEmpty(); - for (GaoCustomerPackageItem item : itemList) { - if (decimal(item.getDeliveredQuantity()).compareTo(decimal(item.getTotalQuantity())) < 0) { - fulfilled = false; - break; - } - } - if (fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) { - packageEntity.setStatus(GaoCustomerPackage.Status.FULFILLED); - customerPackageService.updateAggregateState(packageEntity); - } - } - - private List validateRequestItems(List sourceList) { - TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "发放至少需要一个产品"); - Set idSet = new HashSet<>(); - List result = new ArrayList<>(); - for (GaoPackageDeliveryItem source : sourceList) { - TimiException.required(source, "发放产品明细不能为空"); - TimiException.required(source.getCustomerPackageItemId(), "not found customerPackageItemId"); - TimiException.required(source.getQuantity(), "发放数量不能为空"); - TimiException.requiredTrue(0 < source.getQuantity().signum(), "发放数量必须大于 0"); - TimiException.requiredTrue(3 >= source.getQuantity().scale(), "发放数量最多保留三位小数"); - TimiException.requiredTrue(idSet.add(source.getCustomerPackageItemId()), "同一发放单产品不能重复"); - result.add(source); - } - result.sort(Comparator.comparing(GaoPackageDeliveryItem::getCustomerPackageItemId)); - return result; - } - - private GaoPackageDeliveryItem copyDeliveryItem(GaoPackageDeliveryItem source) { - GaoPackageDeliveryItem result = new GaoPackageDeliveryItem(); - result.setCustomerPackageItemId(source.getCustomerPackageItemId()); - result.setQuantity(source.getQuantity()); - return result; - } - - private void verifyCreateIdempotency(GaoPackageDelivery existing, GaoPackageDeliveryCreateRequest request) { - TimiException.requiredTrue(existing.getCustomerPackageId().equals(request.getCustomerPackageId()) - && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应请求内容不一致"); - List existingItems = itemMapper.selectByDeliveryId(existing.getId()); - List requestItems = validateRequestItems(request.getItemList()); - TimiException.requiredTrue(existingItems.size() == requestItems.size(), "requestNo 对应请求内容不一致"); - Map requestMap = requestItems.stream().collect(java.util.stream.Collectors.toMap(GaoPackageDeliveryItem::getCustomerPackageItemId, GaoPackageDeliveryItem::getQuantity)); - for (GaoPackageDeliveryItem item : existingItems) { - TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getCustomerPackageItemId(), ZERO)) == 0, "requestNo 对应请求内容不一致"); - } - } - - private boolean hasAttachment(List attachmentList) { - for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { - if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { - return true; - } - } - return false; - } - - private boolean hasReason(String reason) { - return reason != null && !reason.trim().isEmpty(); - } - - private boolean canDeliver(GaoCustomerPackage packageEntity) { - return safeLong(packageEntity.getSaleAmount()) == 0 || 0 < safeLong(packageEntity.getPaidAmount()); - } - - private void checkPaymentForDelivery(GaoCustomerPackage packageEntity) { - long saleAmount = safeLong(packageEntity.getSaleAmount()); - long paidAmount = safeLong(packageEntity.getPaidAmount()); - TimiException.requiredTrue(canDeliver(packageEntity), "套餐未有已核销收款,不能发放"); - if (0 < paidAmount && paidAmount < saleAmount) { - roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name()); - } - } - - private boolean sameText(String left, String right) { - return left == null ? right == null : left.equals(right); - } - - private BigDecimal decimal(BigDecimal value) { - return value == null ? ZERO : value; - } - - private long safeLong(Long value) { - return value == null ? 0L : value; - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java index c1579ea..180813c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java @@ -5,8 +5,8 @@ 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.GaoPointAccount; -import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper; import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoPointAccountService; import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage; import com.imyeyu.java.bean.timi.TimiException; @@ -15,13 +15,13 @@ import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; /// GAO 客户积分账户服务实现 /// @@ -32,7 +32,7 @@ import java.util.stream.Collectors; public class GaoPointAccountServiceImplement extends AbstractEntityService implements GaoPointAccountService { private final GaoPointAccountMapper mapper; - private final GaoCustomerMapper customerMapper; + private final ObjectProvider customerServiceProvider; private final AttachmentService attachmentService; @Override @@ -88,8 +88,7 @@ public class GaoPointAccountServiceImplement extends AbstractEntityService customerMap = customerMapper.selectByIdList(customerIdList).stream() - .collect(Collectors.toMap(GaoCustomer::getId, item -> item)); + Map customerMap = customerServiceProvider.getObject().mapByIdList(customerIdList); Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList); for (GaoPointAccount account : accountList) { GaoCustomer customer = customerMap.get(account.getCustomerId()); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java index 529e00a..06c77d0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java @@ -1,15 +1,19 @@ 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.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.service.AttachmentService; import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoPointAccount; import com.imyeyu.api.modules.gao.entity.GaoPointLedger; import com.imyeyu.api.modules.gao.entity.GaoPointRule; -import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerConsumptionRecordMapper; 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.GaoPointAccountService; import com.imyeyu.api.modules.gao.service.GaoPointLedgerService; @@ -42,7 +46,9 @@ import java.util.Map; @RequiredArgsConstructor public class GaoPointLedgerServiceImplement extends AbstractEntityService implements GaoPointLedgerService { + private final ObjectMapper jackson; private final GaoPointLedgerMapper mapper; + private final GaoPointLedgerConsumptionRecordMapper consumptionRecordMapper; private final GaoPointAccountService accountService; private final GaoCustomerService customerService; private final AttachmentService attachmentService; @@ -71,23 +77,35 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService execution, + GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId, + String storeId, String customerId, Long occurredAt, String operatorUserId, List consumptionRecordIdList) { TimiException.required(rule, "not found point rule"); - TimiException.requiredTrue(pointDelta != 0, "pointDelta invalid"); - String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId()); + TimiException.required(decision, "not found point decision"); + 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; - 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) @Override - public GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record) { + public GaoPointLedger revoke(GaoPointLedger originLedger, String reason) { TimiException.required(originLedger, "not found origin ledger"); - TimiException.required(record, "not found event record"); - TimiException.requiredTrue(originLedger.getPointDelta() != 0, "origin ledger pointDelta invalid"); - String idempotencyKey = "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId()); - return apply(originLedger.getStoreId(), originLedger.getCustomerId(), GaoPointLedger.ChangeType.REVOKE, -originLedger.getPointDelta(), originLedger.getRuleId(), originLedger.getEventId(), originLedger.getEventRecordId(), originLedger.getSourcePayload(), idempotencyKey, "撤销登记记录积分", userLoginService.getRequireLoginUserId(), Time.now()); + TimiException.required(originLedger.getId(), "not found origin ledger.id"); + TimiException.requiredTrue(originLedger.getPointDelta() != null && originLedger.getPointDelta() != 0, "origin ledger pointDelta invalid"); + String idempotencyKey = "GAO_POINT_LEDGER:%s:REVOKE".formatted(originLedger.getId()); + 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) @@ -102,7 +120,27 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService listRuleChangeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId) { + TimiException.required(triggerSourceType, "not found triggerSourceType"); + TimiException.required(triggerSourceId, "not found triggerSourceId"); + return mapper.selectRuleChangeByTriggerSource(triggerSourceType, triggerSourceId); + } + + @Override + public List listRuleChangeByConsumptionRecordId(String consumptionRecordId) { + TimiException.required(consumptionRecordId, "not found consumptionRecordId"); + return mapper.selectRuleChangeByConsumptionRecordId(consumptionRecordId); + } + + @Override + public List listByIdempotencyKeyList(List idempotencyKeyList) { + TimiException.required(idempotencyKeyList, "not found idempotencyKeyList"); + return mapper.selectByIdempotencyKeyList(idempotencyKeyList); } @Override @@ -133,7 +171,62 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService 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.setContent("storeId=%s, customerId=%s, changeType=%s, pointDelta=%d, idempotencyKey=%s, remark=%s".formatted(storeId, customerId, changeType, pointDelta, idempotencyKey, remark)); try { @@ -169,8 +262,11 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService implements GaoPointRuleService { - private static final int MAX_TIER_COUNT = 100; - private final GaoPointRuleMapper mapper; private final GaoEventService eventService; + private final GroovyScriptService groovyScriptService; @Override protected BaseMapper mapper() { @@ -41,10 +41,10 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService listActive(String storeId, String eventId, long now) { + public List listActive(String storeId, GaoPointRule.TriggerSourceType triggerSourceType, String triggerRefId, long now) { TimiException.required(storeId, "not found storeId"); - TimiException.required(eventId, "not found eventId"); - return mapper.selectActiveByStoreIdAndEventId(storeId, eventId, now); + TimiException.required(triggerSourceType, "not found triggerSourceType"); + return mapper.selectActiveByTrigger(storeId, triggerSourceType, triggerRefId, now); } @Override @@ -89,9 +89,7 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService eventRecordServiceProvider; @Transactional(TimiServerDBConfig.ROLLBACKER) @Override @@ -60,47 +63,82 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { TimiException.required(record.getEventId(), "not found record.eventId"); TimiException.required(record.getRegisteredAt(), "not found record.registeredAt"); - List ruleList = ruleService.listActive(record.getStoreId(), record.getEventId(), Time.now()); - Long count = null; long pointDeltaTotal = 0L; - for (GaoPointRule rule : ruleList) { - RuleResolution resolution = resolveRule(record, rule, count); - if (resolution == null) { + for (GaoPointRule rule : ruleService.listActive(record.getStoreId(), GaoPointRule.TriggerSourceType.EVENT_RECORD, record.getEventId(), record.getRegisteredAt())) { + GaoPointScriptContext context = eventContext(rule, record); + GroovyScriptExecutionResult execution = evaluate(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), + GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId()); + if (execution == null) { continue; } - if (rule.getTriggerType() == GaoPointRule.TriggerType.FIRST_TIME || rule.getTriggerType() == GaoPointRule.TriggerType.AFTER_COUNT_EVERY_TIME) { - count = count == null ? count(record) : count; - } - long pointDelta = resolution.pointDelta(); - if (pointDelta != 0) { - ledgerService.applyRule(record, rule, pointDelta, resolution.sourcePayload()); - pointDeltaTotal = Math.addExact(pointDeltaTotal, pointDelta); - } + pointDeltaTotal += applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), null, + record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), record.getOperatorUserId(), context); if (Boolean.TRUE.equals(rule.getExclusive())) { break; } } record.setEarnedPointValue(pointDeltaTotal); - eventRecordMapper.updateEarnedPointValue(record.getId(), pointDeltaTotal); + eventRecordServiceProvider.getObject().updateEarnedPointValue(record.getId(), pointDeltaTotal); } @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void revokeByEventRecord(GaoEventRecord record) { TimiException.required(record, "not found event record"); - List originLedgerList = ledgerMapper.selectRuleChangeByEventRecordId(record.getId()); - if (originLedgerList.isEmpty()) { - return; + TimiException.required(record.getId(), "not found event record.id"); + revokeByTriggerSource(GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), "撤销登记记录积分"); + GaoCustomerConsumptionRecord fact = consumptionService.getBySource(GaoCustomerConsumptionRecord.SourceType.LEGACY_EVENT, record.getId()); + if (fact != null) { + consumptionService.revokeLegacyEvent(record.getId(), "撤销历史消费事件对应的消费事实"); + revokeDependentLedgers(fact.getId()); } - Set revokedKeySet = new HashSet<>(); - List revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList(); - for (GaoPointLedger ledger : ledgerMapper.selectByIdempotencyKeyList(revokeKeyList)) { - revokedKeySet.add(ledger.getIdempotencyKey()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @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) { - if (!revokedKeySet.contains(revokeKey(record, originLedger))) { - ledgerService.revoke(originLedger, record); + for (GaoPointRule rule : ruleService.listActive(order.getStoreId(), GaoPointRule.TriggerSourceType.SALES_ORDER_PAID, null, order.getPaidAt())) { + GaoPointScriptContext context = orderContext(rule, order); + GroovyScriptExecutionResult 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()); } } @@ -115,94 +153,139 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { 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); - case FIRST_TIME -> { - long currentCount = count == null ? count(record) : count; - yield currentCount == 1 ? new RuleResolution(rule.getPointValue(), null) : null; - } - case AFTER_COUNT_EVERY_TIME -> { - JsonNode condition = rule.getConditionJson(); - long threshold = condition.get("threshold").longValue(); - boolean includeCurrent = condition.get("includeCurrent") == null || condition.get("includeCurrent").isNull() || condition.get("includeCurrent").asBoolean(); - long currentCount = count == null ? count(record) : count; - yield includeCurrent ? threshold <= currentCount ? new RuleResolution(rule.getPointValue(), null) : null : threshold < currentCount ? new RuleResolution(rule.getPointValue(), null) : null; - } - case SOURCE_VALUE -> resolveSourceValue(record, rule); - case CUSTOMER_EVENT_VALUE_TIER -> resolveTier(record, rule); - }; + /// 按规则评估积分决策 + /// + /// 简单规则直接构造固定积分决策;Groovy 规则执行发布版本脚本;脚本未返回决策视为未命中 + /// + /// @param rule 积分规则 + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param occurredAt 发生时间 + /// @param triggerRefType 触发对象筛选类型,可空 + /// @param triggerRefId 触发对象筛选 ID,可空 + /// @return 脚本执行结果,未命中返回 null + private GroovyScriptExecutionResult evaluate(GaoPointRule rule, String storeId, String customerId, long occurredAt, String triggerRefType, String triggerRefId) { + if (rule.getRuleMode() == GaoPointRule.RuleMode.SIMPLE) { + GaoPointDecision decision = new GaoPointDecision(rule.getPointValue(), rule.getName(), Map.of("ruleMode", GaoPointRule.RuleMode.SIMPLE.name()), List.of()); + return new GroovyScriptExecutionResult<>(null, null, null, null, decision); + } + GroovyScript script = groovyScriptService.get(rule.getGroovyScriptId()); + TimiException.required(script, "规则绑定的 Groovy 脚本不存在"); + GaoPointScriptContext context = buildContext(rule, storeId, customerId, occurredAt, triggerRefType, triggerRefId); + Map 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; - } - permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.POINT_SOURCE_VALUE.getValue()); - long pointValue = record.getPointValue(); - TimiException.requiredTrue(pointValue != 0, "pointValue invalid"); - JsonNode condition = rule.getConditionJson(); - if (condition != null) { - JsonNode min = condition.get("min"); - JsonNode max = condition.get("max"); - TimiException.requiredTrue(min == null || min.isNull() || min.longValue() <= pointValue, "pointValue 小于规则最小值"); - TimiException.requiredTrue(max == null || max.isNull() || pointValue <= max.longValue(), "pointValue 大于规则最大值"); - } - ObjectNode payload = jackson.createObjectNode(); - payload.put("submittedPointValue", pointValue); - payload.put("validatedPointValue", pointValue); - return new RuleResolution(pointValue, payload); + /// 写入积分流水并累计决策结果 + /// + /// @return 本次决策的积分变化 + private long applyDecision(GaoPointRule rule, GroovyScriptExecutionResult execution, + GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId, + String storeId, String customerId, Long occurredAt, String operatorUserId, GaoPointScriptContext context) { + GaoPointDecision decision = execution.result(); + TimiException.required(decision, "规则未返回积分决策"); + TimiException.requiredTrue(decision.pointDelta() != 0, "规则积分变化不能为 0"); + ledgerService.applyRule(rule, decision, isGroovy(execution) ? execution : null, triggerSourceType, triggerSourceId, salesOrderId, + storeId, customerId, occurredAt, operatorUserId, decision.consumptionRecordIdList()); + return decision.pointDelta(); } - private RuleResolution resolveTier(GaoEventRecord record, GaoPointRule rule) { - JsonNode condition = rule.getConditionJson(); - String sourceEventId = condition.get("sourceEventId").asText(); - long amount = condition.get("window").get("amount").longValue(); - ZoneId zone = ZoneId.systemDefault(); - LocalDate currentDate = Instant.ofEpochMilli(record.getRegisteredAt()).atZone(zone).toLocalDate(); - long windowBeginAt = currentDate.minusYears(amount).atStartOfDay(zone).toInstant().toEpochMilli(); - long windowEndAt = Math.addExact(record.getRegisteredAt(), 1L); - long sumValue = TimiJava.defaultIfNull(eventRecordMapper.sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt), 0L); + /// 撤销指定触发来源产生的全部规则积分 + private void revokeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String reason) { + List originLedgerList = ledgerService.listRuleChangeByTriggerSource(triggerSourceType, triggerSourceId); + revokeAll(originLedgerList, reason); + } - JsonNode matchedTier = null; - for (JsonNode tier : condition.get("tiers")) { - long min = tier.get("min").longValue(); - JsonNode maxNode = tier.get("max"); - if (min <= sumValue && (maxNode == null || maxNode.isNull() || sumValue <= maxNode.longValue())) { - matchedTier = tier; - break; + /// 复算并撤销依赖指定消费事实的条件积分 + /// + /// 按原积分流水保存的脚本版本和原触发时间重算,只有不再满足原条件的积分才撤销 + private void revokeDependentLedgers(String consumptionRecordId) { + List dependentList = ledgerService.listRuleChangeByConsumptionRecordId(consumptionRecordId); + if (dependentList.isEmpty()) { + return; + } + List 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) { - return null; + GaoPointRule replayRule = new GaoPointRule(); + 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 bindings = new HashMap<>(); + bindings.put("context", context); + try { + GroovyScriptExecutionResult 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); - payload.put("sourceValueField", "numberValue"); - payload.put("windowBeginAt", windowBeginAt); - payload.put("windowEndAt", windowEndAt); - payload.put("sumValue", sumValue); - ObjectNode tierPayload = payload.putObject("tier"); - tierPayload.put("min", matchedTier.get("min").longValue()); - JsonNode maxNode = matchedTier.get("max"); - if (maxNode != null && !maxNode.isNull()) { - tierPayload.put("max", maxNode.longValue()); + } + + /// 幂等撤销一批原始积分流水 + private void revokeAll(List originLedgerList, String reason) { + if (originLedgerList == null || originLedgerList.isEmpty()) { + return; + } + List revokeKeyList = originLedgerList.stream().map(origin -> "GAO_POINT_LEDGER:%s:REVOKE".formatted(origin.getId())).toList(); + Set revokedKeySet = new HashSet<>(); + for (GaoPointLedger ledger : ledgerService.listByIdempotencyKeyList(revokeKeyList)) { + revokedKeySet.add(ledger.getIdempotencyKey()); + } + 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) { - return TimiJava.defaultIfNull(eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null), 0L); + private GaoPointScriptContext eventContext(GaoPointRule rule, GaoEventRecord record) { + return buildContext(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), + GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId()); } - private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) { - return "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId()); + private GaoPointScriptContext orderContext(GaoPointRule rule, GaoSalesOrder order) { + 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 execution) { + return execution != null && execution.versionId() != null; + } + + private boolean isGroovyLedger(GaoPointLedger origin) { + return TimiJava.isNotEmpty(origin.getGroovyScriptVersionId()); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductCategoryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductCategoryServiceImplement.java new file mode 100644 index 0000000..63ae599 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductCategoryServiceImplement.java @@ -0,0 +1,152 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.mapper.GaoProductCategoryMapper; +import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// GAO 产品分类服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoProductCategoryServiceImplement extends AbstractEntityService implements GaoProductCategoryService { + + private final GaoProductCategoryMapper mapper; + private final GaoStoreScopeService storeScopeService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoProductCategoryPage page) { + TimiException.required(page, "not found category page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + return result; + } + + @Override + public List listByStore(String storeId) { + TimiException.required(storeId, "not found storeId"); + return mapper.selectByStoreId(storeId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void sort(List idList) { + if (TimiJava.isEmpty(idList)) { + return; + } + List distinctIdList = new ArrayList<>(new LinkedHashSet<>(idList)); + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_SORT", distinctIdList, () -> { + Map categoryMap = mapper.selectByIdList(new LinkedHashSet<>(distinctIdList)).stream() + .collect(Collectors.toMap(GaoProductCategory::getId, Function.identity())); + TimiException.requiredTrue(categoryMap.size() == distinctIdList.size(), "产品分类列表包含不存在的分类"); + for (GaoProductCategory category : categoryMap.values()) { + storeScopeService.checkStoreId(category.getStoreId()); + } + mapper.updateSort(distinctIdList, Time.now()); + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoProductCategory category) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_CREATE", category, () -> { + prepare(category, null); + super.create(category); + return category; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoProductCategory request) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_UPDATE", request, () -> { + TimiException.required(request, "not found category"); + TimiException.required(request.getId(), "not found category.id"); + GaoProductCategory category = get(request.getId()); + TimiException.required(category, "not found category"); + storeScopeService.checkStoreId(category.getStoreId()); + category.setName(request.getName()); + category.setSort(request.getSort()); + category.setStatus(request.getStatus()); + prepare(category, category.getId()); + super.update(category); + return category; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void disable(String id) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_DISABLE", id, () -> { + GaoProductCategory category = get(id); + TimiException.required(category, "not found category"); + storeScopeService.checkStoreId(category.getStoreId()); + category.setStatus(GaoProductCategory.Status.INACTIVE); + category.setUpdatedAt(Time.now()); + category.setSort(TimiJava.defaultIfNull(category.getSort(), 0)); + super.update(category); + return category; + }); + } + + @Override + public void delete(String id) { + disable(id); + } + + @Override + public void destroy(String id) { + throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "产品分类不允许物理删除"); + } + + private void prepare(GaoProductCategory category, String excludeId) { + TimiException.required(category, "not found category"); + TimiException.required(category.getStoreId(), "not found category.storeId"); + storeScopeService.checkStoreId(category.getStoreId()); + String name = requiredText(category.getName(), "分类名称不能为空"); + TimiException.requiredNull(mapper.selectByStoreIdAndName(category.getStoreId(), name, excludeId), "门店内分类名称不能重复"); + category.setName(name); + category.setSort(TimiJava.defaultIfNull(category.getSort(), 0)); + category.setStatus(TimiJava.defaultIfNull(category.getStatus(), GaoProductCategory.Status.ACTIVE)); + TimiException.requiredTrue(category.getStatus() != null, "分类状态不能为空"); + TimiException.required(userLoginService.getRequireLoginUserId(), "not found operatorUserId"); + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductChartServiceImplement.java new file mode 100644 index 0000000..9779df6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductChartServiceImplement.java @@ -0,0 +1,109 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoProductChartMapper; +import com.imyeyu.api.modules.gao.service.GaoProductChartService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// GAO 产品图表服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoProductChartServiceImplement implements GaoProductChartService { + + private static final int DEFAULT_TOP_N = 20; + private static final int MAX_TOP_N = 100; + + private final GaoProductChartMapper mapper; + + @Override + public List salesDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectSalesDaily(req); + } + + @Override + public List salesRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectSalesRank(req); + } + + @Override + public List fulfillmentDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectFulfillmentDaily(req); + } + + @Override + public List fulfillmentRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectFulfillmentRank(req); + } + + @Override + public List stockMovementDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectStockMovementDaily(req); + } + + @Override + public List stockHealth(GaoProductChartReq req) { + checkStoreReq(req); + normalizeTopN(req); + return mapper.selectStockHealth(req); + } + + @Override + public List type(GaoProductChartReq req) { + checkStoreReq(req); + return mapper.selectType(req); + } + + @Override + public List category(GaoProductChartReq req) { + checkStoreReq(req); + return mapper.selectCategory(req); + } + + @Override + public List unitSalesRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectUnitSalesRank(req); + } + + private void checkRangeReq(GaoProductChartReq req) { + checkStoreReq(req); + TimiException.required(req.getBeginAt(), "产品图表开始时间不能为空"); + TimiException.required(req.getEndAt(), "产品图表结束时间不能为空"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "产品图表结束时间早于开始时间"); + } + + private void checkStoreReq(GaoProductChartReq req) { + TimiException.required(req, "产品图表请求不能为空"); + TimiException.required(req.getStoreId(), "产品图表门店不能为空"); + } + + private void normalizeTopN(GaoProductChartReq req) { + int topN = req.getTopN() == null ? DEFAULT_TOP_N : req.getTopN(); + TimiException.requiredTrue(0 < topN && topN < MAX_TOP_N + 1, "产品图表排行数量无效"); + req.setTopN(topN); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductServiceImplement.java new file mode 100644 index 0000000..75dd959 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductServiceImplement.java @@ -0,0 +1,271 @@ +package com.imyeyu.api.modules.gao.service.implement; + +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.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.mapper.GaoProductMapper; +import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; +import com.imyeyu.api.modules.gao.service.GaoProductSpecService; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/// GAO 产品服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoProductServiceImplement extends AbstractEntityService implements GaoProductService { + + private final GaoProductMapper mapper; + private final AttachmentService attachmentService; + private final ObjectProvider categoryServiceProvider; + private final ObjectProvider skuServiceProvider; + private final ObjectProvider specServiceProvider; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoProductPage page) { + TimiException.required(page, "not found product page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + List list = mapper.selectByQuery(page); + fillTransient(list); + result.setList(list); + return result; + } + + @Override + public GaoProduct detail(String id) { + GaoProduct product = get(id); + TimiException.required(product, "not found product"); + fillTransient(List.of(product)); + return product; + } + + @Override + public List listByIdList(Collection idList) { + if (idList == null || idList.isEmpty()) { + return List.of(); + } + List list = mapper.selectByIdList(idList); + fillTransient(list); + return list; + } + + @Override + public List listSellable(GaoProductPage page) { + TimiException.required(page, "not found product page"); + page.setStatus(GaoProduct.Status.ACTIVE); + List list = mapper.selectByQuery(page); + fillTransient(list); + for (GaoProduct product : list) { + product.setSkuList(product.getSkuList().stream().filter(sku -> sku.getStatus() == com.imyeyu.api.modules.gao.entity.GaoProductSku.Status.ACTIVE && sku.getUnitList().stream().anyMatch(unit -> Boolean.TRUE.equals(unit.getAllowSale()))).toList()); + } + return list.stream().filter(product -> !product.getSkuList().isEmpty()).toList(); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoProduct product) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CREATE", product, () -> { + prepare(product, null); + super.create(product); + Map specValueIdMap = saveSpecs(product, product.getSpecList()); + normalizeSkuSpecValueIds(product.getSkuList(), specValueIdMap); + saveSkus(product, product.getSkuList()); + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList()); + return product; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoProduct request) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_UPDATE", request, () -> { + TimiException.required(request, "not found product"); + TimiException.required(request.getId(), "not found product.id"); + GaoProduct product = get(request.getId()); + TimiException.required(product, "not found product"); + product.setCategoryId(request.getCategoryId()); + product.setCode(TimiJava.defaultIfNull(request.getCode(), product.getCode())); + product.setName(TimiJava.defaultIfNull(request.getName(), product.getName())); + product.setProductType(TimiJava.defaultIfNull(request.getProductType(), product.getProductType())); + product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled())); + product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus())); + product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort())); + product.setRemark(request.getRemark()); + product.setSkuList(request.getSkuList() == null ? skuServiceProvider.getObject().listByProductId(product.getId()) : request.getSkuList()); + prepare(product, product.getId()); + super.update(product); + if (request.getSkuList() != null) { + saveSkus(product, request.getSkuList()); + } + if (request.getAttachmentList() != null) { + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList()); + } + return product; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void disable(String id) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_DISABLE", id, () -> { + GaoProduct product = get(id); + TimiException.required(product, "not found product"); + TimiException.requiredTrue(skuServiceProvider.getObject().listByProductId(id).stream().noneMatch(sku -> sku.getStatus() == com.imyeyu.api.modules.gao.entity.GaoProductSku.Status.ACTIVE), "请先停用产品的全部 SKU"); + product.setStatus(GaoProduct.Status.INACTIVE); + product.setUpdatedBy(userLoginService.getRequireLoginUserId()); + product.setUpdatedAt(Time.now()); + super.update(product); + return product; + }); + } + + @Override + public void delete(String id) { + disable(id); + } + + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "已建立产品库的产品不允许物理删除"); + } + + private void prepare(GaoProduct product, String excludeId) { + TimiException.required(product, "not found product"); + TimiException.required(product.getStoreId(), "not found product.storeId"); + product.setCode(requiredText(product.getCode(), "产品编码不能为空")); + product.setName(requiredText(product.getName(), "产品名称不能为空")); + product.setProductType(TimiJava.defaultIfNull(product.getProductType(), GaoProduct.ProductType.GOODS)); + product.setStockEnabled(TimiJava.defaultIfNull(product.getStockEnabled(), product.getProductType() != GaoProduct.ProductType.SERVICE)); + TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存"); + product.setStatus(TimiJava.defaultIfNull(product.getStatus(), GaoProduct.Status.ACTIVE)); + product.setSort(TimiJava.defaultIfNull(product.getSort(), 0)); + TimiException.requiredNull(mapper.selectByStoreIdAndCode(product.getStoreId(), product.getCode(), excludeId), "门店内产品编码不能重复"); + if (product.getCategoryId() != null) { + GaoProductCategory category = categoryServiceProvider.getObject().get(product.getCategoryId()); + TimiException.required(category, "产品分类不存在"); + TimiException.requiredTrue(product.getStoreId().equals(category.getStoreId()), "产品分类不属于当前门店"); + } + product.setUpdatedBy(userLoginService.getRequireLoginUserId()); + } + + private void saveSkus(GaoProduct product, List skuList) { + List sourceList = skuList; + if (sourceList == null || sourceList.isEmpty()) { + com.imyeyu.api.modules.gao.entity.GaoProductSku sku = new com.imyeyu.api.modules.gao.entity.GaoProductSku(); + sku.setProductId(product.getId()); sku.setCode(product.getCode()); sku.setName(product.getName()); + com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit unit = new com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit(); + unit.setName("件"); unit.setIsBase(true); unit.setBaseQuantityPerUnit(BigDecimal.ONE); + sku.setUnitList(List.of(unit)); sourceList = List.of(sku); + } + for (com.imyeyu.api.modules.gao.entity.GaoProductSku source : sourceList) { + source.setProductId(product.getId()); + if (source.getId() == null) skuServiceProvider.getObject().create(source); else skuServiceProvider.getObject().update(source); + } + } + + private Map saveSpecs(GaoProduct product, List specList) { + Map specValueIdMap = new HashMap<>(); + if (specList == null || specList.isEmpty()) { + return specValueIdMap; + } + for (int specIndex = 0; specIndex < specList.size(); specIndex++) { + GaoProductSpec spec = specList.get(specIndex); + spec.setId(null); + spec.setProductId(product.getId()); + spec.setSort(TimiJava.defaultIfNull(spec.getSort(), specIndex)); + specServiceProvider.getObject().createSpec(spec); + List valueList = spec.getValueList(); + if (valueList == null) { + continue; + } + for (int valueIndex = 0; valueIndex < valueList.size(); valueIndex++) { + GaoProductSpecValue value = valueList.get(valueIndex); + String clientKey = value.getClientKey(); + value.setId(null); + value.setSpecId(spec.getId()); + value.setSort(TimiJava.defaultIfNull(value.getSort(), valueIndex)); + specServiceProvider.getObject().createValue(value); + if (clientKey != null && !clientKey.isBlank()) { + specValueIdMap.put(clientKey, value.getId()); + } + } + } + return specValueIdMap; + } + + private void normalizeSkuSpecValueIds(List skuList, Map specValueIdMap) { + if (skuList == null || skuList.isEmpty() || specValueIdMap.isEmpty()) { + return; + } + for (GaoProductSku sku : skuList) { + List specValueIdList = sku.getSpecValueIdList(); + if (specValueIdList != null) { + sku.setSpecValueIdList(specValueIdList.stream().map(id -> specValueIdMap.getOrDefault(id, id)).toList()); + } + } + } + + private void fillTransient(List list) { + if (list == null || list.isEmpty()) { + return; + } + List productIdList = list.stream().map(GaoProduct::getId).toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PRODUCT, productIdList); + Map> skuMap = skuServiceProvider.getObject().listByProductIdList(productIdList).stream() + .collect(Collectors.groupingBy(com.imyeyu.api.modules.gao.entity.GaoProductSku::getProductId)); + for (GaoProduct product : list) { + product.setSkuList(skuMap.getOrDefault(product.getId(), List.of())); + product.setSpecList(specServiceProvider.getObject().listByProductId(product.getId())); + product.setAttachmentList(attachmentMap.getOrDefault(product.getId(), List.of())); + if (product.getCategoryId() != null) { + product.setCategory(categoryServiceProvider.getObject().get(product.getCategoryId())); + } + } + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuChartServiceImplement.java new file mode 100644 index 0000000..713f3bf --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuChartServiceImplement.java @@ -0,0 +1,95 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuChartMapper; +import com.imyeyu.api.modules.gao.service.GaoProductSkuChartService; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// GAO SKU 图表服务实现 +/// +/// @author Codex +/// @since 2026-09-07 +@Service +@RequiredArgsConstructor +public class GaoProductSkuChartServiceImplement implements GaoProductSkuChartService { + + private static final int DEFAULT_TOP_N = 20; + private static final int MAX_TOP_N = 100; + + private final GaoProductSkuChartMapper mapper; + + @Override + public List salesDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectSalesDaily(req); + } + + @Override + public List salesRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectSalesRank(req); + } + + @Override + public List unitSalesRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectUnitSalesRank(req); + } + + @Override + public List fulfillmentDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectFulfillmentDaily(req); + } + + @Override + public List fulfillmentRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectFulfillmentRank(req); + } + + @Override + public List stockMovementDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectStockMovementDaily(req); + } + + @Override + public List stockHealth(GaoProductChartReq req) { + checkStoreReq(req); + normalizeTopN(req); + return mapper.selectStockHealth(req); + } + + private void checkRangeReq(GaoProductChartReq req) { + checkStoreReq(req); + TimiException.required(req.getBeginAt(), "SKU 图表开始时间不能为空"); + TimiException.required(req.getEndAt(), "SKU 图表结束时间不能为空"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "SKU 图表结束时间早于开始时间"); + } + + private void checkStoreReq(GaoProductChartReq req) { + TimiException.required(req, "SKU 图表请求不能为空"); + TimiException.required(req.getStoreId(), "SKU 图表门店不能为空"); + } + + private void normalizeTopN(GaoProductChartReq req) { + int topN = req.getTopN() == null ? DEFAULT_TOP_N : req.getTopN(); + TimiException.requiredTrue(0 < topN && topN < MAX_TOP_N + 1, "SKU 图表排行数量无效"); + req.setTopN(topN); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java new file mode 100644 index 0000000..749979f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java @@ -0,0 +1,393 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +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.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuSpecValue; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuSpecValueMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuUnitMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecValueMapper; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/// GAO 产品 SKU 服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoProductSkuServiceImplement extends AbstractEntityService implements GaoProductSkuService { + + private static final BigDecimal ONE = BigDecimal.ONE; + + private final GaoProductSkuMapper mapper; + private final GaoProductSkuUnitMapper unitMapper; + private final GaoProductSkuSpecValueMapper skuSpecValueMapper; + private final GaoProductSpecMapper specMapper; + private final GaoProductSpecValueMapper valueMapper; + private final AttachmentService attachmentService; + private final ObjectProvider productServiceProvider; + private final UserLoginService userLoginService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public GaoProductSku detail(String id) { + GaoProductSku sku = get(id); + TimiException.required(sku, "not found product sku"); + fillTransient(List.of(sku)); + return sku; + } + + @Override + public List listByProductId(String productId) { + TimiException.required(productId, "not found productId"); + List result = mapper.selectByProductId(productId); + fillTransient(result); + return result; + } + + @Override + public List listByProductIdList(Collection productIdList) { + if (productIdList == null || productIdList.isEmpty()) { + return List.of(); + } + List result = mapper.selectByProductIdList(productIdList); + fillTransient(result); + return result; + } + + @Override + public GaoProductSkuUnitSelection getSellableSkuUnit(String storeId, String skuId, String skuUnitId) { + return getSkuUnit(storeId, skuId, skuUnitId, false); + } + + @Override + public GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(skuId, "not found skuId"); + TimiException.required(skuUnitId, "not found skuUnitId"); + GaoProductSku sku = mapper.selectForUpdate(skuId); + TimiException.required(sku, "not found product sku"); + TimiException.requiredTrue(storeId.equals(sku.getStoreId()), "SKU 不属于当前门店"); + TimiException.requiredTrue(sku.getStatus() == GaoProductSku.Status.ACTIVE, "SKU 已停用"); + GaoProduct product = productServiceProvider.getObject().get(sku.getProductId()); + TimiException.required(product, "not found product"); + TimiException.requiredTrue(product.getStatus() == GaoProduct.Status.ACTIVE, "产品已停用"); + GaoProductSkuUnit unit = unitMapper.selectForUpdate(skuUnitId); + TimiException.required(unit, "not found product sku unit"); + TimiException.requiredTrue(skuId.equals(unit.getSkuId()), "SKU 单位不属于当前 SKU"); + TimiException.requiredTrue(!purchase || Boolean.TRUE.equals(unit.getAllowPurchase()), "SKU 单位不允许采购入库"); + TimiException.requiredTrue(purchase || Boolean.TRUE.equals(unit.getAllowSale()), "SKU 单位不允许销售"); + return new GaoProductSkuUnitSelection(product, sku, unit); + } + + @Override + public List listUnitBySkuId(String skuId) { + TimiException.required(skuId, "not found skuId"); + return unitMapper.selectBySkuId(skuId); + } + + @Override + public JsonNode getSkuSpecSnapshot(String skuId) { + TimiException.required(skuId, "not found skuId"); + GaoProductSku sku = get(skuId); + TimiException.required(sku, "not found product sku"); + return getSkuSpecSnapshot(sku); + } + + @Override + public JsonNode getSkuSpecSnapshot(GaoProductSku sku) { + TimiException.required(sku, "not found product sku"); + List specList = specMapper.selectByProductId(sku.getProductId()); + if (specList.isEmpty()) { + return JsonNodeFactory.instance.arrayNode(); + } + List specIdList = specList.stream().map(GaoProductSpec::getId).toList(); + Map> valueMap = new LinkedHashMap<>(); + for (GaoProductSpecValue value : valueMapper.selectBySpecIdList(specIdList)) { + valueMap.computeIfAbsent(value.getSpecId(), key -> new ArrayList<>()).add(value); + } + Set selectedValueIdSet = skuSpecValueMapper.selectBySkuIdList(List.of(sku.getId())).stream() + .map(GaoProductSkuSpecValue::getSpecValueId) + .collect(Collectors.toSet()); + ArrayNode result = JsonNodeFactory.instance.arrayNode(); + for (GaoProductSpec spec : specList) { + for (GaoProductSpecValue value : valueMap.getOrDefault(spec.getId(), List.of())) { + if (!selectedValueIdSet.contains(value.getId())) { + continue; + } + ObjectNode snapshot = result.addObject(); + snapshot.put("specId", spec.getId()); + snapshot.put("specName", spec.getName()); + snapshot.put("specValueId", value.getId()); + snapshot.put("specValueName", value.getName()); + } + } + return result; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoProductSku sku) { + prepare(sku, null); + super.create(sku); + saveSpecValues(sku.getId(), sku.getSpecValueIdList()); + saveUnits(sku.getId(), sku.getUnitList(), List.of()); + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT_SKU, sku.getId(), sku.getAttachmentList()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoProductSku request) { + TimiException.required(request, "not found product sku"); + TimiException.required(request.getId(), "not found product sku.id"); + GaoProductSku sku = get(request.getId()); + TimiException.required(sku, "not found product sku"); + sku.setCode(TimiJava.defaultIfNull(request.getCode(), sku.getCode())); + sku.setBarcode(request.getBarcode()); + sku.setName(TimiJava.defaultIfNull(request.getName(), sku.getName())); + sku.setSpecSummary(TimiJava.defaultIfNull(request.getSpecSummary(), sku.getSpecSummary())); + sku.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), sku.getStockEnabled())); + sku.setAllowDecimalQuantity(TimiJava.defaultIfNull(request.getAllowDecimalQuantity(), sku.getAllowDecimalQuantity())); + sku.setMinStockQuantity(request.getMinStockQuantity()); + sku.setStatus(TimiJava.defaultIfNull(request.getStatus(), sku.getStatus())); + sku.setSort(TimiJava.defaultIfNull(request.getSort(), sku.getSort())); + sku.setRemark(request.getRemark()); + sku.setSpecValueIdList(request.getSpecValueIdList() == null ? currentSpecValueIds(sku.getId()) : request.getSpecValueIdList()); + sku.setUnitList(request.getUnitList() == null ? unitMapper.selectBySkuId(sku.getId()) : request.getUnitList()); + prepare(sku, sku.getId()); + super.update(sku); + if (request.getSpecValueIdList() != null) { + saveSpecValues(sku.getId(), sku.getSpecValueIdList()); + } + if (request.getUnitList() != null) { + saveUnits(sku.getId(), sku.getUnitList(), unitMapper.selectBySkuId(sku.getId())); + } + if (request.getAttachmentList() != null) { + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT_SKU, sku.getId(), request.getAttachmentList()); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void disable(String id) { + GaoProductSku sku = get(id); + TimiException.required(sku, "not found product sku"); + sku.setStatus(GaoProductSku.Status.INACTIVE); + sku.setUpdatedBy(userLoginService.getRequireLoginUserId()); + sku.setUpdatedAt(Time.now()); + super.update(sku); + } + + @Override + public void delete(String id) { + disable(id); + } + + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "已建立 SKU 的产品不允许物理删除"); + } + + private void prepare(GaoProductSku sku, String excludeId) { + TimiException.required(sku, "not found product sku"); + GaoProduct product = productServiceProvider.getObject().get(sku.getProductId()); + TimiException.required(product, "产品不存在"); + sku.setStoreId(product.getStoreId()); + sku.setCode(requiredText(sku.getCode(), "SKU 编码不能为空")); + sku.setName(requiredText(sku.getName(), "SKU 名称不能为空")); + sku.setBarcode(trimToNull(sku.getBarcode())); + sku.setSpecSummary(trimToNull(sku.getSpecSummary())); + sku.setStockEnabled(TimiJava.defaultIfNull(sku.getStockEnabled(), product.getStockEnabled())); + TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !sku.getStockEnabled(), "SERVICE 产品的 SKU 不能启用库存"); + sku.setAllowDecimalQuantity(TimiJava.defaultIfNull(sku.getAllowDecimalQuantity(), false)); + sku.setStatus(TimiJava.defaultIfNull(sku.getStatus(), GaoProductSku.Status.ACTIVE)); + if (!Boolean.TRUE.equals(sku.getStockEnabled())) { + TimiException.requiredNull(sku.getMinStockQuantity(), "未启用库存的 SKU 不能填写最低库存"); + } + if (sku.getMinStockQuantity() != null) { + TimiException.requiredTrue(0 <= sku.getMinStockQuantity().signum(), "最低库存不能为负数"); + TimiException.requiredTrue(3 >= sku.getMinStockQuantity().scale(), "最低库存最多保留三位小数"); + } + sku.setSort(TimiJava.defaultIfNull(sku.getSort(), 0)); + TimiException.requiredNull(mapper.selectByStoreIdAndCode(sku.getStoreId(), sku.getCode(), excludeId), "门店内 SKU 编码不能重复"); + if (sku.getBarcode() != null) { + TimiException.requiredNull(mapper.selectByStoreIdAndBarcode(sku.getStoreId(), sku.getBarcode(), excludeId), "门店内 SKU 条码不能重复"); + } + validateSpecValues(sku); + sku.setSpecValueKey(buildSpecValueKey(sku.getSpecValueIdList())); + TimiException.requiredNull(mapper.selectByProductIdAndSpecValueKey(sku.getProductId(), sku.getSpecValueKey(), excludeId), "同一产品的 SKU 规格组合不能重复"); + sku.setUnitList(validateAndNormalizeUnits(sku.getUnitList())); + sku.setUpdatedBy(userLoginService.getRequireLoginUserId()); + } + + private void validateSpecValues(GaoProductSku sku) { + List specValueIdList = sku.getSpecValueIdList() == null ? List.of() : sku.getSpecValueIdList(); + TimiException.requiredTrue(specValueIdList.size() == new HashSet<>(specValueIdList).size(), "SKU 规格值不能重复"); + Set specIdSet = new HashSet<>(); + for (String specValueId : specValueIdList) { + GaoProductSpecValue value = valueMapper.selectById(specValueId); + TimiException.required(value, "SKU 规格值不存在"); + TimiException.requiredTrue(Boolean.TRUE.equals(value.getEnabled()), "SKU 规格值已停用"); + TimiException.requiredTrue(sku.getProductId().equals(specMapper.selectProductIdBySpecValueId(specValueId)), "SKU 规格值不属于当前产品"); + TimiException.requiredTrue(specIdSet.add(value.getSpecId()), "同一规格项只能选择一个规格值"); + } + Set requiredSpecIdSet = specMapper.selectByProductId(sku.getProductId()).stream().map(GaoProductSpec::getId).collect(Collectors.toSet()); + TimiException.requiredTrue(requiredSpecIdSet.equals(specIdSet), "SKU 必须为每个规格项选择一个规格值"); + } + + private String buildSpecValueKey(List specValueIdList) { + if (specValueIdList == null || specValueIdList.isEmpty()) { + return "DEFAULT"; + } + return specValueIdList.stream().sorted().collect(Collectors.joining(",")); + } + + private List validateAndNormalizeUnits(List sourceList) { + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "SKU 至少需要一个单位"); + List result = new ArrayList<>(); + Set nameSet = new HashSet<>(); + Set idSet = new HashSet<>(); + int baseCount = 0; + for (int i = 0; i < sourceList.size(); i++) { + GaoProductSkuUnit source = sourceList.get(i); + TimiException.required(source, "SKU 单位不能为空"); + GaoProductSkuUnit unit = new GaoProductSkuUnit(); + unit.setId(source.getId()); + if (unit.getId() != null) { + TimiException.requiredTrue(idSet.add(unit.getId()), "SKU 单位 ID 不能重复"); + } + unit.setName(requiredText(source.getName(), "SKU 单位名称不能为空")); + TimiException.requiredTrue(nameSet.add(unit.getName()), "同一 SKU 的单位名称不能重复"); + unit.setBaseQuantityPerUnit(TimiJava.defaultIfNull(source.getBaseQuantityPerUnit(), ONE)); + TimiException.requiredTrue(0 < unit.getBaseQuantityPerUnit().signum(), "基础单位数量系数必须大于 0"); + TimiException.requiredTrue(6 >= unit.getBaseQuantityPerUnit().scale(), "基础单位数量系数最多保留六位小数"); + if (source.getSalePrice() != null) { + TimiException.requiredTrue(0 <= source.getSalePrice(), "单位建议售价不能为负数"); + } + unit.setSalePrice(source.getSalePrice()); + unit.setIsBase(Boolean.TRUE.equals(source.getIsBase())); + unit.setAllowPurchase(TimiJava.defaultIfNull(source.getAllowPurchase(), true)); + unit.setAllowSale(TimiJava.defaultIfNull(source.getAllowSale(), true)); + unit.setSort(TimiJava.defaultIfNull(source.getSort(), i)); + if (unit.getIsBase()) { + baseCount++; + TimiException.requiredTrue(1 >= baseCount, "一个 SKU 只能有一个基础单位"); + TimiException.requiredTrue(ONE.compareTo(unit.getBaseQuantityPerUnit()) == 0, "基础单位数量系数必须为 1"); + } + result.add(unit); + } + TimiException.requiredTrue(0 < baseCount, "SKU 必须有一个基础单位"); + return result; + } + + private void saveUnits(String skuId, List unitList, List existingUnitList) { + Map existingById = existingUnitList.stream().collect(Collectors.toMap(GaoProductSkuUnit::getId, item -> item)); + Set retainedIdSet = new HashSet<>(); + for (GaoProductSkuUnit source : unitList) { + GaoProductSkuUnit existing = source.getId() == null ? null : existingById.get(source.getId()); + if (source.getId() != null && existing == null) { + throw new TimiException(TimiCode.ERROR, "SKU 单位不存在或不属于当前 SKU"); + } + GaoProductSkuUnit unit = new GaoProductSkuUnit(); + unit.setId(existing == null ? null : existing.getId()); + unit.setSkuId(skuId); + unit.setName(source.getName()); + unit.setBaseQuantityPerUnit(source.getBaseQuantityPerUnit()); + unit.setSalePrice(source.getSalePrice()); + unit.setIsBase(source.getIsBase()); + unit.setAllowPurchase(source.getAllowPurchase()); + unit.setAllowSale(source.getAllowSale()); + unit.setSort(source.getSort()); + if (existing == null) { + unitMapper.insert(unit); + } else { + retainedIdSet.add(existing.getId()); + TimiException.requiredTrue(1 == unitMapper.updateUnit(unit), "SKU 单位更新失败"); + } + } + for (GaoProductSkuUnit existing : existingUnitList) { + if (!retainedIdSet.contains(existing.getId())) { + unitMapper.delete(existing.getId()); + } + } + } + + private void saveSpecValues(String skuId, List specValueIdList) { + skuSpecValueMapper.deleteBySkuId(skuId); + for (String specValueId : specValueIdList == null ? List.of() : specValueIdList) { + GaoProductSkuSpecValue relation = new GaoProductSkuSpecValue(); + relation.setSkuId(skuId); + relation.setSpecValueId(specValueId); + skuSpecValueMapper.insert(relation); + } + } + + private List currentSpecValueIds(String skuId) { + return skuSpecValueMapper.selectBySkuIdList(List.of(skuId)).stream().map(GaoProductSkuSpecValue::getSpecValueId).toList(); + } + + private void fillTransient(List skuList) { + if (skuList.isEmpty()) return; + List skuIdList = skuList.stream().map(GaoProductSku::getId).toList(); + Map> unitMap = unitMapper.selectBySkuIdList(skuIdList).stream().collect(Collectors.groupingBy(GaoProductSkuUnit::getSkuId)); + Map> specValueIdMap = skuSpecValueMapper.selectBySkuIdList(skuIdList) + .stream() + .collect(Collectors.groupingBy(GaoProductSkuSpecValue::getSkuId, Collectors.mapping(GaoProductSkuSpecValue::getSpecValueId, Collectors.toList()))); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PRODUCT_SKU, skuIdList); + for (GaoProductSku sku : skuList) { + sku.setUnitList(unitMap.getOrDefault(sku.getId(), List.of())); + sku.setSpecValueIdList(specValueIdMap.getOrDefault(sku.getId(), List.of())); + sku.setAttachmentList(attachmentMap.getOrDefault(sku.getId(), List.of())); + } + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private String trimToNull(String value) { + return value == null || value.trim().isEmpty() ? null : value.trim(); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSpecServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSpecServiceImplement.java new file mode 100644 index 0000000..15669e8 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSpecServiceImplement.java @@ -0,0 +1,65 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecValueMapper; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSpecService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/// GAO 产品规格服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoProductSpecServiceImplement implements GaoProductSpecService { + + private final GaoProductSpecMapper specMapper; + private final GaoProductSpecValueMapper valueMapper; + private final GaoProductService productService; + + @Override + public List listByProductId(String productId) { + List specList = specMapper.selectByProductId(productId); + Map> valueMap = valueMapper.selectBySpecIdList(specList.stream().map(GaoProductSpec::getId).toList()).stream().collect(Collectors.groupingBy(GaoProductSpecValue::getSpecId)); + for (GaoProductSpec spec : specList) spec.setValueList(valueMap.getOrDefault(spec.getId(), List.of())); + return specList; + } + + @Override + public String getProductIdBySpecId(String specId) { + return specMapper.selectProductIdBySpecId(specId); + } + + @Override + public void createSpec(GaoProductSpec spec) { + TimiException.required(spec, "not found product spec"); + GaoProduct product = productService.get(spec.getProductId()); + TimiException.required(product, "产品不存在"); + spec.setName(requiredText(spec.getName(), "规格名称不能为空")); + spec.setSort(TimiJava.defaultIfNull(spec.getSort(), 0)); + specMapper.insert(spec); + } + + @Override + public void createValue(GaoProductSpecValue value) { + TimiException.required(value, "not found product spec value"); + TimiException.required(specMapper.selectProductIdBySpecId(value.getSpecId()), "规格项不存在"); + value.setName(requiredText(value.getName(), "规格值名称不能为空")); + value.setEnabled(TimiJava.defaultIfNull(value.getEnabled(), true)); + value.setSort(TimiJava.defaultIfNull(value.getSort(), 0)); + valueMapper.insert(value); + } + + private String requiredText(String value, String message) { TimiException.required(value, message); String result = value.trim(); TimiException.requiredTrue(!result.isEmpty(), message); return result; } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java new file mode 100644 index 0000000..1cc297d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java @@ -0,0 +1,142 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.mapper.GaoProductStockRecordMapper; +import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; +import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/// GAO 库存流水服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoProductStockRecordServiceImplement extends AbstractEntityService implements GaoProductStockRecordService { + + private final GaoProductStockRecordMapper mapper; + private final AttachmentService attachmentService; + private final UserService userService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoProductStockRecordPage page) { + TimiException.required(page, "not found stock record page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + fillTransient(result.getList()); + return result; + } + + @Override + public GaoProductStockRecord detail(String id) { + TimiException.required(id, "not found stock record id"); + GaoProductStockRecord result = get(id); + TimiException.required(result, "not found stock record"); + GaoProductStockRecord revoke = mapper.selectRevokeBySourceRecordId(result.getId()); + result.setRevokeRecordId(revoke == null ? null : revoke.getId()); + fillTransient(List.of(result)); + return result; + } + + @Override + public List listByBiz(GaoProductStockRecord.BizType bizType, String bizId) { + TimiException.required(bizType, "not found stock bizType"); + TimiException.required(bizId, "not found stock bizId"); + List result = mapper.selectByBiz(bizType, bizId); + fillTransient(result); + return result; + } + + @Override + public List listByRequestNo(String storeId, String requestNo) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(requestNo, "not found requestNo"); + return mapper.selectByRequestNo(storeId, requestNo); + } + + @Override + public GaoProductStockRecord getForUpdate(String id) { + TimiException.required(id, "not found stock record id"); + return mapper.selectForUpdate(id); + } + + @Override + public GaoProductStockRecord getRevokeBySourceRecordId(String sourceRecordId) { + TimiException.required(sourceRecordId, "not found sourceRecordId"); + return mapper.selectRevokeBySourceRecordId(sourceRecordId); + } + + @Override + public GaoProductStockRecord getByBizSku(GaoProductStockRecord.BizType bizType, String bizId, String skuId, + GaoProductStockRecord.OperationType operationType) { + TimiException.required(bizType, "not found stock bizType"); + TimiException.required(bizId, "not found stock bizId"); + TimiException.required(skuId, "not found skuId"); + TimiException.required(operationType, "not found operationType"); + return mapper.selectByBizSku(bizType, bizId, skuId, operationType); + } + + @Override + public void createOperationRecord(GaoProductStockRecord record, List attachmentList) { + TimiException.required(record, "not found stock record"); + mapper.insert(record); + if (attachmentList != null && !attachmentList.isEmpty()) { + attachmentService.updateByBizId(Attachment.BizType.GAO_STOCK_RECORD, record.getId(), attachmentList); + } + } + + @Override + public void create(GaoProductStockRecord record) { + throw new TimiException(TimiCode.ERROR, "库存流水必须通过库存操作创建"); + } + + @Override + public void update(GaoProductStockRecord record) { + throw new TimiException(TimiCode.ERROR, "库存流水不允许修改"); + } + + @Override + public void delete(String id) { + throw new TimiException(TimiCode.ERROR, "库存流水不允许删除"); + } + + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "库存流水不允许物理删除"); + } + + private void fillTransient(List recordList) { + if (recordList == null || recordList.isEmpty()) { + return; + } + List idList = recordList.stream().map(GaoProductStockRecord::getId).toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_STOCK_RECORD, idList); + List userIdList = recordList.stream().map(GaoProductStockRecord::getOperatorUserId) + .filter(value -> value != null && !value.isBlank()).distinct().toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoProductStockRecord record : recordList) { + record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); + record.setOperator(userMap.get(record.getOperatorUserId())); + } + } + +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java new file mode 100644 index 0000000..b2ff198 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java @@ -0,0 +1,489 @@ +package com.imyeyu.api.modules.gao.service.implement; + +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.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductStockService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; +import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; +import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/// GAO 产品库存服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoProductStockServiceImplement implements GaoProductStockService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoProductStockMapper stockMapper; + private final AttachmentService attachmentService; + private final GaoProductStockRecordService recordService; + private final GaoProductService productService; + private final GaoProductSkuService skuService; + private final GaoStoreScopeService storeScopeService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + public PageResult pageByQuery(GaoProductStockPage page) { + TimiException.required(page, "not found stock page"); + PageResult result = new PageResult<>(); + result.setTotal(stockMapper.countByQuery(page)); + List list = stockMapper.selectByQuery(page); + Map photoMap = attachmentService.mapFirstImageByIdList( + Attachment.BizType.GAO_PRODUCT_SKU, + list.stream().map(GaoProductStock::getSkuId).filter(id -> id != null && !id.isBlank()).toList() + ); + for (GaoProductStock stock : list) { + fillBalance(stock); + if (stock.getSku() != null) { + Attachment photo = photoMap.get(stock.getSkuId()); + stock.getSku().setAttachmentList(photo == null ? List.of() : List.of(photo)); + } + } + result.setList(list); + return result; + } + + @Override + public PageResult pageProductByQuery(GaoProductStockPage page) { + TimiException.required(page, "not found stock product page"); + PageResult result = new PageResult<>(); + result.setTotal(stockMapper.countProductByQuery(page)); + result.setList(stockMapper.selectProductByQuery(page)); + return result; + } + + @Override + public GaoProductStock detail(String storeId, String skuId) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(skuId, "not found skuId"); + storeScopeService.checkStoreId(storeId); + GaoProductSku sku = skuService.get(skuId); + TimiException.required(sku, "not found sku"); + TimiException.requiredTrue(storeId.equals(sku.getStoreId()), "SKU 不属于当前门店"); + GaoProductStock stock = stockMapper.selectByStoreIdAndSkuId(storeId, skuId); + if (stock == null) { + stock = new GaoProductStock(); + stock.setStoreId(storeId); + stock.setSkuId(skuId); + stock.setOnHandQuantity(ZERO); + stock.setReservedQuantity(ZERO); + } + stock.setSku(sku); + fillBalance(stock); + return stock; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock initialIn(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_INITIAL_IN", request, () -> change(request, GaoProductStockRecord.OperationType.INITIAL_IN)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock manualIn(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_IN", request, () -> change(request, GaoProductStockRecord.OperationType.MANUAL_IN)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock manualOut(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_OUT", request, () -> change(request, GaoProductStockRecord.OperationType.MANUAL_OUT)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock manualRevoke(GaoProductStockManualRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_REVOKE", request, () -> manualRevokeInternal(request)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock stocktakeGain(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_GAIN", request, () -> change(request, GaoProductStockRecord.OperationType.STOCKTAKE_GAIN)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock stocktakeLoss(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_LOSS", request, () -> change(request, GaoProductStockRecord.OperationType.STOCKTAKE_LOSS)); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public boolean hasSufficientForFulfillment(String storeId, List items) { + TimiException.required(storeId, "not found storeId"); + if (items == null || items.isEmpty()) { + return true; + } + List sortedItems = new ArrayList<>(items); + sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getSkuId)); + for (GaoStockChangeItem item : sortedItems) { + GaoProductStock stock = stockMapper.selectForUpdate(storeId, item.getSkuId()); + if (stock == null || decimal(stock.getOnHandQuantity()).subtract(decimal(stock.getReservedQuantity())).compareTo(item.getBaseQuantity()) < 0) { + return false; + } + } + return true; + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List reserveForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_RESERVE, null); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List releaseForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_RELEASE, null); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List outForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_OUT, null); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List revokeForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_REVOKE, null); + } + + private GaoProductStock change(GaoProductStockChangeRequest request, GaoProductStockRecord.OperationType operationType) { + TimiException.required(request, "not found stock change request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String storeId = storeScopeService.resolveStoreId(request.getStoreId()); + TimiException.required(storeId, "not found storeId"); + boolean purchase = operationType == GaoProductStockRecord.OperationType.INITIAL_IN + || operationType == GaoProductStockRecord.OperationType.MANUAL_IN + || operationType == GaoProductStockRecord.OperationType.STOCKTAKE_GAIN; + GaoProductSkuUnitSelection selection = skuService.getSkuUnit(storeId, request.getSkuId(), request.getSkuUnitId(), purchase); + GaoProductSku sku = selection.getSku(); + GaoProduct product = selection.getProduct(); + GaoProductSkuUnit unit = selection.getUnit(); + TimiException.requiredTrue(Boolean.TRUE.equals(sku.getStockEnabled()), "该 SKU 未启用库存管理"); + BigDecimal quantity = validateQuantity(sku, request.getQuantity()); + BigDecimal baseQuantity = quantity.multiply(unit.getBaseQuantityPerUnit()); + List existingList = recordService.listByRequestNo(storeId, requestNo); + if (!existingList.isEmpty()) { + GaoProductStockRecord existing = existingList.getFirst(); + verifyExisting(existing, operationType, sku.getId(), unit.getId(), quantity, baseQuantity, request.getRemark()); + return detail(storeId, sku.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片"); + TimiException.requiredTrue(hasText(request.getRemark()), "库存操作必须填写备注"); + GaoStockChangeItem item = GaoStockChangeItem.of(product, sku, unit, quantity, baseQuantity); + item.setSkuSpecSnapshot(skuService.getSkuSpecSnapshot(sku)); + List result = executeInternal(storeId, sku.getId(), List.of(item), requestNo, + userLoginService.getRequireLoginUserId(), operationType, request.getRemark(), request.getAttachmentList()); + return result.getFirst(); + } + + private GaoProductStock manualRevokeInternal(GaoProductStockManualRevokeRequest request) { + TimiException.required(request, "not found stock manual revoke request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String sourceRecordId = requiredText(request.getSourceRecordId(), "not found sourceRecordId"); + String remark = requiredText(request.getRemark(), "库存流水冲正备注不能为空"); + GaoProductStockRecord source = recordService.getForUpdate(sourceRecordId); + TimiException.required(source, "not found source stock record"); + TimiException.requiredTrue(source.getBizType() == GaoProductStockRecord.BizType.PRODUCT_STOCK, "原库存流水不是手工库存流水"); + TimiException.requiredTrue(source.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_IN + || source.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_OUT, "只有手工入库或手工出库流水可以冲正"); + TimiException.requiredNull(source.getSourceRecordId(), "只能冲正原始手工库存流水"); + String storeId = requiredText(source.getStoreId(), "原库存流水缺少门店"); + String skuId = requiredText(source.getSkuId(), "原库存流水缺少 SKU"); + storeScopeService.checkStoreId(storeId); + + List existingList = recordService.listByRequestNo(storeId, requestNo); + if (!existingList.isEmpty()) { + verifyManualRevokeExisting(existingList, sourceRecordId, remark); + return detail(storeId, skuId); + } + TimiException.requiredNull(recordService.getRevokeBySourceRecordId(sourceRecordId), "该手工库存流水已经冲正"); + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存流水冲正必须上传照片"); + TimiException.required(source.getQuantity(), "原库存流水缺少业务数量"); + TimiException.required(source.getBaseQuantity(), "原库存流水缺少基础单位数量"); + TimiException.required(source.getOnHandDelta(), "原库存流水缺少库存变动"); + BigDecimal sourceReservedDelta = decimal(source.getReservedDelta()); + TimiException.requiredTrue(sourceReservedDelta.signum() == 0, "手工库存流水不能包含预占变动"); + + GaoProductStock stock = lockOrCreateStock(storeId, skuId); + BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); + BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); + BigDecimal onHandDelta = source.getOnHandDelta().negate(); + BigDecimal reservedDelta = sourceReservedDelta.negate(); + int updated = stockMapper.updateQuantity(stock.getId(), onHandDelta, reservedDelta, userLoginService.getRequireLoginUserId()); + TimiException.requiredTrue(updated == 1, "库存余额已变化,无法冲正该流水"); + BigDecimal onHandAfter = onHandBefore.add(onHandDelta); + BigDecimal reservedAfter = reservedBefore.add(reservedDelta); + GaoProductStockRecord revoke = new GaoProductStockRecord(); + revoke.setStoreId(storeId); + revoke.setSkuId(skuId); + revoke.setSkuUnitId(source.getSkuUnitId()); + revoke.setProductIdSnapshot(source.getProductIdSnapshot()); + revoke.setProductNameSnapshot(source.getProductNameSnapshot()); + revoke.setSkuCodeSnapshot(source.getSkuCodeSnapshot()); + revoke.setSkuNameSnapshot(source.getSkuNameSnapshot()); + revoke.setSpecSummarySnapshot(source.getSpecSummarySnapshot()); + revoke.setSkuSpecSnapshot(source.getSkuSpecSnapshot()); + revoke.setUnitSnapshot(source.getUnitSnapshot()); + revoke.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); + revoke.setQuantity(source.getQuantity()); + revoke.setBaseQuantity(source.getBaseQuantity()); + revoke.setOperationType(GaoProductStockRecord.OperationType.MANUAL_REVOKE); + revoke.setOnHandDelta(onHandDelta); + revoke.setReservedDelta(reservedDelta); + revoke.setOnHandBefore(onHandBefore); + revoke.setOnHandAfter(onHandAfter); + revoke.setReservedBefore(reservedBefore); + revoke.setReservedAfter(reservedAfter); + revoke.setBizType(GaoProductStockRecord.BizType.PRODUCT_STOCK); + revoke.setBizId(source.getBizId()); + revoke.setSourceRecordId(source.getId()); + revoke.setRequestNo(requestNo); + revoke.setOperatorUserId(userLoginService.getRequireLoginUserId()); + revoke.setOccurredAt(Time.now()); + revoke.setRemark(remark); + recordService.createOperationRecord(revoke, request.getAttachmentList()); + stock.setOnHandQuantity(onHandAfter); + stock.setReservedQuantity(reservedAfter); + stock.setUpdatedBy(revoke.getOperatorUserId()); + fillBalance(stock); + return stock; + } + + private List executeInternal(String storeId, String bizId, List sourceList, String requestNo, + String operatorUserId, GaoProductStockRecord.OperationType operationType, String remark) { + return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, remark, null); + } + + private List executeInternal(String storeId, String bizId, List sourceList, String requestNo, + String operatorUserId, GaoProductStockRecord.OperationType operationType, String remark, List attachmentList) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(bizId, "not found stock bizId"); + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "库存变更至少需要一个产品"); + String normalizedRequestNo = requiredText(requestNo, "not found requestNo"); + storeScopeService.checkStoreId(storeId); + List itemList = new ArrayList<>(sourceList); + itemList.sort(Comparator.comparing(GaoStockChangeItem::getSkuId)); + Set skuIdSet = new HashSet<>(); + for (GaoStockChangeItem item : itemList) { + TimiException.required(item, "库存变更项不能为空"); + TimiException.requiredTrue(skuIdSet.add(item.getSkuId()), "同一库存操作不能重复 SKU"); + TimiException.required(item.getBaseQuantity(), "基础单位数量不能为空"); + TimiException.requiredTrue(0 < item.getBaseQuantity().signum(), "基础单位数量必须大于 0"); + } + List existingList = recordService.listByRequestNo(storeId, normalizedRequestNo); + if (!existingList.isEmpty()) { + verifyExistingList(existingList, itemList, operationType, bizId); + List result = new ArrayList<>(); + for (GaoStockChangeItem item : itemList) { + result.add(detail(storeId, item.getSkuId())); + } + return result; + } + List result = new ArrayList<>(); + for (GaoStockChangeItem item : itemList) { + GaoProductStock stock = lockOrCreateStock(storeId, item.getSkuId()); + BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); + BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); + BigDecimal onHandDelta = onHandDelta(operationType, item.getBaseQuantity()); + BigDecimal reservedDelta = reservedDelta(operationType, item.getBaseQuantity()); + int updated = stockMapper.updateQuantity(stock.getId(), onHandDelta, reservedDelta, operatorUserId); + TimiException.requiredTrue(updated == 1, "库存余额已变化或库存数量不足"); + BigDecimal onHandAfter = onHandBefore.add(onHandDelta); + BigDecimal reservedAfter = reservedBefore.add(reservedDelta); + GaoProductStockRecord record = new GaoProductStockRecord(); + record.setStoreId(storeId); + record.setSkuId(item.getSkuId()); + record.setSkuUnitId(item.getSkuUnitId()); + record.setProductIdSnapshot(item.getProductIdSnapshot()); + record.setProductNameSnapshot(item.getProductNameSnapshot()); + record.setSkuCodeSnapshot(item.getSkuCodeSnapshot()); + record.setSkuNameSnapshot(item.getSkuNameSnapshot()); + record.setSpecSummarySnapshot(item.getSpecSummarySnapshot()); + record.setSkuSpecSnapshot(item.getSkuSpecSnapshot()); + record.setUnitSnapshot(item.getUnitSnapshot()); + record.setBaseQuantityPerUnitSnapshot(item.getBaseQuantityPerUnitSnapshot()); + record.setQuantity(item.getQuantity()); + record.setBaseQuantity(item.getBaseQuantity()); + record.setOperationType(operationType); + record.setOnHandDelta(onHandDelta); + record.setReservedDelta(reservedDelta); + record.setOnHandBefore(onHandBefore); + record.setOnHandAfter(onHandAfter); + record.setReservedBefore(reservedBefore); + record.setReservedAfter(reservedAfter); + record.setBizType(operationType.name().startsWith("ORDER_") ? GaoProductStockRecord.BizType.ORDER_FULFILLMENT : GaoProductStockRecord.BizType.PRODUCT_STOCK); + record.setBizId(bizId); + record.setSourceRecordId(item.getSourceRecordId()); + record.setRequestNo(normalizedRequestNo); + record.setOperatorUserId(operatorUserId); + record.setOccurredAt(Time.now()); + record.setRemark(remark); + recordService.createOperationRecord(record, attachmentList); + stock.setOnHandQuantity(onHandAfter); + stock.setReservedQuantity(reservedAfter); + stock.setUpdatedBy(operatorUserId); + fillBalance(stock); + result.add(stock); + } + return result; + } + + private GaoProductStock lockOrCreateStock(String storeId, String skuId) { + GaoProductStock stock = stockMapper.selectForUpdate(storeId, skuId); + if (stock != null) { + return stock; + } + stock = new GaoProductStock(); + stock.setId(UUID.randomUUID().toString()); + stock.setStoreId(storeId); + stock.setSkuId(skuId); + stock.setOnHandQuantity(ZERO); + stock.setReservedQuantity(ZERO); + stock.setUpdatedBy(userLoginService.getRequireLoginUserId()); + stock.setCreatedAt(Time.now()); + stockMapper.insertIgnore(stock); + stock = stockMapper.selectForUpdate(storeId, skuId); + TimiException.required(stock, "库存余额创建失败"); + return stock; + } + + private BigDecimal onHandDelta(GaoProductStockRecord.OperationType operationType, BigDecimal quantity) { + return switch (operationType) { + case INITIAL_IN, MANUAL_IN, STOCKTAKE_GAIN, ORDER_REVOKE -> quantity; + case MANUAL_OUT, STOCKTAKE_LOSS, ORDER_OUT -> quantity.negate(); + case ORDER_RESERVE, ORDER_RELEASE -> ZERO; + case MANUAL_REVOKE -> throw new TimiException(TimiCode.ERROR, "手工库存冲正必须基于原库存流水"); + }; + } + + private BigDecimal reservedDelta(GaoProductStockRecord.OperationType operationType, BigDecimal quantity) { + return switch (operationType) { + case ORDER_RESERVE -> quantity; + case ORDER_RELEASE, ORDER_OUT -> quantity.negate(); + default -> ZERO; + }; + } + + private BigDecimal validateQuantity(GaoProductSku sku, BigDecimal quantity) { + TimiException.required(quantity, "数量不能为空"); + TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0"); + TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "数量最多保留三位小数"); + if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + } + return quantity; + } + + private void verifyExisting(GaoProductStockRecord existing, GaoProductStockRecord.OperationType operationType, String skuId, String unitId, + BigDecimal quantity, BigDecimal baseQuantity, String remark) { + TimiException.requiredTrue(existing.getOperationType() == operationType + && skuId.equals(existing.getSkuId()) + && unitId.equals(existing.getSkuUnitId()) + && quantity.compareTo(existing.getQuantity()) == 0 + && baseQuantity.compareTo(existing.getBaseQuantity()) == 0 + && sameText(existing.getRemark(), remark), "requestNo 对应库存操作内容不一致"); + } + + private void verifyManualRevokeExisting(List existingList, String sourceRecordId, String remark) { + TimiException.requiredTrue(existingList.size() == 1, "requestNo 对应库存操作内容不一致"); + GaoProductStockRecord existing = existingList.getFirst(); + TimiException.requiredTrue(existing.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_REVOKE + && sourceRecordId.equals(existing.getSourceRecordId()) + && sameText(existing.getRemark(), remark), "requestNo 对应库存操作内容不一致"); + } + + private void verifyExistingList(List existingList, List itemList, GaoProductStockRecord.OperationType operationType, String bizId) { + Map recordMap = new HashMap<>(); + for (GaoProductStockRecord record : existingList) { + TimiException.requiredTrue(record.getOperationType() == operationType && bizId.equals(record.getBizId()), "requestNo 已被其他库存操作使用"); + recordMap.put(record.getSkuId(), record); + } + TimiException.requiredTrue(recordMap.size() == itemList.size(), "requestNo 对应库存操作内容不一致"); + for (GaoStockChangeItem item : itemList) { + GaoProductStockRecord record = recordMap.get(item.getSkuId()); + TimiException.required(record, "requestNo 对应库存操作内容不一致"); + TimiException.requiredTrue(item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0, "requestNo 对应库存操作内容不一致"); + } + } + + private void fillBalance(GaoProductStock stock) { + stock.setOnHandQuantity(decimal(stock.getOnHandQuantity())); + stock.setReservedQuantity(decimal(stock.getReservedQuantity())); + stock.setAvailableQuantity(stock.getOnHandQuantity().subtract(stock.getReservedQuantity())); + if (stock.getSku() == null && stock.getSkuId() != null) { + stock.setSku(skuService.get(stock.getSkuId())); + } + } + + private boolean hasAttachment(List list) { + for (Attachment attachment : TimiJava.safeIterable(list)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } + + private BigDecimal decimal(BigDecimal value) { + return value == null ? ZERO : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java new file mode 100644 index 0000000..2385ec7 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java @@ -0,0 +1,127 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderChartMapper; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderChartService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderSkuRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// GAO 销售订单图表服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoSalesOrderChartServiceImplement implements GaoSalesOrderChartService { + + private static final int DEFAULT_TOP_N = 20; + private static final int MAX_TOP_N = 100; + + private final GaoSalesOrderChartMapper mapper; + + @Override + public List daily(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectDaily(req); + } + + @Override + public List type(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectType(req); + } + + @Override + public List payment(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectPayment(req); + } + + @Override + public List customerRank(GaoSalesOrderChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectCustomerRank(req); + } + + @Override + public List customerProductRank(GaoSalesOrderChartReq req) { + checkRangeReq(req); + TimiException.required(req.getCustomerId(), "客户订单产品排行必须指定客户"); + normalizeTopN(req); + return mapper.selectCustomerProductRank(req); + } + + @Override + public List customerSkuRank(GaoSalesOrderChartReq req) { + checkRangeReq(req); + TimiException.required(req.getCustomerId(), "客户订单 SKU 排行必须指定客户"); + normalizeTopN(req); + return mapper.selectCustomerSkuRank(req); + } + + @Override + public List status(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectStatus(req); + } + + @Override + public GaoSalesOrderReceivableStatView receivable(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectReceivable(req); + } + + @Override + public List consumptionDaily(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectConsumptionDaily(req); + } + + @Override + public List calendar(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectCalendar(req); + } + + @Override + public List repeatDaily(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectRepeatDaily(req); + } + + @Override + public List repeatInterval(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectRepeatInterval(req); + } + + private void checkRangeReq(GaoSalesOrderChartReq req) { + TimiException.required(req, "订单图表请求不能为空"); + TimiException.required(req.getStoreId(), "订单图表门店不能为空"); + TimiException.required(req.getBeginAt(), "订单图表开始时间不能为空"); + TimiException.required(req.getEndAt(), "订单图表结束时间不能为空"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "订单图表结束时间早于开始时间"); + } + + private void normalizeTopN(GaoSalesOrderChartReq req) { + int topN = req.getTopN() == null ? DEFAULT_TOP_N : req.getTopN(); + TimiException.requiredTrue(0 < topN && topN < MAX_TOP_N + 1, "订单图表排行数量无效"); + req.setTopN(topN); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java new file mode 100644 index 0000000..9d16dc6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java @@ -0,0 +1,639 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.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.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderItemMapper; +import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; +import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +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.GaoProductSkuService; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderUpdateRequest; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.PermissionChecker; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import org.springframework.beans.factory.ObjectProvider; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/// GAO 销售订单服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoSalesOrderServiceImplement extends AbstractEntityService implements GaoSalesOrderService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoSalesOrderMapper mapper; + private final GaoSalesOrderItemMapper itemMapper; + private final GaoFundRecordService fundRecordService; + private final ObjectProvider fulfillmentServiceProvider; + private final GaoCustomerService customerService; + private final GaoProductService productService; + private final GaoProductSkuService skuService; + private final GaoStoreService storeService; + private final GaoStoreScopeService storeScopeService; + private final GaoEventRecordService eventRecordService; + private final GaoPointTriggerService pointTriggerService; + private final AttachmentService attachmentService; + private final PermissionChecker permissionChecker; + private final RoleChecker roleChecker; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoSalesOrderPage page) { + TimiException.required(page, "not found sales order page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + List list = mapper.selectByQuery(page); + fillItemList(list); + fillCustomer(list); + result.setList(list); + return result; + } + + @Override + public GaoSalesOrder detail(String id) { + GaoSalesOrder order = get(id); + TimiException.required(order, "not found sales order"); + storeScopeService.checkStoreId(order.getStoreId()); + List itemList = itemMapper.selectBySalesOrderId(order.getId()); + for (GaoSalesOrderItem item : itemList) { + fillItemBalance(item); + } + fillItemAttachments(itemList); + order.setItemList(itemList); + fillCustomer(List.of(order)); + order.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId())); + order.setFundRecordList(fundRecordService.listByBizId(order.getId())); + if (order.getSourceEventRecordId() != null) { + order.setSourceEventRecord(eventRecordService.get(order.getSourceEventRecordId())); + } + order.setUnpaidAmount(safe(order.getSaleAmount()) - safe(order.getPaidAmount())); + return order; + } + + @Override + public GaoSalesOrder getForUpdate(String id) { + GaoSalesOrder order = mapper.selectForUpdate(id); + TimiException.required(order, "not found sales order"); + storeScopeService.checkStoreId(order.getStoreId()); + return order; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE", request, () -> createDirectInternal(request)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoSalesOrder updateRemarkAndAttachments(GaoSalesOrderUpdateRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_UPDATE", request, () -> { + TimiException.required(request, "not found sales order update request"); + TimiException.required(request.getId(), "not found sales order.id"); + GaoSalesOrder order = getForUpdate(request.getId()); + TimiException.requiredTrue(1 == mapper.updateRemark(order.getId(), request.getRemark()), "订单备注更新失败"); + attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId(), request.getAttachmentList()); + return detail(order.getId()); + }); + } + + private GaoSalesOrder createDirectInternal(GaoDirectProductOrderCreateRequest request) { + TimiException.required(request, "not found direct product order request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String storeId = storeScopeService.resolveStoreId(request.getStoreId()); + TimiException.required(storeId, "not found storeId"); + GaoSalesOrder existing = mapper.selectByRequestNo(storeId, requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.DIRECT_PRODUCT, "requestNo 已被其他订单使用"); + verifyExistingDirectItems(existing, request.getItemList()); + return detail(existing.getId()); + } + GaoCustomer customer = customerService.get(request.getCustomerId()); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店"); + GaoStore store = storeService.get(storeId); + TimiException.required(store, "not found store"); + storeScopeService.checkStore(store); + List itemList = resolveDirectItems(storeId, request.getItemList()); + long saleAmount = 0L; + for (GaoSalesOrderItem item : itemList) { + saleAmount = Math.addExact(saleAmount, item.getLineAmount()); + } + Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now()); + String operatorUserId = userLoginService.getRequireLoginUserId(); + GaoSalesOrder order = createOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.DIRECT_PRODUCT, requestNo, + purchasedAt, operatorUserId, request.getRemark(), saleAmount, itemList); + attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId(), request.getAttachmentList()); + return order; + } + + @Override + public GaoSalesOrder getByStoreIdAndRequestNo(String storeId, String requestNo) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(requestNo, "not found requestNo"); + return mapper.selectByRequestNo(storeId, requestNo); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public GaoSalesOrder createPackageOrder(GaoPackageOrderCreateData data) { + TimiException.required(data, "not found package order data"); + String requestNo = requiredText(data.getRequestNo(), "not found requestNo"); + String storeId = requiredText(data.getStoreId(), "not found storeId"); + TimiException.required(data.getCustomerId(), "not found customerId"); + TimiException.required(data.getPackageConfigId(), "not found packageConfigId"); + TimiException.required(data.getSaleAmount(), "not found saleAmount"); + storeScopeService.checkStoreId(storeId); + GaoSalesOrder existing = mapper.selectByRequestNo(storeId, requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.PACKAGE + && data.getCustomerId().equals(existing.getCustomerId()) + && data.getSaleAmount().equals(existing.getSaleAmount()), "requestNo 已被其他订单使用"); + verifyExistingPackageItems(existing, data.getItemList()); + return detail(existing.getId()); + } + GaoCustomer customer = customerService.get(data.getCustomerId()); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店"); + TimiException.requiredTrue(0 <= data.getSaleAmount(), "成交价不能为负数"); + return createOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.PACKAGE, requestNo, + TimiJava.defaultIfNull(data.getPurchasedAt(), Time.now()), data.getOperatorUserId(), data.getRemark(), + data.getSaleAmount(), data.getItemList()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoSalesOrder cancel(GaoSalesOrderCancelRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CANCEL", request, () -> cancelInternal(request)); + } + + private GaoSalesOrder cancelInternal(GaoSalesOrderCancelRequest request) { + TimiException.required(request, "not found cancel request"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + TimiException.required(request.getReason(), "取消原因不能为空"); + String reason = request.getReason().trim(); + TimiException.requiredTrue(!reason.isEmpty(), "取消原因不能为空"); + GaoSalesOrder order = getForUpdate(request.getSalesOrderId()); + if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) { + return order; + } + TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.ACTIVE, "当前订单不可取消"); + TimiException.requiredTrue(fundRecordService.countPendingReceiveByBizId(order.getId()) == 0, "订单存在待核销收款,不能取消"); + TimiException.requiredTrue(fulfillmentServiceProvider.getObject().countPendingBySalesOrderId(order.getId()) == 0, "订单存在待履约操作,不能取消"); + String userId = userLoginService.getRequireLoginUserId(); + GaoSalesOrder.RefundStatus refundStatus = safe(order.getPaidAmount()) == 0 ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING; + Long cancelledAt = Time.now(); + TimiException.requiredTrue(1 == mapper.cancelIf(order.getId(), GaoSalesOrder.Status.ACTIVE, GaoSalesOrder.Status.CANCELLED, + cancelledAt, userId, reason, refundStatus), "订单状态已变化"); + // 订单取消立即失去有效消费资格,不等待退款完成 + pointTriggerService.revokeBySalesOrder(order, reason); + order.setStatus(GaoSalesOrder.Status.CANCELLED); + order.setCancelledAt(cancelledAt); + order.setCancelOperatorUserId(userId); + order.setCancelReason(reason); + order.setRefundStatus(refundStatus); + return order; + } + + @Override + public void updatePayment(GaoSalesOrder order) { + TimiException.required(order, "not found sales order"); + TimiException.requiredTrue(1 == mapper.updatePayment(order.getId(), safeLong(order.getPendingPaidAmount()), safeLong(order.getPaidAmount()), + order.getPaidAt(), safeLong(order.getRefundedAmount()), order.getPaymentStatus(), order.getRefundStatus()), "订单付款汇总更新失败"); + } + + @Override + public void recomputePaymentStatus(GaoSalesOrder order) { + long saleAmount = safeLong(order.getSaleAmount()); + long paidAmount = safeLong(order.getPaidAmount()); + long pendingAmount = safeLong(order.getPendingPaidAmount()); + TimiException.requiredTrue(0 <= paidAmount && paidAmount <= saleAmount, "有效收款超出订单金额"); + TimiException.requiredTrue(0 <= pendingAmount && pendingAmount + paidAmount <= saleAmount, "待核销收款超出订单金额"); + order.setPaymentStatus(saleAmount == 0 || paidAmount == saleAmount + ? 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 + public void checkFulfillmentPermission(GaoSalesOrder order) { + TimiException.required(order, "not found sales order"); + if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) { + return; + } + TimiException.requiredTrue(0 < safe(order.getPaidAmount()), "订单未有已核销收款,不能履约"); + roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name()); + } + + @Override + public int updateItemReservedQuantity(String itemId, BigDecimal delta) { + return itemMapper.updateReservedQuantity(itemId, delta); + } + + @Override + public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) { + return itemMapper.updateFulfilledQuantity(itemId, delta); + } + + @Override + public GaoSalesOrderItem getItemForUpdate(String itemId) { + TimiException.required(itemId, "not found salesOrderItemId"); + return itemMapper.selectForUpdate(itemId); + } + + @Override + public void refreshFulfillmentStatus(GaoSalesOrder order) { + TimiException.required(order, "not found sales order"); + if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) { + return; + } + List itemList = itemMapper.selectBySalesOrderId(order.getId()); + boolean fulfilled = !itemList.isEmpty(); + for (GaoSalesOrderItem item : itemList) { + if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) { + fulfilled = false; + break; + } + } + updateFulfillmentStatus(order, fulfilled); + } + + @Override + public void updateFulfillmentStatus(GaoSalesOrder order, boolean fulfilled) { + TimiException.required(order, "not found sales order"); + if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) { + return; + } + GaoSalesOrder.Status status = fulfilled ? GaoSalesOrder.Status.FULFILLED : GaoSalesOrder.Status.ACTIVE; + if (order.getStatus() != status) { + mapper.updateStatus(order.getId(), status); + order.setStatus(status); + } + } + + @Override + public List listItemByOrderId(String orderId) { + TimiException.required(orderId, "not found salesOrderId"); + return itemMapper.selectBySalesOrderId(orderId); + } + + @Override + public Map mapByIdList(Collection idList) { + if (idList == null || idList.isEmpty()) { + return Map.of(); + } + List orderList = mapper.selectByIdList(idList); + fillItemList(orderList); + return orderList.stream().collect(java.util.stream.Collectors.toMap(GaoSalesOrder::getId, java.util.function.Function.identity())); + } + + private List resolveDirectItems(String storeId, List sourceList) { + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "直接产品订单至少需要一个产品"); + Set skuUnitKeySet = new HashSet<>(); + List result = new ArrayList<>(); + boolean canOverridePrice = permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.SALES_ORDER_PRICE_OVERRIDE.getValue()); + for (int i = 0; i < sourceList.size(); i++) { + GaoSalesOrderItemRequest source = sourceList.get(i); + TimiException.required(source, "订单产品明细不能为空"); + TimiException.required(source.getSkuId(), "SKU 不能为空"); + TimiException.required(source.getSkuUnitId(), "SKU 单位不能为空"); + TimiException.requiredTrue(skuUnitKeySet.add(source.getSkuId() + "|" + source.getSkuUnitId()), "同一订单不能重复 SKU 和单位"); + GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, source.getSkuId(), source.getSkuUnitId()); + GaoProduct product = selection.getProduct(); + GaoProductSku sku = selection.getSku(); + GaoProductSkuUnit unit = selection.getUnit(); + BigDecimal quantity = validateQuantity(sku, source.getQuantity()); + Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), unit.getSalePrice()); + TimiException.required(unitPrice, "产品单价不能为空"); + TimiException.requiredTrue(0 <= unitPrice, "产品单价不能为负数"); + if (!canOverridePrice) { + TimiException.required(unit.getSalePrice(), "普通员工只能使用单位建议售价"); + TimiException.requiredTrue(unit.getSalePrice().equals(unitPrice), "普通员工只能使用单位建议售价"); + } + long lineAmount = toCentAmount(quantity, unitPrice); + GaoSalesOrderItem item = new GaoSalesOrderItem(); + item.setSkuId(sku.getId()); + item.setSkuUnitId(unit.getId()); + item.setItemType(GaoSalesOrderItem.ItemType.DIRECT_PRODUCT); + item.setProductNameSnapshot(product.getName()); + item.setProductIdSnapshot(product.getId()); + item.setSkuCodeSnapshot(sku.getCode()); + item.setSkuNameSnapshot(sku.getName()); + item.setSpecSummarySnapshot(sku.getSpecSummary()); + item.setSkuSpecSnapshot(skuService.getSkuSpecSnapshot(sku)); + item.setUnitSnapshot(unit.getName()); + item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit()); + item.setQuantity(quantity); + item.setReservedQuantity(ZERO); + item.setFulfilledQuantity(ZERO); + item.setUnitPrice(unitPrice); + item.setLineAmount(lineAmount); + item.setSort(TimiJava.defaultIfNull(source.getSort(), i)); + item.setRemark(source.getRemark()); + result.add(item); + } + result.sort(Comparator.comparing(GaoSalesOrderItem::getSort)); + return result; + } + + private void verifyExistingDirectItems(GaoSalesOrder existing, List requestList) { + List existingList = itemMapper.selectBySalesOrderId(existing.getId()); + TimiException.requiredTrue(requestList != null && existingList.size() == requestList.size(), "requestNo 对应订单内容不一致"); + Set skuUnitKeySet = new HashSet<>(); + for (GaoSalesOrderItemRequest request : requestList) { + TimiException.required(request, "订单产品明细不能为空"); + TimiException.required(request.getSkuId(), "SKU 不能为空"); + TimiException.required(request.getSkuUnitId(), "SKU 单位不能为空"); + TimiException.required(request.getQuantity(), "购买数量不能为空"); + TimiException.requiredTrue(skuUnitKeySet.add(request.getSkuId() + "|" + request.getSkuUnitId()), "同一订单不能重复 SKU 和单位"); + GaoSalesOrderItem item = existingList.stream() + .filter(value -> value.getSkuId().equals(request.getSkuId()) && value.getSkuUnitId().equals(request.getSkuUnitId())) + .findFirst().orElse(null); + TimiException.required(item, "requestNo 对应订单内容不一致"); + TimiException.requiredTrue(item.getSkuUnitId().equals(request.getSkuUnitId()) + && item.getQuantity().compareTo(request.getQuantity()) == 0 + && (request.getUnitPrice() == null || item.getUnitPrice().equals(request.getUnitPrice())), "requestNo 对应订单内容不一致"); + } + } + + private GaoSalesOrder newOrder(String storeId, String customerId, GaoSalesOrder.OrderType orderType, String requestNo, Long purchasedAt, String operatorUserId, String remark) { + GaoSalesOrder order = new GaoSalesOrder(); + order.setStoreId(storeId); + order.setCustomerId(customerId); + order.setOrderNo("SO-%d-%s".formatted(purchasedAt, UUID.randomUUID().toString().replace("-", "").substring(0, 8))); + order.setOrderType(orderType); + order.setStatus(GaoSalesOrder.Status.ACTIVE); + order.setRefundStatus(GaoSalesOrder.RefundStatus.NOT_REQUIRED); + order.setSaleAmount(0L); + order.setPendingPaidAmount(0L); + order.setPaidAmount(0L); + order.setRefundedAmount(0L); + order.setRequestNo(requestNo); + order.setPurchasedAt(purchasedAt); + order.setOperatorUserId(operatorUserId == null ? userLoginService.getRequireLoginUserId() : operatorUserId); + order.setRemark(remark); + return order; + } + + private GaoSalesOrder createOrder(String storeId, String customerId, GaoSalesOrder.OrderType orderType, String requestNo, + Long purchasedAt, String operatorUserId, String remark, long saleAmount, List itemList) { + TimiException.requiredTrue(itemList != null && !itemList.isEmpty(), "销售订单至少需要一个订单明细"); + TimiException.requiredTrue(0 <= saleAmount, "成交价不能为负数"); + GaoSalesOrderItem.ItemType expectedItemType = orderType == GaoSalesOrder.OrderType.PACKAGE + ? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT; + Set skuUnitKeySet = new HashSet<>(); + long lineAmountSum = 0L; + for (GaoSalesOrderItem item : itemList) { + TimiException.required(item, "销售订单明细不能为空"); + TimiException.requiredTrue(item.getItemType() == expectedItemType, "订单项类型与订单类型不匹配"); + TimiException.required(item.getSkuId(), "订单项 SKU 不能为空"); + TimiException.required(item.getSkuUnitId(), "订单项 SKU 单位不能为空"); + TimiException.requiredTrue(skuUnitKeySet.add(item.getSkuId() + "|" + item.getSkuUnitId()), + "同一订单不能重复 SKU 和单位"); + TimiException.required(item.getQuantity(), "订单项数量不能为空"); + TimiException.requiredTrue(0 < item.getQuantity().signum(), "订单项数量必须大于 0"); + TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数"); + TimiException.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空"); + TimiException.requiredTrue(0 < item.getBaseQuantityPerUnitSnapshot().signum(), "订单项单位换算快照必须大于 0"); + TimiException.requiredTrue(item.getReservedQuantity() != null && item.getFulfilledQuantity() != null, + "订单项履约数量不能为空"); + TimiException.requiredTrue(0 <= item.getReservedQuantity().signum() && 0 <= item.getFulfilledQuantity().signum(), + "订单项履约数量不能为负数"); + TimiException.requiredTrue(item.getReservedQuantity().add(item.getFulfilledQuantity()).compareTo(item.getQuantity()) <= 0, + "订单项履约数量不能超过购买数量"); + if (expectedItemType == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT) { + TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价"); + TimiException.requiredNull(item.getLineAmount(), "套餐订单项不能填写行金额"); + } else { + TimiException.required(item.getUnitPrice(), "直接产品订单项单价不能为空"); + TimiException.required(item.getLineAmount(), "直接产品订单项行金额不能为空"); + TimiException.requiredTrue(0 <= item.getUnitPrice() && 0 <= item.getLineAmount(), "直接产品订单金额不能为负数"); + lineAmountSum = Math.addExact(lineAmountSum, item.getLineAmount()); + } + } + if (expectedItemType == GaoSalesOrderItem.ItemType.DIRECT_PRODUCT) { + TimiException.requiredTrue(lineAmountSum == saleAmount, "直接产品订单行金额合计必须等于成交价"); + } + GaoSalesOrder order = newOrder(storeId, customerId, orderType, requestNo, purchasedAt, operatorUserId, remark); + order.setSaleAmount(saleAmount); + boolean paidAtCreate = saleAmount == 0; + order.setPaymentStatus(paidAtCreate ? GaoSalesOrder.PaymentStatus.PAID : GaoSalesOrder.PaymentStatus.UNPAID); + if (paidAtCreate) { + order.setPaidAt(Time.now()); + } + super.create(order); + for (GaoSalesOrderItem item : itemList) { + item.setSalesOrderId(order.getId()); + itemMapper.insert(item); + } + order.setItemList(itemList); + for (GaoSalesOrderItem item : itemList) { + fillItemBalance(item); + } + if (paidAtCreate) { + pointTriggerService.onSalesOrderPaid(order); + } + return order; + } + + private void verifyExistingPackageItems(GaoSalesOrder existing, List requestItemList) { + TimiException.requiredTrue(requestItemList != null && !requestItemList.isEmpty(), "销售订单至少需要一个订单明细"); + List existingItemList = itemMapper.selectBySalesOrderId(existing.getId()); + TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应订单内容不一致"); + Map existingItemMap = existingItemList.stream() + .collect(java.util.stream.Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), java.util.function.Function.identity())); + for (GaoSalesOrderItem requestItem : requestItemList) { + GaoSalesOrderItem existingItem = existingItemMap.get(requestItem.getSkuId() + "|" + requestItem.getSkuUnitId()); + TimiException.required(existingItem, "requestNo 对应订单内容不一致"); + TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT + && existingItem.getQuantity().compareTo(requestItem.getQuantity()) == 0 + && existingItem.getSort().equals(requestItem.getSort()) + && sameText(existingItem.getRemark(), requestItem.getRemark()), "requestNo 对应订单内容不一致"); + } + } + + private void fillItemList(List orderList) { + if (orderList == null || orderList.isEmpty()) { + return; + } + List orderIdList = orderList.stream() + .map(GaoSalesOrder::getId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map> itemMap = new HashMap<>(); + if (!orderIdList.isEmpty()) { + List queriedItemList = itemMapper.selectBySalesOrderIdList(orderIdList); + for (GaoSalesOrderItem item : TimiJava.safeIterable(queriedItemList)) { + itemMap.computeIfAbsent(item.getSalesOrderId(), key -> new ArrayList<>()).add(item); + } + } + List itemList = new ArrayList<>(); + for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) { + List orderItemList = itemMap.getOrDefault(order.getId(), List.of()); + order.setItemList(orderItemList); + itemList.addAll(orderItemList); + order.setUnpaidAmount(safe(order.getSaleAmount()) - safe(order.getPaidAmount())); + } + for (GaoSalesOrderItem item : itemList) { + fillItemBalance(item); + } + fillItemAttachments(itemList); + } + + /// 填充订单项履约余额投影 + /// + /// @param item 订单项 + private void fillItemBalance(GaoSalesOrderItem item) { + BigDecimal quantity = decimal(item.getQuantity()); + BigDecimal reserved = decimal(item.getReservedQuantity()); + BigDecimal fulfilled = decimal(item.getFulfilledQuantity()); + item.setPendingQuantity(reserved); + item.setAvailableQuantity(quantity.subtract(reserved).subtract(fulfilled)); + item.setRemainingQuantity(quantity.subtract(fulfilled)); + } + + private void fillItemAttachments(List itemList) { + if (itemList == null || itemList.isEmpty()) { + return; + } + List skuIdList = itemList.stream() + .map(GaoSalesOrderItem::getSkuId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (skuIdList.isEmpty()) { + return; + } + Map> attachmentMap = attachmentService.mapByBizIdList( + Attachment.BizType.GAO_PRODUCT_SKU, + skuIdList); + for (GaoSalesOrderItem item : itemList) { + item.setAttachmentList(attachmentMap.getOrDefault(item.getSkuId(), List.of())); + } + } + + private void fillCustomer(List orderList) { + Set customerIdSet = new HashSet<>(); + for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) { + if (TimiJava.isNotEmpty(order.getCustomerId())) { + customerIdSet.add(order.getCustomerId()); + } + } + if (customerIdSet.isEmpty()) { + return; + } + + List customerIdList = new ArrayList<>(customerIdSet); + Map customerMap = customerService.mapByIdList(customerIdList); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList); + for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) { + GaoCustomer customer = customerMap.get(order.getCustomerId()); + if (customer == null) { + continue; + } + customer.setAttachmentList(attachmentMap.getOrDefault(customer.getId(), List.of())); + order.setCustomer(customer); + } + } + + private BigDecimal validateQuantity(GaoProductSku sku, BigDecimal quantity) { + TimiException.required(quantity, "购买数量不能为空"); + TimiException.requiredTrue(0 < quantity.signum(), "购买数量必须大于 0"); + TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "购买数量最多保留三位小数"); + if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + } + return quantity; + } + + private long toCentAmount(BigDecimal quantity, long unitPrice) { + try { + return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact(); + } catch (ArithmeticException e) { + throw new TimiException(TimiCode.ERROR, "数量乘单价后不能精确保存为分", e); + } + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } + + private long safe(Long value) { + return value == null ? 0L : value; + } + + private long safeLong(Long value) { + return safe(value); + } + + private BigDecimal decimal(BigDecimal value) { + return value == null ? ZERO : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java index 4f289af..112923e 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java @@ -2,14 +2,15 @@ package com.imyeyu.api.modules.gao.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; -import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoStoreBusinessDayMapper; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,7 +26,7 @@ import java.util.List; public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService implements GaoStoreBusinessDayService { private final GaoStoreBusinessDayMapper mapper; - private final GaoEventRecordMapper eventRecordMapper; + private final ObjectProvider eventRecordServiceProvider; @Override protected BaseMapper mapper() { @@ -112,7 +113,7 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService customerDailyTrend(GaoStoreChartReq req) { @@ -46,19 +46,19 @@ public class GaoStoreChartServiceImplement implements GaoStoreChartService { @Override public List eventRecordDailyStat(GaoStoreChartReq req) { checkRangeReq(req); - return eventRecordMapper.stateDailyByRange(req); + return eventRecordService.stateDailyByRange(req); } @Override public List eventRecordNumberValueDailyStat(GaoStoreChartReq req) { checkRangeReq(req); - return eventRecordMapper.stateNumberValueDailyByRange(req); + return eventRecordService.stateNumberValueDailyByRange(req); } @Override public List eventRecordCalendar(GaoStoreChartReq req) { checkRangeReq(req); - return eventRecordMapper.selectCalendarByRange(req); + return eventRecordService.calendarByStoreRange(req); } private void checkRangeReq(GaoStoreChartReq req) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java index 81c93c0..8827db4 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java @@ -3,7 +3,6 @@ package com.imyeyu.api.modules.gao.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.common.entity.Article; import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.common.mapper.ArticleMapper; import com.imyeyu.api.modules.common.service.ArticleService; import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.gao.mapper.GaoStoreDiaryMapper; @@ -26,7 +25,6 @@ public class GaoStoreDiaryServiceImplement implements GaoStoreDiaryService { private final ArticleService articleService; private final AttachmentService attachmentService; - private final ArticleMapper articleMapper; private final GaoStoreDiaryMapper diaryMapper; @Override @@ -83,7 +81,7 @@ public class GaoStoreDiaryServiceImplement implements GaoStoreDiaryService { Article example = new Article(); example.setBizType(Article.BizType.GAO_STORE_DIARY); example.setBizId(storeId); - for (Article article : articleMapper.selectAllByExample(example)) { + for (Article article : articleService.listByExample(example)) { attachmentService.deleteByBizId(Attachment.BizType.ARTICLE, article.getId()); articleService.delete(article.getId()); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java index 4a1fdad..cb683bf 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.common.entity.Tag; import com.imyeyu.api.modules.common.service.TagService; +import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.mapper.GaoStoreMapper; import com.imyeyu.api.modules.gao.service.GaoCustomerService; diff --git a/src/main/java/com/imyeyu/api/modules/gao/task/GaoOrderReminderTask.java b/src/main/java/com/imyeyu/api/modules/gao/task/GaoOrderReminderTask.java new file mode 100644 index 0000000..fd5670a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/task/GaoOrderReminderTask.java @@ -0,0 +1,41 @@ +package com.imyeyu.api.modules.gao.task; + +import com.imyeyu.api.modules.gao.service.GaoOrderReminderService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/// GAO 订单提醒定时任务 +/// +/// @author Codex +/// @since 2026-09-06 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoOrderReminderTask { + + private static final String ZONE_ID = "Asia/Shanghai"; + + private final GaoOrderReminderService service; + + /// 每天早上八点发送订单收款提醒 + @Scheduled(cron = "0 0 8 * * ?", zone = ZONE_ID) + public void sendOrderReceiveReminder() { + execute("订单收款", service::sendOrderReceiveReminder); + } + + /// 每天晚上九点发送核销提醒 + @Scheduled(cron = "0 0 21 * * ?", zone = ZONE_ID) + public void sendVerifyReminder() { + execute("核销", service::sendVerifyReminder); + } + + private void execute(String reminderName, Runnable action) { + try { + action.run(); + } catch (Exception e) { + log.error("GAO {}提醒任务执行失败", reminderName, e); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java new file mode 100644 index 0000000..09d71f7 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java @@ -0,0 +1,55 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 客户订单产品排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerOrderProductRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 已预占数量 + private BigDecimal reservedQuantity; + + /// 已履约数量 + private BigDecimal fulfilledQuantity; + + /// 当前可履约数量 + private BigDecimal availableQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 已预占数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderRankView.java new file mode 100644 index 0000000..55ebd03 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderRankView.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户订单排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerOrderRankView { + + /// 客户 ID + private String customerId; + + /// 客户编码 + private String customerCode; + + /// 客户名称 + private String customerName; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderSkuRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderSkuRankView.java new file mode 100644 index 0000000..b3d1014 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderSkuRankView.java @@ -0,0 +1,52 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 客户订单 SKU 排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoCustomerOrderSkuRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 待履约数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java index eab3cb6..a36f97a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java @@ -1,6 +1,6 @@ package com.imyeyu.api.modules.gao.vo; -import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.java.bean.BasePage; import lombok.Data; import lombok.EqualsAndHashCode; @@ -22,18 +22,7 @@ public class GaoCustomerPackagePage extends BasePage { /// 客户或套餐关键词 private String keyword; - /// 套餐状态 - private GaoCustomerPackage.Status status; + /// 关联订单状态 + private GaoSalesOrder.Status status; - /// 付款状态 - private GaoCustomerPackage.PaymentStatus paymentStatus; - - /// 退款状态 - private GaoCustomerPackage.RefundStatus refundStatus; - - /// 购买开始时间 - private Long beginAt; - - /// 购买结束时间 - private Long endAt; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java index bd877a5..9058943 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java @@ -11,11 +11,32 @@ import java.math.BigDecimal; @Data public class GaoCustomerPackagePurchaseItem { - /// 产品名称 - private String productName; + /// SKU ID + private String skuId; - /// 产品单位 - private String unit; + /// SKU 单位 ID + private String skuUnitId; + + /// 产品名称快照,由后端生成 + private String productNameSnapshot; + + /// 产品 ID 快照,由后端生成 + private String productIdSnapshot; + + /// SKU 编码快照,由后端生成 + private String skuCodeSnapshot; + + /// SKU 名称快照,由后端生成 + private String skuNameSnapshot; + + /// 规格摘要快照,由后端生成 + private String specSummarySnapshot; + + /// 单位快照,由后端生成 + private String unitSnapshot; + + /// 基础单位数量系数快照,由后端生成 + private BigDecimal baseQuantityPerUnitSnapshot; /// 总权益数量 private BigDecimal totalQuantity; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java index c87ceef..b6f7e57 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java @@ -15,8 +15,8 @@ public class GaoCustomerPackagePurchaseRequest { /// 客户端幂等请求号 private String requestNo; - /// 购买事件 ID - private String eventId; + /// 套餐配置 ID + private String packageConfigId; /// 客户 ID private String customerId; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatDailyStatView.java new file mode 100644 index 0000000..b0b40bb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatDailyStatView.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户复购每日统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerRepeatDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 当日购买客户数 + private Long customerCount; + + /// 当日复购客户数 + private Long repeatCustomerCount; + + /// 当日复购订单数 + private Long repeatOrderCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatIntervalStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatIntervalStatView.java new file mode 100644 index 0000000..de99bce --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatIntervalStatView.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户复购间隔统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerRepeatIntervalStatView { + + /// 复购间隔天数 + private Long intervalDays; + + /// 发生复购的客户数 + private Long customerCount; + + /// 复购订单数 + private Long orderCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoDirectProductOrderCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoDirectProductOrderCreateRequest.java new file mode 100644 index 0000000..2fd8703 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoDirectProductOrderCreateRequest.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 直接产品订单创建请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoDirectProductOrderCreateRequest { + + /// 创建订单幂等号 + private String requestNo; + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 下单时间 + private Long purchasedAt; + + /// 订单明细 + private List itemList; + + /// 购买照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java index ec0d9f2..2e3e568 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java @@ -19,6 +19,12 @@ public class GaoFundRecordPage extends BasePage { /// 客户套餐 ID private String customerPackageId; + /// 销售订单 ID + private String salesOrderId; + + /// 资金业务类型 + private GaoFundRecord.BizType bizType; + /// 客户 ID private String customerId; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentActionRequest.java new file mode 100644 index 0000000..0774d8e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentActionRequest.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 统一履约状态操作请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentActionRequest { + + /// 履约单 ID + private String fulfillmentId; + + /// 拒绝或撤销原因 + private String reason; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentCreateRequest.java new file mode 100644 index 0000000..1086a57 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentCreateRequest.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 统一履约创建请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentCreateRequest { + + /// 销售订单 ID + private String salesOrderId; + + /// 客户端幂等请求号 + private String requestNo; + + /// 实际履约时间 + private Long deliveredAt; + + /// 履约明细 + private List itemList; + + /// 履约照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java new file mode 100644 index 0000000..b21a153 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java @@ -0,0 +1,19 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// GAO 统一履约明细请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentItemRequest { + + /// 来源销售订单明细 ID + private String sourceOrderItemId; + + /// 本次履约数量 + private BigDecimal quantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPage.java new file mode 100644 index 0000000..8638b99 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPage.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 统一履约分页查询请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoOrderFulfillmentPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 销售订单 ID + private String salesOrderId; + + /// 客户 ID + private String customerId; + + /// 履约状态 + private GaoOrderFulfillment.Status status; + + /// 起始时间 + private Long beginAt; + + /// 结束时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPrepareStockRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPrepareStockRequest.java new file mode 100644 index 0000000..a0eed07 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPrepareStockRequest.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 履约占库准备请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentPrepareStockRequest { + + /// 履约单 ID + private String fulfillmentId; + + /// 占库准备原因 + private String reason; + + /// 备注 + private String remark; + + /// 补充照片 + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentRevokeRequest.java similarity index 50% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentRevokeRequest.java index 4eede73..e0e8a81 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentRevokeRequest.java @@ -1,31 +1,33 @@ package com.imyeyu.api.modules.gao.vo; import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; import lombok.Data; import java.util.List; -/// GAO 套餐发放请求 +/// GAO 已核销履约整单冲正请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoPackageDeliveryCreateRequest { +public class GaoOrderFulfillmentRevokeRequest { + + /// 原履约单 ID + private String fulfillmentId; /// 客户端幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - - /// 实际发放时间 + /// 冲正发生时间 private Long deliveredAt; - /// 发放产品明细 - private List itemList; + /// 冲正原因 + private String reason; - /// 发放照片 + /// true 为实物已退回门店 + private Boolean returnedToStore; + + /// 退回或冲正照片 private List attachmentList; /// 备注 diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentActionRequest.java similarity index 50% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentActionRequest.java index 4ca1626..1436c7a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentActionRequest.java @@ -2,16 +2,16 @@ package com.imyeyu.api.modules.gao.vo; import lombok.Data; -/// GAO 资金收款状态操作请求 +/// GAO 销售订单收款操作请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundActionRequest { +public class GaoOrderPaymentActionRequest { - /// 资金记录 ID + /// 资金流水 ID private String fundRecordId; - /// 拒绝或撤销原因 + /// 原因 private String reason; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentNoRefundRequest.java similarity index 52% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentNoRefundRequest.java index 786ce1b..358baa7 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentNoRefundRequest.java @@ -2,25 +2,25 @@ package com.imyeyu.api.modules.gao.vo; import lombok.Data; -/// GAO 确认无需退款请求 +/// GAO 销售订单确认无需退款请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundConfirmNoRefundRequest { +public class GaoOrderPaymentNoRefundRequest { - /// 客户端幂等请求号 + /// 订单 ID + private String salesOrderId; + + /// 幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - - /// 确认时间 - private Long occurredAt; - /// 确认原因 private String reason; + /// 发生时间 + private Long occurredAt; + /// 备注 private String remark; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentReceiveRequest.java similarity index 65% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentReceiveRequest.java index aedb2e8..365bd91 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentReceiveRequest.java @@ -5,23 +5,23 @@ import lombok.Data; import java.util.List; -/// GAO 线下收款登记请求 +/// GAO 销售订单收款请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundReceiveRequest { +public class GaoOrderPaymentReceiveRequest { - /// 客户端幂等请求号 + /// 订单 ID + private String salesOrderId; + + /// 幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - /// 收款金额,单位分 private Long amount; - /// 收款发生时间 + /// 实际收款时间 private Long occurredAt; /// 收款照片 diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRefundRequest.java similarity index 72% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRefundRequest.java index 81dbb35..9a29d1c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRefundRequest.java @@ -5,19 +5,19 @@ import lombok.Data; import java.util.List; -/// GAO 退款登记请求 +/// GAO 销售订单退款请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundRefundRequest { +public class GaoOrderPaymentRefundRequest { - /// 客户端幂等请求号 + /// 订单 ID + private String salesOrderId; + + /// 幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - /// 退款金额,单位分 private Long amount; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRevokeRequest.java similarity index 62% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRevokeRequest.java index ec9e207..bc44bba 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRevokeRequest.java @@ -5,25 +5,25 @@ import lombok.Data; import java.util.List; -/// GAO 已核销发放整单冲正请求 +/// GAO 销售订单资金冲正请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoPackageDeliveryRevokeRequest { +public class GaoOrderPaymentRevokeRequest { - /// 客户端幂等请求号 + /// 原资金流水 ID + private String fundRecordId; + + /// 幂等请求号 private String requestNo; - /// 原发放单 ID - private String deliveryId; - - /// 冲正发生时间 - private Long deliveredAt; - /// 冲正原因 private String reason; + /// 冲正发生时间 + private Long occurredAt; + /// 冲正照片 private List attachmentList; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderReminderCustomer.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderReminderCustomer.java new file mode 100644 index 0000000..52f9bab --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderReminderCustomer.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 订单提醒客户 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoOrderReminderCustomer { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 客户名称 + private String customerName; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java deleted file mode 100644 index dfd2e7c..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.imyeyu.api.modules.gao.vo; - -import lombok.Data; - -/// GAO 套餐发放状态操作请求 -/// -/// @author Codex -/// @since 2026-08-31 -@Data -public class GaoPackageDeliveryActionRequest { - - /// 发放单 ID - private String deliveryId; - - /// 拒绝或撤销原因 - private String reason; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java deleted file mode 100644 index 73c788e..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.imyeyu.api.modules.gao.vo; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.java.bean.BasePage; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/// GAO 套餐发放分页查询请求 -/// -/// @author Codex -/// @since 2026-08-31 -@Data -@EqualsAndHashCode(callSuper = true) -public class GaoPackageDeliveryPage extends BasePage { - - /// 门店 ID - private String storeId; - - /// 客户套餐 ID - private String customerPackageId; - - /// 客户 ID - private String customerId; - - /// 发放状态 - private GaoPackageDelivery.Status status; - - /// 操作类型 - private GaoPackageDelivery.OperationType operationType; - - /// 发放开始时间 - private Long beginAt; - - /// 发放结束时间 - private Long endAt; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java new file mode 100644 index 0000000..dbdf7ca --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java @@ -0,0 +1,47 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import lombok.Data; + +import java.util.List; + +/// GAO 套餐订单内部创建数据 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoPackageOrderCreateData { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 套餐配置 ID + private String packageConfigId; + + /// 套餐名称快照 + private String packageNameSnapshot; + + /// 套餐标准价快照 + private Long standardAmountSnapshot; + + /// 成交价 + private Long saleAmount; + + /// 幂等号 + private String requestNo; + + /// 下单时间 + private Long purchasedAt; + + /// 操作人 + private String operatorUserId; + + /// 备注 + private String remark; + + /// 订单可履约明细 + private List itemList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryPage.java new file mode 100644 index 0000000..5dd63da --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryPage.java @@ -0,0 +1,24 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 产品分类分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductCategoryPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 分类状态 + private GaoProductCategory.Status status; + + /// 名称关键词 + private String keyword; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryStatView.java new file mode 100644 index 0000000..7536aa2 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryStatView.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 产品分类统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductCategoryStatView { + + /// 分类 ID + private String categoryId; + + /// 分类名称 + private String categoryName; + + /// 产品数量 + private Long productCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java new file mode 100644 index 0000000..51a8144 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +/// GAO 产品图表查询请求 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductChartReq { + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// SKU ID + private String skuId; + + /// 分类 ID + private String categoryId; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 开始时间,包含 + private Long beginAt; + + /// 结束时间,包含 + private Long endAt; + + /// 返回排行数量 + private Integer topN; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java new file mode 100644 index 0000000..648bdaa --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品每日履约统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductFulfillmentDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 直接产品履约数量 + private BigDecimal directQuantity; + + /// 套餐权益履约数量 + private BigDecimal packageQuantity; + + /// 直接产品履约数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益履约数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 履约基础单位数量 + private BigDecimal baseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java new file mode 100644 index 0000000..86bcca5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java @@ -0,0 +1,52 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品履约进度排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductFulfillmentRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 待履约数量 + private BigDecimal reservedQuantity; + + /// 已履约数量 + private BigDecimal fulfilledQuantity; + + /// 当前可履约数量 + private BigDecimal availableQuantity; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 待履约数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductPage.java new file mode 100644 index 0000000..e958c44 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductPage.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 产品分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 分类 ID + private String categoryId; + + /// 分类 ID 列表 + private List categoryIdList; + + /// 产品状态 + private GaoProduct.Status status; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 名称或编码关键词 + private String keyword; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java new file mode 100644 index 0000000..5317fa3 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java @@ -0,0 +1,34 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品每日销售统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductSalesDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 包含该产品的订单数 + private Long orderCount; + + /// 直接产品销售数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java new file mode 100644 index 0000000..ce10e8c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品销售排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductSalesRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 包含该产品的订单数 + private Long orderCount; + + /// 直接产品销售数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentDailyStatView.java new file mode 100644 index 0000000..70fd4bc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentDailyStatView.java @@ -0,0 +1,43 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 每日履约统计视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuFulfillmentDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 直接产品履约数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益履约数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 履约基础单位数量 + private BigDecimal baseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentRankView.java new file mode 100644 index 0000000..e2995de --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentRankView.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 履约进度排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuFulfillmentRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 待履约数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesDailyStatView.java new file mode 100644 index 0000000..713ebcd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesDailyStatView.java @@ -0,0 +1,46 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 每日销售统计视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuSalesDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 包含该 SKU 的订单数 + private Long orderCount; + + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesRankView.java new file mode 100644 index 0000000..192a9d6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesRankView.java @@ -0,0 +1,43 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 销售排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuSalesRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 包含该 SKU 的订单数 + private Long orderCount; + + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockHealthView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockHealthView.java new file mode 100644 index 0000000..042b624 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockHealthView.java @@ -0,0 +1,56 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 库存健康度视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuStockHealthView { + + /// SKU ID + private String skuId; + + /// SKU 编码 + private String skuCode; + + /// SKU 名称 + private String skuName; + + /// 规格摘要 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 分类 ID + private String categoryId; + + /// 分类名称 + private String categoryName; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 实际库存,基础单位 + private BigDecimal onHandQuantity; + + /// 预占库存,基础单位 + private BigDecimal reservedQuantity; + + /// 可用库存,基础单位 + private BigDecimal availableQuantity; + + /// 最低库存,基础单位 + private BigDecimal minStockQuantity; + + /// 1 为低于最低库存,0 为正常 + private Integer lowStock; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockMovementDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockMovementDailyStatView.java new file mode 100644 index 0000000..a7f05dd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockMovementDailyStatView.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 每日库存变动统计视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuStockMovementDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 实际入库数量,基础单位 + private BigDecimal inQuantity; + + /// 实际出库数量,基础单位 + private BigDecimal outQuantity; + + /// 实际库存净变化,基础单位 + private BigDecimal netOnHandQuantity; + + /// 新增预占数量,基础单位 + private BigDecimal reserveQuantity; + + /// 释放预占数量,基础单位 + private BigDecimal releaseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSalesRankView.java new file mode 100644 index 0000000..698d72f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSalesRankView.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 单位销售排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuUnitSalesRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// SKU 单位 ID + private String skuUnitId; + + /// 单位名称快照 + private String unitName; + + /// 订单数 + private Long orderCount; + + /// 销售数量 + private BigDecimal quantity; + + /// 销售数量,基础单位 + private BigDecimal baseQuantity; + + /// 销售金额,单位分 + private Long saleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSelection.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSelection.java new file mode 100644 index 0000000..b411dd4 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSelection.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import lombok.AllArgsConstructor; +import lombok.Data; + +/// 已校验的 SKU 单位选项 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@AllArgsConstructor +public class GaoProductSkuUnitSelection { + + /// 产品 + private GaoProduct product; + + /// SKU + private GaoProductSku sku; + + /// SKU 单位 + private GaoProductSkuUnit unit; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java new file mode 100644 index 0000000..b161676 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 人工库存变更请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoProductStockChangeRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 门店 ID + private String storeId; + + /// SKU ID + private String skuId; + + /// SKU 单位 ID + private String skuUnitId; + + /// 业务数量 + private BigDecimal quantity; + + /// 备注 + private String remark; + + /// 操作照片 + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java new file mode 100644 index 0000000..c2b9aa7 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java @@ -0,0 +1,50 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品库存健康度视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductStockHealthView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 分类 ID + private String categoryId; + + /// 分类名称 + private String categoryName; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 实际库存,基础单位 + private BigDecimal onHandQuantity; + + /// 预占库存,基础单位 + private BigDecimal reservedQuantity; + + /// 可用库存,基础单位 + private BigDecimal availableQuantity; + + /// 最低库存,基础单位 + private BigDecimal minStockQuantity; + + /// 1 为低于最低库存,0 为正常 + private Integer lowStock; + + /// 参与库存统计的 SKU 数量 + private Long skuCount; + + /// 低于最低库存的 SKU 数量 + private Long lowStockSkuCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockManualRevokeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockManualRevokeRequest.java new file mode 100644 index 0000000..aa1858a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockManualRevokeRequest.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 手工库存流水冲正请求 +/// +/// @author Codex +/// @since 2026-09-03 +@Data +public class GaoProductStockManualRevokeRequest { + + /// 原手工库存流水 ID + private String sourceRecordId; + + /// 客户端幂等请求号 + private String requestNo; + + /// 冲正备注 + private String remark; + + /// 冲正照片 + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockMovementDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockMovementDailyStatView.java new file mode 100644 index 0000000..c5b5e8b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockMovementDailyStatView.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品每日库存变动统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductStockMovementDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 实际入库数量,基础单位 + private BigDecimal inQuantity; + + /// 实际出库数量,基础单位 + private BigDecimal outQuantity; + + /// 实际库存净变化,基础单位 + private BigDecimal netOnHandQuantity; + + /// 新增预占数量,基础单位 + private BigDecimal reserveQuantity; + + /// 释放预占数量,基础单位 + private BigDecimal releaseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java new file mode 100644 index 0000000..af3b0d8 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 产品库存分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductStockPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 产品或 SKU 名称、编码、规格摘要关键词 + private String keyword; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockProductView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockProductView.java new file mode 100644 index 0000000..bb891d0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockProductView.java @@ -0,0 +1,38 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品库存第一层汇总视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductStockProductView { + + /// 产品 ID + private String productId; + + /// 产品编码 + private String productCode; + + /// 产品名称 + private String productName; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 可库存 SKU 数量 + private Long skuCount; + + /// 库存数量,使用基础单位 + private BigDecimal onHandQuantity; + + /// 预占数量,使用基础单位 + private BigDecimal reservedQuantity; + + /// 可用数量,使用基础单位 + private BigDecimal availableQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java new file mode 100644 index 0000000..0851368 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java @@ -0,0 +1,51 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 库存流水分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductStockRecordPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 产品名称或编码关键词 + private String keyword; + + /// 来源业务类型 + private GaoProductStockRecord.BizType bizType; + + /// 来源业务 ID + private String bizId; + + /// 操作类型 + private GaoProductStockRecord.OperationType operationType; + + /// 仅查询仍然生效的履约预占流水 + private Boolean activeReservationOnly; + + /// 库存变动方向,IN 为入库,OUT 为出库 + public enum Direction { + IN, + OUT + } + + /// 库存变动方向 + private Direction direction; + + /// 起始发生时间 + private Long beginAt; + + /// 结束发生时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductTypeStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductTypeStatView.java new file mode 100644 index 0000000..0009b94 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductTypeStatView.java @@ -0,0 +1,18 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +/// 产品类型统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductTypeStatView { + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 产品数量 + private Long productCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java new file mode 100644 index 0000000..4427b10 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java @@ -0,0 +1,52 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品销售单位排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductUnitSalesRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品单位 ID + private String productUnitId; + + /// SKU 单位 ID + private String skuUnitId; + + /// 单位名称 + private String unitName; + + /// 订单数 + private Long orderCount; + + /// 销售数量 + private BigDecimal quantity; + + /// 销售数量,基础单位 + private BigDecimal baseQuantity; + + /// 销售金额,单位分 + private Long saleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCalendarStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCalendarStatView.java new file mode 100644 index 0000000..de30244 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCalendarStatView.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户订单日历统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderCalendarStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java new file mode 100644 index 0000000..73110d6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 销售订单取消请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoSalesOrderCancelRequest { + + /// 订单 ID + private String salesOrderId; + + /// 取消原因 + private String reason; + + /// 幂等请求号 + private String requestNo; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java new file mode 100644 index 0000000..a484dc1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java @@ -0,0 +1,42 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// GAO 销售订单图表查询请求 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderChartReq { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 产品 ID + private String productId; + + /// SKU ID + private String skuId; + + /// 订单类型 + private GaoSalesOrder.OrderType orderType; + + /// 订单状态 + private GaoSalesOrder.Status status; + + /// 付款状态 + private GaoSalesOrder.PaymentStatus paymentStatus; + + /// 开始时间,包含 + private Long beginAt; + + /// 结束时间,包含 + private Long endAt; + + /// 返回排行数量 + private Integer topN; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderConsumptionDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderConsumptionDailyStatView.java new file mode 100644 index 0000000..6e6aa06 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderConsumptionDailyStatView.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户每日有效消费统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderConsumptionDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 有效消费记录数 + private Long recordCount; + + /// 有效消费金额,单位分 + private Long amount; + + /// 截止当日累计有效消费金额,单位分 + private Long cumulativeAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderDailyStatView.java new file mode 100644 index 0000000..c8b6b31 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderDailyStatView.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 销售订单每日统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java new file mode 100644 index 0000000..89f995c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// GAO 直接产品订单明细请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoSalesOrderItemRequest { + + /// SKU ID + private String skuId; + + /// SKU 单位 ID + private String skuUnitId; + + /// 购买数量 + private BigDecimal quantity; + + /// 实际单价,单位分 + private Long unitPrice; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java new file mode 100644 index 0000000..4c87fcb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java @@ -0,0 +1,48 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 销售订单分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoSalesOrderPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 订单类型 + private GaoSalesOrder.OrderType orderType; + + /// 订单状态 + private GaoSalesOrder.Status status; + + /// 付款状态 + private GaoSalesOrder.PaymentStatus paymentStatus; + + /// 退款状态 + private GaoSalesOrder.RefundStatus refundStatus; + + /// true 为筛选可收款且未付清订单 + private Boolean pendingReceive; + + /// true 为筛选已履约但未付清订单 + private Boolean fulfilledButUnpaid; + + /// 关键词 + private String keyword; + + /// 起始下单时间 + private Long beginAt; + + /// 结束下单时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPaymentStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPaymentStatView.java new file mode 100644 index 0000000..af68e5f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPaymentStatView.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// 销售订单付款状态统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderPaymentStatView { + + /// 付款状态 + private GaoSalesOrder.PaymentStatus paymentStatus; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 未收金额,单位分 + private Long unpaidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderReceivableStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderReceivableStatView.java new file mode 100644 index 0000000..b306990 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderReceivableStatView.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 销售订单应收统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderReceivableStatView { + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 未收金额,单位分 + private Long unpaidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderStatusStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderStatusStatView.java new file mode 100644 index 0000000..d5abe12 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderStatusStatView.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// 销售订单状态统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderStatusStatView { + + /// 订单状态 + private GaoSalesOrder.Status status; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderTypeStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderTypeStatView.java new file mode 100644 index 0000000..db5d249 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderTypeStatView.java @@ -0,0 +1,30 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// 销售订单类型统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderTypeStatView { + + /// 订单类型 + private GaoSalesOrder.OrderType orderType; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderUpdateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderUpdateRequest.java new file mode 100644 index 0000000..237194f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderUpdateRequest.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 销售订单备注和附件修改请求 +/// +/// 该请求只允许修改备注和订单附件,不承载订单其他字段。 +/// +/// @author Codex +/// @since 2026-09-05 +@Data +public class GaoSalesOrderUpdateRequest { + + /// 订单 ID + private String id; + + /// 订单附件 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java new file mode 100644 index 0000000..3d73636 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java @@ -0,0 +1,80 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import lombok.Data; + +import java.math.BigDecimal; + +/// 已校验的库存变更项 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoStockChangeItem { + + /// SKU ID + private String skuId; + + /// SKU 单位 ID + private String skuUnitId; + + /// 产品 ID 快照 + private String productIdSnapshot; + + /// 产品名称快照 + private String productNameSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + + /// SKU 规格结构快照,数组元素包含 specId、specName、specValueId、specValueName + private JsonNode skuSpecSnapshot; + + /// 单位名称快照 + private String unitSnapshot; + + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; + + /// 业务数量 + private BigDecimal quantity; + + /// 基础单位数量 + private BigDecimal baseQuantity; + + /// 冲正来源流水 ID + private String sourceRecordId; + + /// 生成库存项 + /// + /// @param product 产品 + /// @param sku SKU + /// @param unit SKU 单位 + /// @param quantity 业务数量 + /// @param baseQuantity 基础单位数量 + /// @return 库存变更项 + public static GaoStockChangeItem of(GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit, BigDecimal quantity, BigDecimal baseQuantity) { + GaoStockChangeItem result = new GaoStockChangeItem(); + result.skuId = sku.getId(); + result.skuUnitId = unit.getId(); + result.productIdSnapshot = product.getId(); + result.productNameSnapshot = product.getName(); + result.skuCodeSnapshot = sku.getCode(); + result.skuNameSnapshot = sku.getName(); + result.specSummarySnapshot = sku.getSpecSummary(); + result.unitSnapshot = unit.getName(); + result.baseQuantityPerUnitSnapshot = unit.getBaseQuantityPerUnit(); + result.quantity = quantity; + result.baseQuantity = baseQuantity; + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java b/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java index d0f09f6..eac4d8d 100644 --- a/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java +++ b/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java @@ -49,10 +49,10 @@ public class RoleController { @RequireCorePermission(CorePermissionCode.ROLE_READ) @PostMapping("/list") public PageResult list(@RequestBody Page page) { - Set manageableRoleIdSet = listManageableRoleId(); + Set visibleRoleIdSet = listVisibleRoleId(); PageResult result = service.page(page); if (result.getList() != null) { - result.setList(result.getList().stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList()); + result.setList(result.getList().stream().filter(role -> visibleRoleIdSet.contains(role.getId())).toList()); result.getList().forEach(this::fillRoleDetail); } return result; @@ -83,7 +83,7 @@ public class RoleController { @PostMapping("/detail") public Role detail(@RequestParam String id) { Role role = service.get(id); - requireManageableRole(role.getId()); + requireVisibleRole(role.getId()); fillRoleDetail(role); return role; } @@ -105,7 +105,7 @@ public class RoleController { @RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ) @PostMapping("/permission/grantable/list") public List listGrantableRolePermission(@RequestParam String roleId) { - requireManageableRole(roleId); + requireVisibleRole(roleId); return service.listGrantableRolePermission(roleId); } @@ -137,6 +137,12 @@ public class RoleController { return service.listGrantableParentRole(roleId); } + @RequireCorePermission(CorePermissionCode.ROLE_CREATE) + @PostMapping("/parent/create/grantable/list") + public List listCreateGrantableParentRole(@RequestParam ModuleCode moduleCode) { + return service.listCreateGrantableParentRole(moduleCode); + } + @AOPLog @RequireCorePermission(CorePermissionCode.ROLE_UPDATE) @PostMapping("/parent/update") @@ -197,6 +203,19 @@ public class RoleController { .collect(Collectors.toSet()); } + private Set listVisibleRoleId() { + Set roleIdSet = listManageableRoleId(); + String loginUserId = userLoginService.getRequireLoginUserId(); + roleIdSet.addAll(userRoleService.listRoleByUserId(loginUserId).stream().map(Role::getId).toList()); + return roleIdSet; + } + + private void requireVisibleRole(String roleId) { + if (!listVisibleRoleId().contains(roleId)) { + throw new TimiException(TimiCode.PERMISSION_ERROR); + } + } + private void requireManageableRole(String roleId) { if (!listManageableRoleId().contains(roleId)) { throw new TimiException(TimiCode.PERMISSION_ERROR); diff --git a/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java b/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java index f3c5332..73f7f2f 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java @@ -75,6 +75,12 @@ public interface RoleService extends BaseService { /// @return 角色列表 List listGrantableParentRole(String roleId); + /// 查询创建模块角色时可选的管理父级角色 + /// + /// @param moduleCode 目标模块代码 + /// @return 角色列表 + List listCreateGrantableParentRole(ModuleCode moduleCode); + /// 修改角色管理父级 /// /// @param role 角色 diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java index 30baa9c..e927f70 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java @@ -83,11 +83,12 @@ public class AuthorizationBoundaryServiceImplement implements AuthorizationBound @Override public void checkCanCreateRole(String operatorUserId, Role role) { checkCorePermission(operatorUserId, CorePermissionCode.ROLE_CREATE); + TimiException.required(role.getModuleCode(), "not found role.moduleCode"); + TimiException.required(role.getOwnerType(), "not found role.ownerType"); + checkOwnerDefinition(role.getModuleCode(), role.getOwnerType(), role.getOwnerId()); if (authorizationScopeService.isSystemAdmin(operatorUserId)) { return; } - TimiException.required(role.getModuleCode(), "not found role.moduleCode"); - TimiException.required(role.getOwnerType(), "not found role.ownerType"); TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能创建内置角色"); TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能创建受保护角色"); checkOwnerScope(operatorUserId, role.getModuleCode(), role.getOwnerType(), role.getOwnerId()); @@ -98,6 +99,11 @@ public class AuthorizationBoundaryServiceImplement implements AuthorizationBound checkCorePermission(operatorUserId, CorePermissionCode.ROLE_UPDATE); Role dbRole = roleMapper.selectById(role.getId()); TimiException.required(dbRole, "not found role"); + if (role.getOwnerType() != null || role.getOwnerId() != null) { + AuthOwnerType ownerType = TimiJava.defaultIfNull(role.getOwnerType(), dbRole.getOwnerType()); + String ownerId = TimiJava.defaultIfEmpty(role.getOwnerId(), dbRole.getOwnerId()); + checkOwnerDefinition(dbRole.getModuleCode(), ownerType, ownerId); + } if (authorizationScopeService.isSystemAdmin(operatorUserId)) { return; } @@ -339,6 +345,12 @@ public class AuthorizationBoundaryServiceImplement implements AuthorizationBound TimiException.requiredTrue(resolver == null || resolver.canUseOwnerScope(operatorUserId, ownerType, ownerId), "归属范围不在可管理范围内"); } + private void checkOwnerDefinition(ModuleCode moduleCode, AuthOwnerType ownerType, String ownerId) { + if (ownerType == AuthOwnerType.MODULE) { + TimiException.requiredTrue(moduleCode.name().equals(ownerId), "模块级归属 ID 必须等于模块代码"); + } + } + private void checkCorePermission(String operatorUserId, CorePermissionCode permissionCode) { TimiException.requiredTrue(hasPermission(operatorUserId, ModuleCode.CORE, permissionCode.getValue()), "权限不足:%s".formatted(permissionCode.getValue())); } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java index 99a5850..f425eab 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java @@ -355,6 +355,25 @@ public class RoleServiceImplement extends AbstractEntityService im .toList(); } + @Override + public List listCreateGrantableParentRole(ModuleCode moduleCode) { + String loginUserId = userLoginService.getRequireLoginUserId(); + if (authorizationScopeService.isSystemAdmin(loginUserId)) { + return mapper.selectAll(); + } + Set directOperatorRoleIdSet = userRoleMapper.selectByUserIdList(List.of(loginUserId)) + .stream() + .map(UserRole::getRoleId) + .collect(Collectors.toSet()); + if (TimiJava.isEmpty(directOperatorRoleIdSet)) { + return List.of(); + } + return mapper.selectByIdList(directOperatorRoleIdSet) + .stream() + .filter(role -> role.getModuleCode() == moduleCode) + .toList(); + } + @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void updateRoleParent(Role role) { diff --git a/src/main/resources/db/migration/timiserver/V53__create_gao_product_tables.sql b/src/main/resources/db/migration/timiserver/V53__create_gao_product_tables.sql new file mode 100644 index 0000000..901fe91 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V53__create_gao_product_tables.sql @@ -0,0 +1,53 @@ +CREATE TABLE IF NOT EXISTS `gao_product_category` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID', + `name` VARCHAR(64) NOT NULL COMMENT '分类名称', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `status` VARCHAR(32) NOT NULL COMMENT 'ACTIVE 启用,INACTIVE 停用', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_product_category_store_status` (`store_id`, `status`, `deleted_at`, `sort`, `name`) +) COMMENT='GAO 产品分类'; + +CREATE TABLE IF NOT EXISTS `gao_product` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID', + `category_id` VARCHAR(36) NULL COMMENT '产品分类 ID', + `code` VARCHAR(64) NOT NULL COMMENT '门店内产品编码', + `name` VARCHAR(128) NOT NULL COMMENT '产品名称', + `product_type` VARCHAR(32) NOT NULL COMMENT 'SERVICE 服务,GOODS 实物,CONSUMABLE 消耗品', + `base_unit` VARCHAR(32) NOT NULL COMMENT '基础库存单位', + `stock_enabled` TINYINT(1) NOT NULL COMMENT 'true 为启用库存管理', + `allow_decimal_quantity` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'true 为允许业务数量保留小数', + `status` VARCHAR(32) NOT NULL COMMENT 'ACTIVE 启用,INACTIVE 停用', + `sale_price` BIGINT NULL COMMENT '建议售价,单位分', + `reference_cost` BIGINT NULL COMMENT '参考成本,单位分', + `min_stock_quantity` DECIMAL(18,3) NULL COMMENT '最低库存,基础单位', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `remark` VARCHAR(255) NULL COMMENT '备注', + `updated_by` VARCHAR(36) NOT NULL COMMENT '最后修改用户 ID', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_store_code` (`store_id`, `code`), + KEY `idx_gao_product_store_category` (`store_id`, `category_id`, `status`, `deleted_at`, `sort`, `name`) +) COMMENT='GAO 产品'; + +CREATE TABLE IF NOT EXISTS `gao_product_unit` ( + `id` VARCHAR(36) NOT NULL, + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `name` VARCHAR(32) NOT NULL COMMENT '单位名称', + `conversion_to_base` DECIMAL(18,6) NOT NULL COMMENT '折算为基础单位的比例', + `is_base` TINYINT(1) NOT NULL COMMENT 'true 为基础库存单位', + `allow_purchase` TINYINT(1) NOT NULL COMMENT 'true 为允许采购入库', + `allow_sale` TINYINT(1) NOT NULL COMMENT 'true 为允许销售或套餐配置', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_product_unit_product` (`product_id`, `deleted_at`, `sort`) +) COMMENT='GAO 产品单位'; diff --git a/src/main/resources/db/migration/timiserver/V54__create_gao_product_stock_tables.sql b/src/main/resources/db/migration/timiserver/V54__create_gao_product_stock_tables.sql new file mode 100644 index 0000000..11644fd --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V54__create_gao_product_stock_tables.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS `gao_product_stock` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `on_hand_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '实际库存,基础单位', + `reserved_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '预占库存,基础单位', + `updated_by` VARCHAR(36) NOT NULL COMMENT '最后操作人', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_stock_store_product` (`store_id`, `product_id`), + KEY `idx_gao_product_stock_product` (`product_id`) +) COMMENT='GAO 产品库存余额'; + +CREATE TABLE IF NOT EXISTS `gao_stock_record` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + `quantity` DECIMAL(18,3) NOT NULL COMMENT '业务数量', + `base_quantity` DECIMAL(18,3) NOT NULL COMMENT '基础单位数量', + `operation_type` VARCHAR(32) NOT NULL COMMENT '库存操作类型', + `on_hand_delta` DECIMAL(18,3) NOT NULL COMMENT '实际库存增量', + `reserved_delta` DECIMAL(18,3) NOT NULL COMMENT '预占库存增量', + `on_hand_before` DECIMAL(18,3) NOT NULL COMMENT '操作前实际库存', + `on_hand_after` DECIMAL(18,3) NOT NULL COMMENT '操作后实际库存', + `reserved_before` DECIMAL(18,3) NOT NULL COMMENT '操作前预占库存', + `reserved_after` DECIMAL(18,3) NOT NULL COMMENT '操作后预占库存', + `biz_type` VARCHAR(32) NOT NULL COMMENT '来源业务类型', + `biz_id` VARCHAR(36) NOT NULL COMMENT '来源业务 ID', + `source_record_id` VARCHAR(36) NULL COMMENT '冲正来源流水 ID', + `request_no` VARCHAR(128) NOT NULL COMMENT '幂等请求号', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '操作人', + `occurred_at` BIGINT NOT NULL COMMENT '发生时间', + `reason` TEXT NULL COMMENT '原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_stock_record_request_product` (`store_id`, `request_no`, `operation_type`, `product_id`), + UNIQUE KEY `uk_gao_stock_record_source` (`source_record_id`), + KEY `idx_gao_stock_record_product_time` (`store_id`, `product_id`, `occurred_at`, `id`), + KEY `idx_gao_stock_record_biz_time` (`store_id`, `biz_type`, `biz_id`, `occurred_at`, `id`) +) COMMENT='GAO 库存变更流水'; diff --git a/src/main/resources/db/migration/timiserver/V55__create_gao_sales_order_tables.sql b/src/main/resources/db/migration/timiserver/V55__create_gao_sales_order_tables.sql new file mode 100644 index 0000000..321fdb1 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V55__create_gao_sales_order_tables.sql @@ -0,0 +1,61 @@ +ALTER TABLE `gao_store` + ADD COLUMN `direct_product_sale_event_id` VARCHAR(36) NULL COMMENT '直接产品订单使用的消费事件 ID' AFTER `auto_closed_day`, + ADD KEY `idx_gao_store_direct_product_sale_event` (`direct_product_sale_event_id`); + +CREATE TABLE IF NOT EXISTS `gao_sales_order` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `order_no` VARCHAR(64) NOT NULL COMMENT '门店内订单号', + `order_type` VARCHAR(32) NOT NULL COMMENT 'PACKAGE 套餐,DIRECT_PRODUCT 直接产品', + `status` VARCHAR(32) NOT NULL COMMENT '订单状态', + `payment_status` VARCHAR(32) NOT NULL COMMENT '付款状态', + `refund_status` VARCHAR(32) NOT NULL COMMENT '退款状态', + `sale_amount` BIGINT NOT NULL COMMENT '成交金额,单位分', + `pending_paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '待核销收款,单位分', + `paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '有效收款,单位分', + `refunded_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '有效退款,单位分', + `source_event_record_id` VARCHAR(36) NULL COMMENT '来源事件记录 ID', + `request_no` VARCHAR(128) NOT NULL COMMENT '创建订单幂等号', + `purchased_at` BIGINT NOT NULL COMMENT '下单时间', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '下单人', + `cancelled_at` BIGINT NULL COMMENT '取消时间', + `cancel_operator_user_id` VARCHAR(36) NULL COMMENT '取消人', + `cancel_reason` TEXT NULL COMMENT '取消原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_sales_order_store_no` (`store_id`, `order_no`), + UNIQUE KEY `uk_gao_sales_order_store_request` (`store_id`, `request_no`), + UNIQUE KEY `uk_gao_sales_order_source_event` (`source_event_record_id`), + KEY `idx_gao_sales_order_customer` (`store_id`, `customer_id`, `status`, `deleted_at`, `purchased_at`), + KEY `idx_gao_sales_order_payment` (`store_id`, `order_type`, `payment_status`, `refund_status`, `deleted_at`, `purchased_at`) +) COMMENT='GAO 销售订单'; + +CREATE TABLE IF NOT EXISTS `gao_sales_order_item` ( + `id` VARCHAR(36) NOT NULL, + `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + `quantity` DECIMAL(18,3) NOT NULL COMMENT '购买数量', + `reserved_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '待履约数量', + `fulfilled_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已履约数量', + `unit_price` BIGINT NOT NULL COMMENT '实际单价,单位分', + `line_amount` BIGINT NOT NULL COMMENT '行金额,单位分', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `remark` VARCHAR(255) NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_sales_order_item_product_unit` (`sales_order_id`, `product_id`, `product_unit_id`), + KEY `idx_gao_sales_order_item_order` (`sales_order_id`, `sort`, `created_at`) +) COMMENT='GAO 直接产品订单明细'; + +ALTER TABLE `gao_fund_record` + MODIFY COLUMN `biz_type` VARCHAR(32) NOT NULL COMMENT '业务类型,销售订单'; diff --git a/src/main/resources/db/migration/timiserver/V56__add_gao_package_product_snapshots.sql b/src/main/resources/db/migration/timiserver/V56__add_gao_package_product_snapshots.sql new file mode 100644 index 0000000..2fbd2ef --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V56__add_gao_package_product_snapshots.sql @@ -0,0 +1,29 @@ +-- 套餐功能尚未上线,旧套餐记录无法可靠补齐产品主数据引用,清理不可兼容的旧数据 +DELETE FROM `gao_customer_package_item`; +DELETE FROM `gao_customer_package`; +DELETE FROM `gao_package_config_item`; +UPDATE `gao_package_config` + SET `status` = 'INACTIVE', `deleted_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `deleted_at` IS NULL; +UPDATE `gao_event` + SET `status` = 'DELETED', `deleted_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `biz_type` = 'PACKAGE_PURCHASE' AND `deleted_at` IS NULL; + +ALTER TABLE `gao_package_config_item` + ADD COLUMN IF NOT EXISTS `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID' AFTER `package_config_id`, + ADD COLUMN IF NOT EXISTS `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID' AFTER `product_id`, + ADD COLUMN IF NOT EXISTS `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照' AFTER `product_unit_id`, + ADD COLUMN IF NOT EXISTS `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照' AFTER `product_name_snapshot`, + ADD COLUMN IF NOT EXISTS `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照' AFTER `unit_snapshot`, + ADD KEY IF NOT EXISTS `idx_gao_package_config_item_product` (`product_id`, `product_unit_id`); + +ALTER TABLE `gao_customer_package` + ADD COLUMN IF NOT EXISTS `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID' AFTER `id`, + ADD UNIQUE KEY IF NOT EXISTS `uk_gao_customer_package_sales_order` (`sales_order_id`); + +ALTER TABLE `gao_customer_package_item` + ADD COLUMN IF NOT EXISTS `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID' AFTER `customer_package_id`, + ADD COLUMN IF NOT EXISTS `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID' AFTER `product_id`, + ADD COLUMN IF NOT EXISTS `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照' AFTER `unit_snapshot`, + ADD COLUMN IF NOT EXISTS `fulfilled_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已履约数量' AFTER `reserved_quantity`, + ADD KEY IF NOT EXISTS `idx_gao_customer_package_item_product` (`product_id`, `product_unit_id`); diff --git a/src/main/resources/db/migration/timiserver/V57__create_gao_order_fulfillment_tables.sql b/src/main/resources/db/migration/timiserver/V57__create_gao_order_fulfillment_tables.sql new file mode 100644 index 0000000..6ea9455 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V57__create_gao_order_fulfillment_tables.sql @@ -0,0 +1,49 @@ +CREATE TABLE IF NOT EXISTS `gao_order_fulfillment` ( + `id` VARCHAR(36) NOT NULL, + `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID', + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `fulfillment_type` VARCHAR(32) NOT NULL COMMENT '履约类型', + `status` VARCHAR(32) NOT NULL COMMENT '履约状态', + `request_no` VARCHAR(128) NOT NULL COMMENT '履约幂等号', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '提交人', + `delivered_at` BIGINT NOT NULL COMMENT '实际履约时间', + `verifier_user_id` VARCHAR(36) NULL COMMENT '核销或拒绝操作人', + `verified_at` BIGINT NULL COMMENT '核销或拒绝时间', + `source_fulfillment_id` VARCHAR(36) NULL COMMENT '冲正来源履约单 ID', + `reason` TEXT NULL COMMENT '原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_order_fulfillment_store_request` (`store_id`, `request_no`), + UNIQUE KEY `uk_gao_order_fulfillment_source` (`source_fulfillment_id`), + KEY `idx_gao_order_fulfillment_order` (`sales_order_id`, `created_at`), + KEY `idx_gao_order_fulfillment_customer` (`store_id`, `customer_id`, `status`, `delivered_at`), + KEY `idx_gao_order_fulfillment_status` (`store_id`, `status`, `delivered_at`) +) COMMENT='GAO 统一履约单'; + +CREATE TABLE IF NOT EXISTS `gao_order_fulfillment_item` ( + `id` VARCHAR(36) NOT NULL, + `fulfillment_id` VARCHAR(36) NOT NULL COMMENT '履约单 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + `source_order_item_id` VARCHAR(36) NULL COMMENT '直接订单明细 ID', + `source_customer_package_item_id` VARCHAR(36) NULL COMMENT '客户套餐权益项 ID', + `quantity` DECIMAL(18,3) NOT NULL COMMENT '本次履约数量', + `base_quantity` DECIMAL(18,3) NOT NULL COMMENT '基础单位数量', + `reserved_before` DECIMAL(18,3) NOT NULL COMMENT '操作前待履约数量', + `reserved_after` DECIMAL(18,3) NOT NULL COMMENT '操作后待履约数量', + `fulfilled_before` DECIMAL(18,3) NOT NULL COMMENT '操作前已履约数量', + `fulfilled_after` DECIMAL(18,3) NOT NULL COMMENT '操作后已履约数量', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + `created_at` BIGINT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_order_fulfillment_item_order` (`fulfillment_id`, `source_order_item_id`), + UNIQUE KEY `uk_gao_order_fulfillment_item_package` (`fulfillment_id`, `source_customer_package_item_id`), + KEY `idx_gao_order_fulfillment_item_fulfillment` (`fulfillment_id`, `created_at`), + KEY `idx_gao_order_fulfillment_item_order_source` (`source_order_item_id`), + KEY `idx_gao_order_fulfillment_item_package_source` (`source_customer_package_item_id`) +) COMMENT='GAO 统一履约明细'; diff --git a/src/main/resources/db/migration/timiserver/V58__remove_gao_package_legacy_chain.sql b/src/main/resources/db/migration/timiserver/V58__remove_gao_package_legacy_chain.sql new file mode 100644 index 0000000..28267b1 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V58__remove_gao_package_legacy_chain.sql @@ -0,0 +1,34 @@ +ALTER TABLE `gao_package_config_item` + MODIFY COLUMN `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + MODIFY COLUMN `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + MODIFY COLUMN `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + MODIFY COLUMN `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + MODIFY COLUMN `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + DROP COLUMN `product_name`, + DROP COLUMN `unit`, + ADD UNIQUE KEY `uk_gao_package_config_item_product_unit` (`package_config_id`, `product_id`, `product_unit_id`, `deleted_at`); + +ALTER TABLE `gao_customer_package` + MODIFY COLUMN `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID', + DROP INDEX `uk_gao_customer_package_purchase_event`, + DROP INDEX `uk_gao_customer_package_request`, + DROP INDEX `idx_gao_customer_package_payment`, + DROP COLUMN `purchase_event_record_id`, + DROP COLUMN `request_no`, + DROP COLUMN `sale_amount`, + DROP COLUMN `pending_paid_amount`, + DROP COLUMN `paid_amount`, + DROP COLUMN `refunded_amount`, + DROP COLUMN `payment_status`, + DROP COLUMN `refund_status`, + DROP COLUMN `purchased_at`, + DROP COLUMN `operator_user_id`; + +ALTER TABLE `gao_customer_package_item` + MODIFY COLUMN `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + MODIFY COLUMN `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + MODIFY COLUMN `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + DROP COLUMN `delivered_quantity`; + +DROP TABLE IF EXISTS `gao_package_delivery_item`; +DROP TABLE IF EXISTS `gao_package_delivery`; diff --git a/src/main/resources/db/migration/timiserver/V59__move_gao_product_sale_price_to_unit.sql b/src/main/resources/db/migration/timiserver/V59__move_gao_product_sale_price_to_unit.sql new file mode 100644 index 0000000..b23d68f --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V59__move_gao_product_sale_price_to_unit.sql @@ -0,0 +1,28 @@ +ALTER TABLE `gao_product_unit` + ADD COLUMN IF NOT EXISTS `sale_price` BIGINT NULL COMMENT '该业务单位建议售价,单位分' AFTER `name`; + +UPDATE `gao_product_unit` u + JOIN `gao_product` p ON p.`id` = u.`product_id` + SET u.`sale_price` = p.`sale_price` + WHERE u.`sale_price` IS NULL; + +ALTER TABLE `gao_product_unit` + CHANGE COLUMN `conversion_to_base` `base_quantity_per_unit` DECIMAL(18,6) NOT NULL COMMENT '每个业务单位对应的基础单位数量'; + +ALTER TABLE `gao_stock_record` + CHANGE COLUMN `conversion_to_base_snapshot` `base_quantity_per_unit_snapshot` DECIMAL(18,6) NOT NULL COMMENT '基础单位数量系数快照'; + +ALTER TABLE `gao_sales_order_item` + CHANGE COLUMN `conversion_to_base_snapshot` `base_quantity_per_unit_snapshot` DECIMAL(18,6) NOT NULL COMMENT '基础单位数量系数快照'; + +ALTER TABLE `gao_package_config_item` + CHANGE COLUMN `conversion_to_base_snapshot` `base_quantity_per_unit_snapshot` DECIMAL(18,6) NOT NULL COMMENT '基础单位数量系数快照'; + +ALTER TABLE `gao_customer_package_item` + CHANGE COLUMN `conversion_to_base_snapshot` `base_quantity_per_unit_snapshot` DECIMAL(18,6) NOT NULL COMMENT '基础单位数量系数快照'; + +ALTER TABLE `gao_order_fulfillment_item` + CHANGE COLUMN `conversion_to_base_snapshot` `base_quantity_per_unit_snapshot` DECIMAL(18,6) NOT NULL COMMENT '基础单位数量系数快照'; + +ALTER TABLE `gao_product` + DROP COLUMN `sale_price`; diff --git a/src/main/resources/db/migration/timiserver/V60__remove_gao_product_base_unit.sql b/src/main/resources/db/migration/timiserver/V60__remove_gao_product_base_unit.sql new file mode 100644 index 0000000..67ba316 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V60__remove_gao_product_base_unit.sql @@ -0,0 +1,2 @@ +ALTER TABLE `gao_product` + DROP COLUMN `base_unit`; diff --git a/src/main/resources/db/migration/timiserver/V61__remove_gao_product_reference_cost.sql b/src/main/resources/db/migration/timiserver/V61__remove_gao_product_reference_cost.sql new file mode 100644 index 0000000..c234533 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V61__remove_gao_product_reference_cost.sql @@ -0,0 +1,2 @@ +ALTER TABLE `gao_product` + DROP COLUMN `reference_cost`; diff --git a/src/main/resources/db/migration/timiserver/V62__rename_gao_stock_record_and_remove_reason.sql b/src/main/resources/db/migration/timiserver/V62__rename_gao_stock_record_and_remove_reason.sql new file mode 100644 index 0000000..fef9b83 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V62__rename_gao_stock_record_and_remove_reason.sql @@ -0,0 +1,12 @@ +RENAME TABLE `gao_stock_record` TO `gao_product_stock_record`; + +ALTER TABLE `gao_product_stock_record` + DROP KEY `uk_gao_stock_record_request_product`, + DROP KEY `uk_gao_stock_record_source`, + DROP KEY `idx_gao_stock_record_product_time`, + DROP KEY `idx_gao_stock_record_biz_time`, + DROP COLUMN `reason`, + ADD UNIQUE KEY `uk_gao_product_stock_record_request_product` (`store_id`, `request_no`, `operation_type`, `product_id`), + ADD UNIQUE KEY `uk_gao_product_stock_record_source` (`source_record_id`), + ADD KEY `idx_gao_product_stock_record_product_time` (`store_id`, `product_id`, `occurred_at`, `id`), + ADD KEY `idx_gao_product_stock_record_biz_time` (`store_id`, `biz_type`, `biz_id`, `occurred_at`, `id`); diff --git a/src/main/resources/db/migration/timiserver/V63__create_groovy_script_tables.sql b/src/main/resources/db/migration/timiserver/V63__create_groovy_script_tables.sql new file mode 100644 index 0000000..2309aef --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V63__create_groovy_script_tables.sql @@ -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; diff --git a/src/main/resources/db/migration/timiserver/V64__add_gao_consumption_and_point_script_model.sql b/src/main/resources/db/migration/timiserver/V64__add_gao_consumption_and_point_script_model.sql new file mode 100644 index 0000000..d14f80b --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V64__add_gao_consumption_and_point_script_model.sql @@ -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; diff --git a/src/main/resources/db/migration/timiserver/V65__migrate_gao_point_rules_to_groovy.sql b/src/main/resources/db/migration/timiserver/V65__migrate_gao_point_rules_to_groovy.sql new file mode 100644 index 0000000..ccb4858 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V65__migrate_gao_point_rules_to_groovy.sql @@ -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'; diff --git a/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql b/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql new file mode 100644 index 0000000..91f0a51 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql @@ -0,0 +1,44 @@ +ALTER TABLE `gao_point_ledger` + ADD COLUMN IF NOT EXISTS `sales_order_id` VARCHAR(36) NULL COMMENT '关联销售订单 ID' AFTER `trigger_source_id`, + ADD KEY IF NOT EXISTS `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` = `gao_customer_consumption_record`.`id`; diff --git a/src/main/resources/db/migration/timiserver/V67__fix_gao_point_rule_trigger_ref_type.sql b/src/main/resources/db/migration/timiserver/V67__fix_gao_point_rule_trigger_ref_type.sql new file mode 100644 index 0000000..849de79 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V67__fix_gao_point_rule_trigger_ref_type.sql @@ -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'; diff --git a/src/main/resources/db/migration/timiserver/V68__snapshot_gao_fund_record_order_unpaid_amount.sql b/src/main/resources/db/migration/timiserver/V68__snapshot_gao_fund_record_order_unpaid_amount.sql new file mode 100644 index 0000000..374ccf4 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V68__snapshot_gao_fund_record_order_unpaid_amount.sql @@ -0,0 +1,2 @@ +ALTER TABLE `gao_fund_record` + ADD COLUMN IF NOT EXISTS `sales_order_unpaid_amount_snapshot` BIGINT NULL COMMENT '流水发生时销售订单剩余待收款金额快照,单位分' AFTER `amount`; diff --git a/src/main/resources/db/migration/timiserver/V69__unify_gao_package_order_fulfillment.sql b/src/main/resources/db/migration/timiserver/V69__unify_gao_package_order_fulfillment.sql new file mode 100644 index 0000000..622854d --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V69__unify_gao_package_order_fulfillment.sql @@ -0,0 +1,38 @@ +ALTER TABLE `gao_sales_order_item` + ADD COLUMN `item_type` VARCHAR(32) NOT NULL DEFAULT 'DIRECT_PRODUCT' COMMENT 'DIRECT_PRODUCT 直接产品,PACKAGE_ENTITLEMENT 套餐权益' AFTER `sales_order_id`, + MODIFY COLUMN `unit_price` BIGINT NULL COMMENT '实际单价,套餐权益为空', + MODIFY COLUMN `line_amount` BIGINT NULL COMMENT '行金额,套餐权益为空'; + +ALTER TABLE `gao_order_fulfillment_item` + DROP KEY `uk_gao_order_fulfillment_item_package`, + DROP KEY `idx_gao_order_fulfillment_item_package_source`, + MODIFY COLUMN `source_order_item_id` VARCHAR(36) NOT NULL COMMENT '来源销售订单项 ID', + DROP COLUMN `source_customer_package_item_id`; + +ALTER TABLE `gao_order_fulfillment` + DROP COLUMN `fulfillment_type`; + +ALTER TABLE `gao_customer_package` + DROP KEY `idx_gao_customer_package_customer`, + DROP COLUMN `status`, + DROP COLUMN `cancelled_at`, + DROP COLUMN `cancel_operator_user_id`, + DROP COLUMN `cancel_reason`; + +DROP TABLE IF EXISTS `gao_customer_package_item`; + +DELETE role_permission +FROM `role_permission` role_permission +INNER JOIN `permission` permission ON permission.`id` = role_permission.`permission_id` +WHERE permission.`module_code` = 'GAO' + AND permission.`code` IN ('PACKAGE_DELIVERY:CREATE', 'PACKAGE_DELIVERY:READ', 'PACKAGE_DELIVERY:VERIFY', 'PACKAGE_DELIVERY:REVOKE'); + +DELETE role_permission_delegation +FROM `role_permission_delegation` role_permission_delegation +INNER JOIN `permission` permission ON permission.`id` = role_permission_delegation.`permission_id` +WHERE permission.`module_code` = 'GAO' + AND permission.`code` IN ('PACKAGE_DELIVERY:CREATE', 'PACKAGE_DELIVERY:READ', 'PACKAGE_DELIVERY:VERIFY', 'PACKAGE_DELIVERY:REVOKE'); + +DELETE FROM `permission` +WHERE `module_code` = 'GAO' + AND `code` IN ('PACKAGE_DELIVERY:CREATE', 'PACKAGE_DELIVERY:READ', 'PACKAGE_DELIVERY:VERIFY', 'PACKAGE_DELIVERY:REVOKE'); diff --git a/src/main/resources/db/migration/timiserver/V70__refactor_gao_product_to_spu_sku.sql b/src/main/resources/db/migration/timiserver/V70__refactor_gao_product_to_spu_sku.sql new file mode 100644 index 0000000..901714c --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V70__refactor_gao_product_to_spu_sku.sql @@ -0,0 +1,139 @@ +-- GAO 产品模块尚未上线,直接切换为 SPU -> SKU -> SKU 单位模型,不保留旧产品单位引用 + +CREATE TABLE `gao_product_spec` ( + `id` VARCHAR(36) NOT NULL, + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `name` VARCHAR(32) NOT NULL COMMENT '规格项名称', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_spec_product_name` (`product_id`, `name`), + KEY `idx_gao_product_spec_product` (`product_id`, `deleted_at`, `sort`, `created_at`) +) COMMENT='GAO 产品规格项'; + +CREATE TABLE `gao_product_spec_value` ( + `id` VARCHAR(36) NOT NULL, + `spec_id` VARCHAR(36) NOT NULL COMMENT '规格项 ID', + `name` VARCHAR(64) NOT NULL COMMENT '规格值名称', + `enabled` TINYINT(1) NOT NULL COMMENT 'true 为启用', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_spec_value_spec_name` (`spec_id`, `name`), + KEY `idx_gao_product_spec_value_spec` (`spec_id`, `enabled`, `deleted_at`, `sort`, `created_at`) +) COMMENT='GAO 产品规格值'; + +CREATE TABLE `gao_product_sku` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '所属产品 ID', + `code` VARCHAR(64) NOT NULL COMMENT '门店内 SKU 编码', + `barcode` VARCHAR(128) NULL COMMENT '门店内条码', + `name` VARCHAR(128) NOT NULL COMMENT 'SKU 展示名称', + `spec_summary` VARCHAR(255) NULL COMMENT '规格展示摘要', + `spec_value_key` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '按规格项排序的规格值组合键', + `stock_enabled` TINYINT(1) NOT NULL COMMENT 'true 为启用库存管理', + `allow_decimal_quantity` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'true 为允许业务数量保留小数', + `min_stock_quantity` DECIMAL(18,3) NULL COMMENT '最低库存,基础单位', + `status` VARCHAR(32) NOT NULL COMMENT 'ACTIVE 启用,INACTIVE 停用', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `remark` VARCHAR(255) NULL COMMENT '备注', + `updated_by` VARCHAR(36) NOT NULL COMMENT '最后修改用户 ID', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_sku_store_code` (`store_id`, `code`), + UNIQUE KEY `uk_gao_product_sku_store_barcode` (`store_id`, `barcode`), + UNIQUE KEY `uk_gao_product_sku_product_spec_value_key` (`product_id`, `spec_value_key`), + KEY `idx_gao_product_sku_product` (`product_id`, `status`, `deleted_at`, `sort`, `name`), + KEY `idx_gao_product_sku_store_status` (`store_id`, `status`, `deleted_at`, `sort`, `name`) +) COMMENT='GAO 产品 SKU'; + +CREATE TABLE `gao_product_sku_spec_value` ( + `id` VARCHAR(36) NOT NULL, + `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + `spec_value_id` VARCHAR(36) NOT NULL COMMENT '规格值 ID', + `created_at` BIGINT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_sku_spec_value_sku_value` (`sku_id`, `spec_value_id`), + KEY `idx_gao_product_sku_spec_value_value` (`spec_value_id`, `sku_id`) +) COMMENT='GAO SKU 规格值关联'; + +CREATE TABLE `gao_product_sku_unit` ( + `id` VARCHAR(36) NOT NULL, + `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + `name` VARCHAR(32) NOT NULL COMMENT '单位名称', + `base_quantity_per_unit` DECIMAL(18,6) NOT NULL COMMENT '每个业务单位对应的基础单位数量', + `sale_price` BIGINT NULL COMMENT '该业务单位建议售价,单位分', + `is_base` TINYINT(1) NOT NULL COMMENT 'true 为基础库存单位', + `allow_purchase` TINYINT(1) NOT NULL COMMENT 'true 为允许采购入库', + `allow_sale` TINYINT(1) NOT NULL COMMENT 'true 为允许销售和套餐配置', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_sku_unit_sku_name` (`sku_id`, `name`), + KEY `idx_gao_product_sku_unit_sku` (`sku_id`, `deleted_at`, `sort`) +) COMMENT='GAO 产品 SKU 单位'; + +ALTER TABLE `gao_product` + DROP COLUMN `allow_decimal_quantity`, + DROP COLUMN `min_stock_quantity`; + +DROP TABLE `gao_product_unit`; + +ALTER TABLE `gao_product_stock` + DROP KEY `uk_gao_product_stock_store_product`, + DROP KEY `idx_gao_product_stock_product`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + ADD UNIQUE KEY `uk_gao_product_stock_store_sku` (`store_id`, `sku_id`), + ADD KEY `idx_gao_product_stock_sku` (`sku_id`); + +ALTER TABLE `gao_product_stock_record` + DROP KEY `uk_gao_product_stock_record_request_product`, + DROP KEY `idx_gao_product_stock_record_product_time`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD UNIQUE KEY `uk_gao_product_stock_record_request_sku` (`store_id`, `request_no`, `operation_type`, `sku_id`), + ADD KEY `idx_gao_product_stock_record_sku_time` (`store_id`, `sku_id`, `occurred_at`, `id`); + +ALTER TABLE `gao_sales_order_item` + DROP KEY `uk_gao_sales_order_item_product_unit`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD UNIQUE KEY `uk_gao_sales_order_item_sku_unit` (`sales_order_id`, `sku_id`, `sku_unit_id`); + +ALTER TABLE `gao_package_config_item` + DROP KEY `idx_gao_package_config_item_product`, + DROP KEY `uk_gao_package_config_item_product_unit`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD KEY `idx_gao_package_config_item_sku` (`sku_id`, `sku_unit_id`), + ADD UNIQUE KEY `uk_gao_package_config_item_sku_unit` (`package_config_id`, `sku_id`, `sku_unit_id`); + +ALTER TABLE `gao_order_fulfillment_item` + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD KEY `idx_gao_order_fulfillment_item_sku` (`sku_id`, `sku_unit_id`); diff --git a/src/main/resources/db/migration/timiserver/V71__snapshot_gao_sku_spec_structure.sql b/src/main/resources/db/migration/timiserver/V71__snapshot_gao_sku_spec_structure.sql new file mode 100644 index 0000000..4241012 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V71__snapshot_gao_sku_spec_structure.sql @@ -0,0 +1,5 @@ +ALTER TABLE `gao_product_stock_record` + ADD COLUMN `sku_spec_snapshot` JSON NOT NULL COMMENT 'SKU 规格结构快照' AFTER `spec_summary_snapshot`; + +ALTER TABLE `gao_order_fulfillment_item` + ADD COLUMN `sku_spec_snapshot` JSON NOT NULL COMMENT 'SKU 规格结构快照' AFTER `spec_summary_snapshot`; diff --git a/src/main/resources/db/migration/timiserver/V72__snapshot_gao_sales_order_sku_spec_structure.sql b/src/main/resources/db/migration/timiserver/V72__snapshot_gao_sales_order_sku_spec_structure.sql new file mode 100644 index 0000000..b20160c --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V72__snapshot_gao_sales_order_sku_spec_structure.sql @@ -0,0 +1,2 @@ +ALTER TABLE `gao_sales_order_item` + ADD COLUMN `sku_spec_snapshot` JSON NOT NULL COMMENT 'SKU 规格结构快照' AFTER `spec_summary_snapshot`; diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerConsumptionRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerConsumptionRecordMapper.xml new file mode 100644 index 0000000..79f092c --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerConsumptionRecordMapper.xml @@ -0,0 +1,20 @@ + + + + + + + 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 + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml deleted file mode 100644 index fbb8168..0000000 --- a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - UPDATE `gao_customer_package_item` - SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `deleted_at` IS NULL - AND `reserved_quantity` + #{delta} >= 0 - AND `reserved_quantity` + `delivered_quantity` + #{delta} <= `total_quantity` - - - UPDATE `gao_customer_package_item` - SET `delivered_quantity` = `delivered_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `deleted_at` IS NULL - AND `delivered_quantity` + #{delta} >= 0 - AND `reserved_quantity` + `delivered_quantity` + #{delta} <= `total_quantity` - - diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml index d6696fc..f5c2975 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml @@ -1,62 +1,30 @@ - SELECT * FROM `gao_customer_package` - WHERE `store_id` = #{storeId} AND `request_no` = #{requestNo} - LIMIT 1 - - - - p.`deleted_at` IS NULL AND p.`store_id` = #{storeId} AND p.`customer_id` = #{customerId} - AND p.`status` = #{status} - AND p.`payment_status` = #{paymentStatus} - AND p.`refund_status` = #{refundStatus} - AND p.`purchased_at` >= #{beginAt} - AND p.`purchased_at` <= #{endAt} + AND o.`status` = #{status} AND (p.`package_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR c.`name` LIKE CONCAT('%', #{keyword}, '%') OR c.`code` LIKE CONCAT('%', #{keyword}, '%')) - - UPDATE `gao_customer_package` - SET `pending_paid_amount` = `pending_paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `deleted_at` IS NULL AND `pending_paid_amount` + #{delta} >= 0 - - - UPDATE `gao_customer_package` - SET `paid_amount` = `paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `deleted_at` IS NULL AND `paid_amount` + #{delta} >= 0 - - - UPDATE `gao_customer_package` - SET `status` = #{toStatus}, `cancelled_at` = #{cancelledAt}, `cancel_operator_user_id` = #{cancelOperatorUserId}, `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 - diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml index 9b08997..9c5283e 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml @@ -26,6 +26,7 @@ WHERE `store_id` = #{storeId} 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 (`end_at` IS NULL OR #{now} <= `end_at`) AND `deleted_at` IS NULL diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml index 869d9c6..55ee1e1 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml @@ -119,6 +119,12 @@ #{eventId} + + 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 + ) + AND EXISTS ( @@ -212,6 +218,12 @@ #{eventId} + + 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 + ) + AND r.`number_value` IS NOT NULL @@ -386,6 +398,12 @@ #{eventId} + + 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 + ) + SELECT diff --git a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml index 9b445d6..fa509cd 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml @@ -11,6 +11,13 @@ WHERE `id` = #{id} AND `deleted_at` IS NULL FOR UPDATE + f.`deleted_at` IS NULL AND f.`store_id` = #{storeId} AND f.`biz_id` = #{customerPackageId} + AND f.`biz_id` = #{salesOrderId} AND f.`customer_id` = #{customerId} + AND f.`biz_type` = #{bizType} AND f.`operation_type` = #{operationType} AND f.`status` = #{status} AND f.`occurred_at` >= #{beginAt} @@ -39,7 +57,16 @@ SELECT COUNT(1) FROM `gao_fund_record` f WHERE diff --git a/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml new file mode 100644 index 0000000..0d4854b --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentMapper.xml new file mode 100644 index 0000000..b783c47 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentMapper.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + 1 = 1 + AND `store_id` = #{storeId} + AND `sales_order_id` = #{salesOrderId} + AND `customer_id` = #{customerId} + AND `status` = #{status} + AND `delivered_at` >= #{beginAt} + AND `delivered_at` <= #{endAt} + + + + + UPDATE `gao_order_fulfillment` + SET `status` = #{status}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, + `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoOrderReminderMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoOrderReminderMapper.xml new file mode 100644 index 0000000..e1443e0 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoOrderReminderMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml deleted file mode 100644 index 3c723c7..0000000 --- a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml deleted file mode 100644 index 3f2c6c4..0000000 --- a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - d.`deleted_at` IS NULL - AND d.`store_id` = #{storeId} - AND d.`customer_package_id` = #{customerPackageId} - AND d.`customer_id` = #{customerId} - AND d.`status` = #{status} - AND d.`operation_type` = #{operationType} - AND d.`delivered_at` >= #{beginAt} - AND d.`delivered_at` <= #{endAt} - - - - - UPDATE `gao_package_delivery` - SET `status` = #{toStatus}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL - - diff --git a/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml index 1589b75..6fc810c 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml @@ -9,15 +9,26 @@ LIMIT 1 - SELECT * 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 `deleted_at` IS NULL ORDER BY `created_at` ASC, `id` ASC + + - SELECT * FROM `gao_point_rule` WHERE `store_id` = #{storeId} - AND `event_id` = #{eventId} + AND `trigger_source_type` = #{triggerSourceType} + AND (`trigger_ref_id` IS NULL + + OR `trigger_ref_id` = #{triggerRefId} + + ) AND `status` = 'ACTIVE' AND (`begin_at` IS NULL OR `begin_at` <= #{now}) AND (`end_at` IS NULL OR #{now} <= `end_at`) diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductCategoryMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductCategoryMapper.xml new file mode 100644 index 0000000..1e56b64 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductCategoryMapper.xml @@ -0,0 +1,63 @@ + + + + + + + UPDATE `gao_product_category` + SET + `sort` = CASE `id` + + WHEN #{id} THEN #{sort} + + END, + `updated_at` = #{updatedAt} + WHERE `deleted_at` IS NULL + AND `id` IN + + #{id} + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml new file mode 100644 index 0000000..7678d4d --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml @@ -0,0 +1,223 @@ + + + + + AND p.`deleted_at` IS NULL + AND p.`store_id` = #{storeId} + AND p.`id` = #{productId} + AND EXISTS ( + SELECT 1 FROM `gao_product_sku` k_filter + WHERE k_filter.`id` = #{skuId} AND k_filter.`product_id` = p.`id` AND k_filter.`deleted_at` IS NULL + ) + AND p.`category_id` = #{categoryId} + AND p.`product_type` = #{productType} + + + + AND o.`deleted_at` IS NULL + AND i.`deleted_at` IS NULL + AND o.`status` <> 'CANCELLED' + AND o.`store_id` = #{storeId} + AND i.`product_id_snapshot` = #{productId} + AND i.`sku_id` = #{skuId} + AND p.`category_id` = #{categoryId} + AND p.`product_type` = #{productType} + AND o.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductMapper.xml new file mode 100644 index 0000000..b638e34 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml new file mode 100644 index 0000000..2f49939 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml @@ -0,0 +1,199 @@ + + + + + AND o.`deleted_at` IS NULL + AND i.`deleted_at` IS NULL + AND o.`status` <> 'CANCELLED' + AND o.`store_id` = #{storeId} + AND i.`product_id_snapshot` = #{productId} + AND i.`sku_id` = #{skuId} + AND p.`category_id` = #{categoryId} + AND p.`product_type` = #{productType} + AND o.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuMapper.xml new file mode 100644 index 0000000..f46e775 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuSpecValueMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuSpecValueMapper.xml new file mode 100644 index 0000000..4663e47 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuSpecValueMapper.xml @@ -0,0 +1,6 @@ + + + + + DELETE FROM `gao_product_sku_spec_value` WHERE `sku_id`=#{skuId} + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuUnitMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuUnitMapper.xml new file mode 100644 index 0000000..5f206fe --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuUnitMapper.xml @@ -0,0 +1,16 @@ + + + + + + + UPDATE `gao_product_sku_unit` SET `name`=#{name}, `base_quantity_per_unit`=#{baseQuantityPerUnit}, `sale_price`=#{salePrice}, `is_base`=#{isBase}, `allow_purchase`=#{allowPurchase}, `allow_sale`=#{allowSale}, `sort`=#{sort}, `updated_at`=FLOOR(UNIX_TIMESTAMP(NOW(3))*1000) WHERE `id`=#{id} AND `sku_id`=#{skuId} AND `deleted_at` IS NULL + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSpecMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSpecMapper.xml new file mode 100644 index 0000000..60f3e16 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSpecMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSpecValueMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSpecValueMapper.xml new file mode 100644 index 0000000..1976e34 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSpecValueMapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml new file mode 100644 index 0000000..c28beb9 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + INSERT IGNORE INTO `gao_product_stock` + (`id`, `store_id`, `sku_id`, `on_hand_quantity`, `reserved_quantity`, `updated_by`, `created_at`) + VALUES + (#{id}, #{storeId}, #{skuId}, #{onHandQuantity}, #{reservedQuantity}, #{updatedBy}, #{createdAt}) + + + UPDATE `gao_product_stock` + SET `on_hand_quantity` = `on_hand_quantity` + #{onHandDelta}, + `reserved_quantity` = `reserved_quantity` + #{reservedDelta}, + `updated_by` = #{updatedBy}, + `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} + AND `on_hand_quantity` + #{onHandDelta} >= 0 + AND `reserved_quantity` + #{reservedDelta} >= 0 + AND `reserved_quantity` + #{reservedDelta} <= `on_hand_quantity` + #{onHandDelta} + + + UPDATE `gao_product_stock` + SET `reserved_quantity` = `reserved_quantity` + #{quantity}, `updated_by` = #{updatedBy}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `reserved_quantity` + #{quantity} <= `on_hand_quantity` + + + UPDATE `gao_product_stock` + SET `reserved_quantity` = `reserved_quantity` - #{quantity}, `updated_by` = #{updatedBy}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `reserved_quantity` >= #{quantity} + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml new file mode 100644 index 0000000..c8dd5c7 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + + + 1 = 1 + AND r.`store_id` = #{storeId} + AND k.`product_id` = #{productId} + AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%')) + AND r.`biz_type` = #{bizType} + AND r.`biz_id` = #{bizId} + AND r.`operation_type` = #{operationType} + + AND r.`operation_type` = 'ORDER_RESERVE' + AND NOT EXISTS ( + SELECT 1 + FROM `gao_product_stock_record` c + WHERE c.`store_id` = r.`store_id` + AND c.`biz_type` = r.`biz_type` + AND c.`biz_id` = r.`biz_id` + AND c.`sku_id` = r.`sku_id` + AND c.`operation_type` IN ('ORDER_RELEASE', 'ORDER_OUT') + ) + + AND 0 < r.`on_hand_delta` + AND r.`on_hand_delta` < 0 + AND r.`occurred_at` >= #{beginAt} + AND r.`occurred_at` <= #{endAt} + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml new file mode 100644 index 0000000..a78f674 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml @@ -0,0 +1,269 @@ + + + + + AND o.`deleted_at` IS NULL + AND o.`store_id` = #{storeId} + AND o.`customer_id` = #{customerId} + AND o.`order_type` = #{orderType} + AND o.`status` = #{status} + AND o.`payment_status` = #{paymentStatus} + AND o.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + + + + + + + + + + + + + + + + + + + + AND o.`deleted_at` IS NULL + AND o.`store_id` = #{storeId} + AND o.`customer_id` = #{customerId} + AND o.`order_type` = #{orderType} + + AND o.`status` = #{status} + AND o.`status` <> 'CANCELLED' + + AND o.`purchased_at` <= #{endAt} + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml new file mode 100644 index 0000000..54cf638 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + UPDATE `gao_sales_order_item` + SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `reserved_quantity` + #{delta} >= 0 + AND `fulfilled_quantity` + `reserved_quantity` + #{delta} <= `quantity` + + + UPDATE `gao_sales_order_item` + SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `fulfilled_quantity` + #{delta} >= 0 + AND `fulfilled_quantity` + #{delta} <= `quantity` + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml new file mode 100644 index 0000000..ad5a835 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml @@ -0,0 +1,66 @@ + + + + + + + + o.`deleted_at` IS NULL + AND o.`store_id` = #{storeId} + AND o.`customer_id` = #{customerId} + AND o.`order_type` = #{orderType} + AND o.`status` = #{status} + AND o.`payment_status` = #{paymentStatus} + AND o.`refund_status` = #{refundStatus} + AND o.`status` IN ('ACTIVE', 'FULFILLED') AND o.`payment_status` IN ('UNPAID', 'PARTIALLY_PAID') + AND o.`status` = 'FULFILLED' AND o.`paid_amount` < o.`sale_amount` + AND (o.`order_no` LIKE CONCAT('%', #{keyword}, '%') OR c.`name` LIKE CONCAT('%', #{keyword}, '%') OR c.`code` LIKE CONCAT('%', #{keyword}, '%')) + AND o.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + UPDATE `gao_sales_order` + 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) + WHERE `id` = #{id} AND `deleted_at` IS NULL + + + UPDATE `gao_sales_order` + SET `status` = #{toStatus}, `cancelled_at` = #{cancelledAt}, `cancel_operator_user_id` = #{cancelOperatorUserId}, + `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 + + + UPDATE `gao_sales_order` + SET `status` = #{status}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL + + + UPDATE `gao_sales_order` + SET `remark` = #{remark}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml index 18e84a3..04e9ab3 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml @@ -1,6 +1,18 @@ + +