فهرست منبع

积分相关代码提交

yjwang 8 ساعت پیش
والد
کامیت
376880a317

+ 42 - 0
.trae/specs/fix-ybpay-and-promotion-quality/checklist.md

@@ -0,0 +1,42 @@
+# Checklist
+
+## 易宝支付 - 安全性
+- [x] `PayUtil.isSign`(ybPay 版)验签逻辑已修正,签名匹配返回 true
+- [x] `PayUtil.isSign`(pay 版)验签逻辑已修正,签名匹配返回 true
+- [x] 方法名 `isSgin` 已改为 `isSign`,所有调用处已同步更新(PayScrmServiceImpl 3 处)
+- [x] `PayScrmController.ybPayNotify` 双 `@RequestBody` 已修复,参数正确绑定
+- [x] `PayScrmController.ybPayNotify` 验签密钥改为从 DB 配置读取(`fsPayConfig.getYbKey()`)
+- [x] `YbPayController.payNotify` 双 `@RequestBody` 已修复
+- [x] `YbPayController.payNotify` 验签失败不再抛异常,改为返回 `FAIL`
+- [x] `TestYb.java` 硬编码敏感信息已清理(文件已删除)
+
+## 易宝支付 - 健壮性
+- [x] `PayScrmController.ybPayNotify` 的 `params.get("sign")` 已做 null 校验
+- [x] `PayScrmController.ybPayNotify` 的 `order[]` 访问已做数组长度校验
+- [x] `YbPayController.payNotify` 的 `params.get("sign")` 已做 null 校验
+- [x] `YbPayController.payNotify` 的 switch 已补充 default 分支
+- [x] `YbPayController.payNotify` 的 `System.out` 已替换为 Logger
+- [x] `YbPayController.payNotify` 的 `SpringUtils.getBean` 已改为 `@Autowired`
+
+## 易宝支付 - 返回值规范
+- [x] `PayScrmController.ybPayNotify` 返回值统一为 `SUCCESS`/`FAIL`
+- [x] `YbPayController.payNotify` 返回值统一为 `SUCCESS`/`FAIL`
+
+## 满减活动 - 功能正确性
+- [x] `resolveDisabledReason` 中限购判断注释已取消,`limitPerUser` 生效
+- [x] `countEffectiveByActivityAndUser` SQL 已将 `usage_status = 1` 改为 `IN (0, 1)`
+- [x] `applyPromotionFields` 制单改价场景不再重复调用满减(增加 `skipPromotion` 参数)
+- [x] `saveTiers` 保存前按 `thresholdAmount` 升序排序
+- [x] `matchAmountTier` 增加防御性排序
+
+## 满减活动 - 并发与幂等
+- [x] `recordPendingUsage` 插入前已查询 (order_id, activity_id) 是否存在
+- [x] `enableActivity` 并发竞态已修复(条件 UPDATE `enableActivityIfNoConflict` 代替先 count 后 update)
+
+## 验证
+- [x] 验签方法逻辑正确(签名匹配返回 true)— `return sg.equals(sign)` 已确认
+- [x] 易宝回调参数能正确绑定,不发生 NPE — 单一 @RequestBody 已确认
+- [x] 限购次数配置后能实际限制用户参与 — 限购判断已恢复 + 待支付计入统计
+- [x] 满减档位配置顺序混乱时仍能正确匹配 — saveTiers 排序 + matchAmountTier 防御性排序
+- [x] 制单改价订单的 `promotionDiscountAmount` 与 `payPrice` 一致 — skipPromotion 已传入
+- [ ] 项目能正常编译(mvn compile)— 需在 Java 项目环境验证

+ 92 - 0
.trae/specs/fix-ybpay-and-promotion-quality/spec.md

@@ -0,0 +1,92 @@
+# 易宝支付与满减活动代码质量治理 Spec
+
+## Why
+易宝支付回调和满减活动计算代码存在多处严重缺陷:验签逻辑反转、双 `@RequestBody` 致回调失效、验签密钥硬编码、限购功能被注释失效、积分用 double 违反整数约定、满减档位未排序导致匹配错误等。这些问题直接威胁资金安全、计算准确性与功能正确性,违反项目"代码严谨规范、高可用、计算准确"的工程约定。
+
+## What Changes
+- **修复验签逻辑反转**:`PayUtil.isSgin` 方法在签名匹配时返回 false、不匹配时返回 true,逻辑完全反转
+- **修复双 `@RequestBody` 致回调 NPE**:`PayScrmController.ybPayNotify` 和 `YbPayController.payNotify` 使用两个 `@RequestBody`,第二个参数恒为 null 导致 NPE
+- **统一验签密钥来源**:`PayScrmController.ybPayNotify` 改用从 DB 配置读取密钥,不再使用硬编码 `PayConfig.Key`
+- **清理 `TestYb.java` 硬编码敏感信息**:删除生产源码目录中的硬编码商户号、Token、身份证号
+- **补全回调 null 校验与数组越界校验**:所有易宝回调方法的 `params.get("sign")`、`order[]` 访问增加校验
+- **统一回调返回值**:成功统一返回 `SUCCESS`,失败统一返回 `FAIL`,不再大小写混用
+- **`YbPayController.payNotify` 验签失败改返回字符串**:不再抛异常导致易宝重试
+- **修复满减限购失效**:取消 `resolveDisabledReason` 中限购判断的注释,并修改 `countEffectiveByActivityAndUser` 将待支付(status=0)也计入限购统计
+- **修复制单改价满减重复应用**:`applyPromotionFields` 增加 skip 判断,与 `computedOrder` 一致
+- **修复满减档位未排序**:`saveTiers` 按 `thresholdAmount` 排序后再设置 sortOrder
+- **满减 usage 记录增加幂等保护**:`fs_store_promotion_usage` 表增加 (order_id, activity_id) 唯一索引校验
+- **修复 `enableActivity` 并发竞态**:改用数据库唯一约束或 SELECT FOR UPDATE 防止并发启用冲突
+
+## Impact
+- Affected specs: 积分发放规则、多门店积分按比例分配、满减活动计算、支付回调幂等性
+- Affected code:
+  - `fs-service/.../ybPay/util/PayUtil.java`(验签逻辑修复)
+  - `fs-service/.../pay/pay/util/PayUtil.java`(验签逻辑修复)
+  - `fs-user-app/.../store/PayScrmController.java`(双 @RequestBody 修复、密钥来源统一、null 校验)
+  - `fs-user-app/.../YbPayController.java`(双 @RequestBody 修复、验签失败不抛异常、System.out 替换)
+  - `fs-admin/.../hisStore/TestYb.java`(删除或迁移硬编码敏感信息)
+  - `fs-service/.../hisStore/service/impl/FsStorePromotionComputeServiceImpl.java`(限购恢复、usage 幂等)
+  - `fs-service/.../hisStore/service/impl/FsStorePromotionServiceImpl.java`(saveTiers 排序、enableActivity 并发)
+  - `fs-service/.../hisStore/service/impl/FsStoreOrderScrmServiceImpl.java`(applyPromotionFields skip 判断)
+  - `fs-service/.../hisStore/support/FsStorePromotionTierCalculator.java`(档位匹配防御性排序)
+  - `fs-service/.../resources/mapper/hisStore/FsStorePromotionUsageMapper.xml`(限购统计含待支付)
+
+## ADDED Requirements
+
+### Requirement: 支付回调验签正确性
+系统 SHALL 正确实现易宝支付回调的验签逻辑,验签通过返回 true,验签失败返回 false。
+
+#### Scenario: 合法签名回调
+- **WHEN** 易宝发送签名正确的支付回调
+- **THEN** 验签方法返回 true,回调正常处理
+
+#### Scenario: 非法签名回调
+- **WHEN** 攻击者发送签名错误的回调
+- **THEN** 验签方法返回 false,回调被拒绝并记录日志
+
+### Requirement: 支付回调参数绑定
+系统 SHALL 使用单一 `@RequestBody` 接收回调请求体,确保参数正确绑定。
+
+#### Scenario: 易宝回调请求
+- **WHEN** 易宝发送支付回调 POST 请求
+- **THEN** 回调方法能正确解析请求体中的订单信息,不发生 NPE
+
+### Requirement: 满减限购次数生效
+系统 SHALL 正确执行满减活动的限购次数校验,待支付订单也计入已参与次数。
+
+#### Scenario: 用户达到限购次数
+- **WHEN** 用户已参与满减活动次数(含待支付)达到 `limitPerUser` 上限
+- **THEN** 该活动对该用户显示为"已超过参与次数限制",不可再次使用
+
+### Requirement: 满减档位匹配准确性
+系统 SHALL 按 `thresholdAmount` 升序排列满减档位后再匹配,确保返回正确的优惠档位。
+
+#### Scenario: 档位配置顺序混乱
+- **WHEN** 管理员配置档位时未按金额升序录入
+- **THEN** 系统自动排序后匹配,返回符合门槛的最高档位
+
+## MODIFIED Requirements
+
+### Requirement: 支付回调返回值规范
+所有易宝支付回调方法 SHALL 统一返回值:处理成功返回 `SUCCESS`,处理失败返回 `FAIL`,不再大小写混用。
+
+#### Scenario: 回调处理成功
+- **WHEN** 支付回调处理成功
+- **THEN** 返回字符串 `SUCCESS`(全大写)
+
+#### Scenario: 回调处理失败
+- **WHEN** 支付回调处理失败(验签失败、业务异常)
+- **THEN** 返回字符串 `FAIL`(全大写),让易宝平台重试
+
+### Requirement: 制单改价满减应用
+制单改价场景下,`applyPromotionFields` SHALL 跳过满减计算,与 `computedOrder` 的 `skipPromotion` 逻辑保持一致。
+
+#### Scenario: 制单改价创建订单
+- **WHEN** 制单改价场景创建订单时调用 `applyPromotionFields`
+- **THEN** 不重新计算满减优惠,`promotionDiscountAmount` 保持试算时的值或为 0
+
+## REMOVED Requirements
+
+### Requirement: TestYb.java 中的硬编码敏感信息
+**Reason**: 生产源码目录中硬编码真实商户号、Token、身份证号,存在泄露风险
+**Migration**: 将测试代码移至 `src/test/java` 或直接删除,敏感参数从配置文件或环境变量读取

+ 48 - 0
.trae/specs/fix-ybpay-and-promotion-quality/tasks.md

@@ -0,0 +1,48 @@
+# Tasks
+
+- [x] Task 1: 修复 PayUtil 验签逻辑反转
+  - [x] SubTask 1.1: 修复 `PayUtil.java`(ybPay 版)`isSgin` 方法,改为 `return sg.equals(sign);`
+  - [x] SubTask 1.2: 修复 `PayUtil.java`(pay 版)`isSgin` 方法,同样修正逻辑反转
+  - [x] SubTask 1.3: 修正两个文件中方法名拼写 `isSgin` → `isSign`,`PayScrmServiceImpl.java` 3 处调用已同步更新
+
+- [x] Task 2: 修复 PayScrmController.ybPayNotify 双 @RequestBody 与密钥硬编码
+  - [x] SubTask 2.1: 移除第二个 `@RequestBody OrderCallback o`,改为从 `params` 获取 `lowOrderId`
+  - [x] SubTask 2.2: 验签密钥改为从 DB 配置读取 `PayUtil.sign(params, fsPayConfig.getYbKey())`
+  - [x] SubTask 2.3: `params.get("sign")` 增加 null 校验,返回 `FAIL`
+  - [x] SubTask 2.4: `lowOrderId.split("-")` 增加数组长度校验
+  - [x] SubTask 2.5: 返回值统一为 `SUCCESS`/`FAIL`
+
+- [x] Task 3: 修复 YbPayController.payNotify 双 @RequestBody 与异常处理
+  - [x] SubTask 3.1: 移除第二个 `@RequestBody`
+  - [x] SubTask 3.2: `params.get("sign")` 增加 null 校验
+  - [x] SubTask 3.3: 验签失败不再抛异常,改为返回 `FAIL`
+  - [x] SubTask 3.4: `System.out.println` 替换为 Logger
+  - [x] SubTask 3.5: `SpringUtils.getBean` 改为 `@Autowired` 注入 `ISysConfigService`
+  - [x] SubTask 3.6: switch 补充 default 分支
+
+- [x] Task 4: 清理 TestYb.java 硬编码敏感信息
+  - [x] SubTask 4.1: 已删除 `TestYb.java` 文件,确认无其他文件引用
+
+- [x] Task 5: 修复满减限购失效
+  - [x] SubTask 5.1: 取消 `resolveDisabledReason` 中限购判断的注释
+  - [x] SubTask 5.2: `countEffectiveByActivityAndUser` SQL 改为 `usage_status IN (0, 1)`
+
+- [x] Task 6: 修复制单改价满减重复应用
+  - [x] SubTask 6.1: `applyPromotionFields` 增加 `skipPromotion` 参数,两处调用点(createOrder、createOrderMultiStore)均传入
+
+- [x] Task 7: 修复满减档位未排序
+  - [x] SubTask 7.1: `saveTiers` 保存前按 `thresholdAmount` 升序排序
+  - [x] SubTask 7.2: `matchAmountTier` 增加防御性排序(使用 stream 生成新 list)
+
+- [x] Task 8: 满减 usage 记录幂等保护
+  - [x] SubTask 8.1: `recordPendingUsage` 插入前查询 (order_id, activity_id) 是否存在
+  - [x] SubTask 8.2: 新增 `selectByOrderIdAndActivityId` Mapper 方法和 XML SQL
+
+- [x] Task 9: 修复 enableActivity 并发竞态
+  - [x] SubTask 9.1: 新增 `enableActivityIfNoConflict` 条件 UPDATE 方法,通过受影响行数判断冲突
+
+# Task Dependencies
+- Task 1(PayUtil 验签修复)与 Task 2、Task 3(回调修复)可并行,但 Task 2、3 依赖 Task 1 完成后验证验签链路
+- Task 4(TestYb 清理)独立,可并行
+- Task 5、6、7、8、9(满减修复)相互独立,可并行
+- Task 2 与 Task 3 修改不同文件,可并行

+ 35 - 0
.trae/specs/refactor-integral-controllers-quality/checklist.md

@@ -0,0 +1,35 @@
+# Checklist
+
+## 安全性
+- [x] `UserSignScrmController.getIntegral` 行 71 自赋值 Bug 已修复,改为从 `getUserId()` 获取
+- [x] `IntegralScrmController.getIntegralOrderById` 已增加订单归属校验,非本人订单返回错误
+- [x] `StoreOrderScrmController.create` 已增加 `@RepeatSubmit` 注解
+- [x] `StoreOrderScrmController.createMultiStore` 已增加 `@RepeatSubmit` 注解
+- [x] `PayScrmController.hfPayNotify` 已增加分布式锁,锁 key 基于订单号
+- [x] `PayScrmController.ybPayNotify` 已增加分布式锁,锁 key 基于订单号
+
+## 健壮性
+- [x] `UserSignScrmController.getUserSign` 中 `user.getIntegral()` 已做 null 校验
+- [x] `IntegralScrmController.getUserSign` 中 `user.getIntegral()` 的 NPE 风险已随重复方法删除消除
+- [x] `PayScrmController` 所有 switch 语句已补充 default 分支并记录日志
+- [x] `CourseScrmController.getIntegral` 参数已加 `@Validated`
+- [x] `CourseScrmController.getIntegralByH5Video` 参数已加 `@Validated`
+
+## 代码规范
+- [x] `IntegralScrmController` 中重复的 `getUserSign`、`sign` 方法已删除
+- [x] 删除重复方法后,未使用的依赖注入已清理(3 个 Service + 2 个 import)
+- [x] `HuifuPayScrmController` 被注释的 switch 死代码已删除
+- [x] `HuifuPayScrmController` 未使用的依赖注入已清理(7 个 Service + 对应 import)
+- [x] `HuifuPayScrmController` logger 已改为 `@Slf4j`
+- [x] `"store.sign"` 配置 key 已抽取为常量 `CONFIG_KEY_STORE_SIGN`
+- [x] `PayScrmController` 支付来源魔法值 `1` 已抽取为常量 `PAY_SOURCE_ONLINE`(4 处全部替换)
+- [x] `IntegralScrmController.getIntegralGoodsList`、`getIntegralGoodsById` 的 `@Login` 策略已确认(公开接口不加)
+
+## 验证
+- [x] `UserSignScrmController` 自赋值 Bug 已修复,NPE 已防护,常量已定义
+- [x] `IntegralScrmController` IDOR 已修复,重复方法已删除,无用依赖已清理
+- [x] `StoreOrderScrmController` 两个下单方法均已加 `@RepeatSubmit`
+- [x] `PayScrmController` 两个单门店回调已加分布式锁,4 处 switch 均有 default,4 处魔法值已常量化
+- [x] `HuifuPayScrmController` 死代码已清理,logger 已规范化,异常处理已增加
+- [x] `CourseScrmController` 两个积分方法参数已加 `@Validated`
+- [ ] 改动后项目能正常编译(mvn compile)— 需在 Java 项目环境验证

+ 76 - 0
.trae/specs/refactor-integral-controllers-quality/spec.md

@@ -0,0 +1,76 @@
+# 小程序端积分相关 Controller 代码质量与规范治理 Spec
+
+## Why
+`fs-user-app/.../controller/store` 目录下涉及积分的 Controller 存在多处代码规范与质量问题:防重复提交缺失、支付回调无分布式锁、IDOR 越权漏洞、NPE 风险、死代码、自赋值 Bug、代码重复等。这些问题直接影响积分扣减/发放的资金安全与数据一致性,违反项目内存中"代码严谨规范、高可用、计算准确"的工程约定。
+
+## What Changes
+- 为下单接口 `StoreOrderScrmController.create/createMultiStore` 增加 `@RepeatSubmit` 防重复提交注解
+- 为单门店支付回调 `PayScrmController.hfPayNotify/ybPayNotify` 增加分布式锁,避免重复回调导致积分重复发放
+- 修复 `IntegralScrmController.getIntegralOrderById` 的 IDOR 越权漏洞,校验订单归属
+- 修复 `UserSignScrmController.getIntegral` 行 71 的自赋值 Bug(`param.setUserId(param.getUserId())` 改为从登录态取值)
+- 清理 `HuifuPayScrmController` 被注释的死代码及未使用的依赖注入
+- 修复 `user.getIntegral().intValue()` 的 NPE 风险,增加 null 校验
+- 删除 `IntegralScrmController` 中与 `UserSignScrmController` 重复的 `getUserSign`、`sign` 方法
+- 为支付回调 switch 语句补充 default 分支并记录日志
+- 抽取硬编码的配置 key(`store.sign`)与魔法值(支付来源 `1`)为常量
+- 统一 `@Login` 注解使用策略
+- **BREAKING**:删除 `IntegralScrmController.getUserSign` 和 `IntegralScrmController.sign` 两个重复端点(`/store/app/integral/getUserSign`、`/store/app/integral/sign`),前端需改用 `/store/app/sign/*` 路径
+
+## Impact
+- Affected specs: 积分发放规则(points.grantRule)、多门店积分按比例分配、签到积分、积分商品兑换
+- Affected code:
+  - `fs-user-app/.../store/StoreOrderScrmController.java`(下单防重)
+  - `fs-user-app/.../store/PayScrmController.java`(回调加锁、switch default)
+  - `fs-user-app/.../store/IntegralScrmController.java`(IDOR 修复、删除重复方法、NPE 修复)
+  - `fs-user-app/.../store/UserSignScrmController.java`(自赋值 Bug 修复、NPE 修复、常量化)
+  - `fs-user-app/.../store/HuifuPayScrmController.java`(死代码清理)
+  - `fs-user-app/.../store/CourseScrmController.java`(方法命名、参数校验)
+
+## ADDED Requirements
+
+### Requirement: 下单接口防重复提交
+系统 SHALL 为所有涉及积分扣减的下单接口提供防重复提交保护。
+
+#### Scenario: 用户快速双击提交订单
+- **WHEN** 用户在 3 秒内重复调用 `/store/app/order/create` 或 `/store/app/order/createMultiStore`
+- **THEN** 第二次及后续请求被拦截,返回"请勿重复提交"提示,积分不被重复扣减
+
+### Requirement: 支付回调分布式锁保护
+系统 SHALL 为所有支付回调接口提供分布式锁保护,防止第三方支付平台重复回调导致积分重复发放。
+
+#### Scenario: 第三方支付平台重复回调
+- **WHEN** 汇付/易宝在短时间内对同一笔订单发送多次支付成功回调
+- **THEN** 仅第一次回调处理成功并发放积分,后续回调因获取锁失败直接返回 SUCCESS 且不重复发放积分
+
+### Requirement: 积分订单越权访问防护
+系统 SHALL 校验积分订单查询接口的订单归属,防止用户查看他人订单。
+
+#### Scenario: 用户枚举他人订单 ID 查询
+- **WHEN** 用户 A 调用 `/store/app/integral/getIntegralOrderById?orderId=用户B的订单ID`
+- **THEN** 系统返回"无权访问该订单",不返回订单数据
+
+## MODIFIED Requirements
+
+### Requirement: 签到积分发放
+签到接口 `UserSignScrmController.sign` SHALL 正确从登录态获取 userId,并保证积分余额字段读取时无 NPE 风险。
+
+#### Scenario: 用户查询积分记录
+- **WHEN** 用户调用 `/store/app/sign/getIntegral`
+- **THEN** 返回当前登录用户的积分记录(修复前因自赋值 Bug 返回错误数据)
+
+### Requirement: 积分相关配置 key 常量化
+所有积分相关的配置 key(如 `store.sign`)与魔法值(如支付来源 `1`)SHALL 抽取为常量,禁止在方法体内硬编码。
+
+#### Scenario: 配置 key 统一管理
+- **WHEN** 开发者需要修改签到配置 key
+- **THEN** 在常量类中修改一处即可,无需在多个方法体内查找替换
+
+## REMOVED Requirements
+
+### Requirement: IntegralScrmController 中的签到接口
+**Reason**: `getUserSign` 和 `sign` 方法在 `IntegralScrmController` 和 `UserSignScrmController` 中完全重复,违反 DRY 原则,维护困难
+**Migration**: 前端需将 `/store/app/integral/getUserSign` 和 `/store/app/integral/sign` 的调用迁移到 `/store/app/sign/getUserSign` 和 `/store/app/sign/sign`
+
+### Requirement: HuifuPayScrmController 被注释的支付回调逻辑
+**Reason**: 整个 switch 语句被注释,方法体仅剩 `return "ok"`,注入的 7 个 Service 全部未使用,属于死代码
+**Migration**: 真正生效的支付回调在 `PayScrmController` 中,无需迁移;直接清理被注释代码与未使用依赖

+ 42 - 0
.trae/specs/refactor-integral-controllers-quality/tasks.md

@@ -0,0 +1,42 @@
+# Tasks
+
+- [x] Task 1: 修复 UserSignScrmController 自赋值 Bug 与 NPE 风险
+  - [x] SubTask 1.1: 修复 `getIntegral` 方法行 71 `param.setUserId(param.getUserId())` 自赋值 Bug,改为 `param.setUserId(Long.parseLong(getUserId()))`
+  - [x] SubTask 1.2: 修复 `getUserSign` 方法 `user.getIntegral().intValue()` 的 NPE 风险,增加 null 校验(integral 为 null 时返回 0)
+
+- [x] Task 2: 修复 IntegralScrmController IDOR 越权漏洞与 NPE 风险
+  - [x] SubTask 2.1: 修复 `getIntegralOrderById` 方法,增加订单归属校验(校验订单 userId 是否等于当前登录 userId,不匹配返回错误)
+  - [x] SubTask 2.2: 修复 `getUserSign` 方法 `user.getIntegral().intValue()` 的 NPE 风险(随 Task 3 删除重复方法一并消除)
+
+- [x] Task 3: 删除 IntegralScrmController 中与 UserSignScrmController 重复的方法
+  - [x] SubTask 3.1: 删除 `IntegralScrmController.getUserSign` 方法(行 107-118)
+  - [x] SubTask 3.2: 删除 `IntegralScrmController.sign` 方法(行 119-126)
+  - [x] SubTask 3.3: 清理因删除方法而不再使用的依赖注入(`IFsUserScrmService userService`、`IFsUserSignScrmService userSignService`、`ISysConfigService configService` 已删除,同时清理无用 import)
+
+- [x] Task 4: 为 StoreOrderScrmController 下单接口增加防重复提交
+  - [x] SubTask 4.1: 为 `create` 方法(行 376)增加 `@RepeatSubmit` 注解
+  - [x] SubTask 4.2: 为 `createMultiStore` 方法(行 390)增加 `@RepeatSubmit` 注解
+
+- [x] Task 5: 为 PayScrmController 单门店支付回调增加分布式锁
+  - [x] SubTask 5.1: 为 `hfPayNotify` 方法(行 101-135)增加 Redisson 分布式锁,锁 key 基于订单号
+  - [x] SubTask 5.2: 为 `ybPayNotify` 方法(行 137-159)增加 Redisson 分布式锁,锁 key 基于订单号
+  - [x] SubTask 5.3: 为 `hfPayNotify`、`ybPayNotify`、`multiStoreHfPayNotify` 的 switch 语句补充 default 分支并记录 warn 日志
+
+- [x] Task 6: 清理 HuifuPayScrmController 死代码
+  - [x] SubTask 6.1: 删除 `asyncMessage` 方法中被注释的 switch 语句(行 46-68)
+  - [x] SubTask 6.2: 删除因 switch 被注释而未使用的依赖注入(7 个 Service 全部删除)
+  - [x] SubTask 6.3: 修复 logger 声明(使用 `@Slf4j` 注解,删除非标准 logger 字段)
+
+- [x] Task 7: 抽取硬编码常量
+  - [x] SubTask 7.1: 在 `UserSignScrmController` 中将 `"store.sign"` 抽取为常量 `CONFIG_KEY_STORE_SIGN`(IntegralScrmController 因 configService 已删除无需处理)
+  - [x] SubTask 7.2: 在 `PayScrmController` 中将支付来源魔法值 `1` 抽取为常量 `PAY_SOURCE_ONLINE`(已替换全部 4 处调用)
+
+- [x] Task 8: 统一 @Login 注解与参数校验
+  - [x] SubTask 8.1: `IntegralScrmController.getIntegralGoodsList`、`getIntegralGoodsById` 保持公开不加 `@Login`(商品查询为公开接口,符合业务惯例)
+  - [x] SubTask 8.2: 为 `CourseScrmController.getIntegral` 和 `getIntegralByH5Video` 的 param 参数补充 `@Validated` 注解
+
+# Task Dependencies
+- Task 3 依赖 Task 1、Task 2 完成(先修复保留的方法,再删除重复方法)
+- Task 7 可与 Task 1-6 并行(常量抽取互不影响)
+- Task 8 可与 Task 1-7 并行
+- Task 5 的 SubTask 5.3 依赖 5.1、5.2(同一文件的 switch 修改建议顺序执行避免冲突)

+ 8 - 0
src/api/his/userIntegralLogs.js

@@ -50,4 +50,12 @@ export function exportUserIntegralLogs(query) {
     method: 'get',
     params: query
   })
+}
+
+// 按订单 ID 查询积分明细列表
+export function getIntegralLogsByOrderId(orderId) {
+  return request({
+    url: '/his/userIntegralLogs/getByOrderId/' + orderId,
+    method: 'get'
+  })
 }

+ 26 - 1
src/views/his/userIntegralLogs/index.vue

@@ -158,10 +158,35 @@ export default {
   created() {
     this.getList();
     this.getDicts("sys_integral_log_type").then(response => {
-      this.intefralLogTypeOptions = response.data;
+      this.intefralLogTypeOptions = this.mergeLogTypeOptions(response.data || []);
     });
   },
   methods: {
+    /** 合并字典与本地兜底类型,确保所有已知 logType 都有对应名称 */
+    mergeLogTypeOptions(dictOptions) {
+      // 本地完整映射(与后端 FsUserIntegralLogTypeEnum 对齐),字典缺失时兜底
+      const localMap = {
+        1: '签到获得积分', 2: '消费获得积分', 3: '分享获得积分', 4: '退款扣除积分',
+        5: '购买积分商品', 6: '平台取消售后', 7: '积分过期', 8: '螳螂同步积分',
+        9: '购买课程小节扣除', 10: '观看课程获取积分', 11: '新用户完善就诊人获得积分',
+        12: '新用户填写收货地址获取积分', 13: '浏览商品获得积分', 14: '浏览视频获得积分',
+        15: '首次下单疗法获得积分', 16: '点播看课获得积分', 17: '点播答题获得积分',
+        18: '邀请新用户获取积分', 19: '填写邀请码获取积分', 20: '首次完成手机号绑定注册',
+        21: '首次完成专家咨询', 22: '首次完成积分商城下单', 23: '管理员添加',
+        24: '付费课程订阅', 25: '下单使用积分抵扣', 26: '取消订单退回积分', 27: '退款订单退回积分'
+      };
+      // 字典已有项优先,收集已有 value
+      const result = [...dictOptions];
+      const existValues = new Set(dictOptions.map(o => Number(o.dictValue)));
+      // 补充字典缺失的类型
+      Object.keys(localMap).forEach(key => {
+        const val = Number(key);
+        if (!existValues.has(val)) {
+          result.push({ dictValue: String(val), dictLabel: localMap[val] });
+        }
+      });
+      return result;
+    },
     /** 查询积分记录列表 */
     getList() {
       this.loading = true;

+ 79 - 0
src/views/hisStore/components/productAfterSalesOrder.vue

@@ -398,6 +398,35 @@
             </template>
           </el-table-column>
         </el-table>
+
+        <div style="margin-top: 20px">
+          <span class="font-small">积分明细</span>
+        </div>
+        <el-table
+          border
+          :data="integralLogs"
+          size="small"
+          style="width: 100%;margin-top: 20px"
+          empty-text="该订单暂无积分变动记录">
+          <el-table-column label="类型" align="center" min-width="160">
+            <template slot-scope="scope">
+              <el-tag size="mini" :type="scope.row.integral >= 0 ? 'success' : 'danger'">
+                {{ formatLogType(scope.row.logType) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="积分变动" align="center" width="120">
+            <template slot-scope="scope">
+              <span :style="{ color: scope.row.integral >= 0 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
+                {{ scope.row.integral >= 0 ? '+' + scope.row.integral : scope.row.integral }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="变动后余额" align="center" prop="balance" width="120" />
+          <el-table-column label="业务订单ID" align="center" prop="businessId" width="140" />
+          <el-table-column label="时间" align="center" prop="createTime" width="160" />
+          <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
+        </el-table>
       </el-card>
 
     </div>
@@ -473,6 +502,7 @@
 <script>
 import {getStoreAfterSales,cancel,refund,audit1,audit2,updateStoreAfterSales,getExpress} from "@/api/hisStore/storeAfterSales";
 import { getOrderPromotionDetail } from "@/api/hisStore/storePromotion";
+import { getIntegralLogsByOrderId } from "@/api/his/userIntegralLogs";
 
 import productOrder from "./productOrder";
 export default {
@@ -520,6 +550,9 @@ export default {
       store: null,
       logs:[],
       orderPromotion: null,
+      integralLogs: [],
+      // 积分日志类型字典(字典 + 本地兜底,与 FsUserIntegralLogTypeEnum 对齐)
+      integralLogTypeOptions: [],
       form:{
         refundAmount:0,
       },
@@ -544,6 +577,10 @@ export default {
      this.getDicts("store_after_sales_service_type").then((response) => {
         this.serviceTypeOptions = response.data;
     });
+    // 加载积分日志类型字典(合并本地兜底,确保所有已知 logType 都有中文名称)
+    this.getDicts("sys_integral_log_type").then((response) => {
+      this.integralLogTypeOptions = this.mergeLogTypeOptions(response.data || []);
+    });
   },
   methods: {
     formatPromotionDiscount(amount) {
@@ -668,6 +705,9 @@ export default {
             this.store=response.store;
             if (this.order && this.order.id) {
               this.loadOrderPromotion(this.order.id);
+              this.loadIntegralLogs(this.order.id);
+            } else {
+              this.integralLogs = [];
             }
         });
      },
@@ -677,6 +717,45 @@ export default {
        }).catch(() => {
          this.orderPromotion = null;
        });
+     },
+     // 加载积分明细
+     loadIntegralLogs(orderId) {
+       getIntegralLogsByOrderId(orderId).then(response => {
+         this.integralLogs = response.data || [];
+       }).catch(() => {
+         this.integralLogs = [];
+       });
+     },
+     /** 合并字典与本地兜底类型,确保所有已知 logType 都有对应名称 */
+     mergeLogTypeOptions(dictOptions) {
+       // 本地完整映射(与后端 FsUserIntegralLogTypeEnum 对齐),字典缺失时兜底
+       const localMap = {
+         1: '签到获得积分', 2: '消费获得积分', 3: '分享获得积分', 4: '退款扣除积分',
+         5: '购买积分商品', 6: '平台取消售后', 7: '积分过期', 8: '螳螂同步积分',
+         9: '购买课程小节扣除', 10: '观看课程获取积分', 11: '新用户完善就诊人获得积分',
+         12: '新用户填写收货地址获取积分', 13: '浏览商品获得积分', 14: '浏览视频获得积分',
+         15: '首次下单疗法获得积分', 16: '点播看课获得积分', 17: '点播答题获得积分',
+         18: '邀请新用户获取积分', 19: '填写邀请码获取积分', 20: '首次完成手机号绑定注册',
+         21: '首次完成专家咨询', 22: '首次完成积分商城下单', 23: '管理员添加',
+         24: '付费课程订阅', 25: '下单使用积分抵扣', 26: '取消订单退回积分', 27: '退款订单退回积分'
+       };
+       // 字典已有项优先,收集已有 value
+       const result = [...dictOptions];
+       const existValues = new Set(dictOptions.map(o => Number(o.dictValue)));
+       // 补充字典缺失的类型
+       Object.keys(localMap).forEach(key => {
+         const val = Number(key);
+         if (!existValues.has(val)) {
+           result.push({ dictValue: String(val), dictLabel: localMap[val] });
+         }
+       });
+       return result;
+     },
+     /** 将 logType 数值转换为中文名称 */
+     formatLogType(logType) {
+       if (logType == null) return '-';
+       const item = this.integralLogTypeOptions.find(o => Number(o.dictValue) === Number(logType));
+       return item ? item.dictLabel : ('类型' + logType);
      }
   }
 };

+ 77 - 0
src/views/hisStore/components/productOrder.vue

@@ -553,6 +553,35 @@
           <el-table-column label="备注" align="center" prop="remark" />
       </el-table>
 
+      <div style="margin-top: 20px">
+        <span class="font-small">积分明细</span>
+      </div>
+      <el-table
+        border
+        :data="integralLogs"
+        size="small"
+        style="width: 100%;margin-top: 20px"
+        empty-text="该订单暂无积分变动记录">
+        <el-table-column label="类型" align="center" min-width="160">
+          <template slot-scope="scope">
+            <el-tag size="mini" :type="scope.row.integral >= 0 ? 'success' : 'danger'">
+              {{ formatLogType(scope.row.logType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="积分变动" align="center" width="120">
+          <template slot-scope="scope">
+            <span :style="{ color: scope.row.integral >= 0 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
+              {{ scope.row.integral >= 0 ? '+' + scope.row.integral : scope.row.integral }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="变动后余额" align="center" prop="balance" width="120" />
+        <el-table-column label="业务订单ID" align="center" prop="businessId" width="140" />
+        <el-table-column label="时间" align="center" prop="createTime" width="160" />
+        <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
+      </el-table>
+
         <div style="margin-top: 20px">
           <span class="font-small">审批信息</span>
         </div>
@@ -760,6 +789,7 @@ import {auditPayRemain,addTuiMoney,syncExpress,updateExpress,getEroOrder,refundO
   getStoreOrderPhone,getCommentInfo,updateCommentInfo} from "@/api/hisStore/storeOrder";
 import { getOrderPromotionDetail } from "@/api/hisStore/storePromotion";
 import { getTcmScheduleList } from "@/api/company/schedule";
+import { getIntegralLogsByOrderId } from "@/api/his/userIntegralLogs";
 export default {
   name: "order",
   data() {
@@ -837,6 +867,9 @@ export default {
       auditLogs: [],
       storeScrm: null,
       orderPromotion: null,
+      integralLogs: [],
+      // 积分日志类型字典(字典 + 本地兜底,与 FsUserIntegralLogTypeEnum 对齐)
+      integralLogTypeOptions: [],
     };
   },
   created() {
@@ -862,6 +895,10 @@ export default {
     getTcmScheduleList().then(response => {
       this.scheduleOptions = response.data;
     });
+    // 加载积分日志类型字典(合并本地兜底,确保所有已知 logType 都有中文名称)
+    this.getDicts("sys_integral_log_type").then((response) => {
+      this.integralLogTypeOptions = this.mergeLogTypeOptions(response.data || []);
+    });
   },
   methods: {
     formatPromotionDiscount(amount) {
@@ -1130,8 +1167,48 @@ export default {
             this.tuiMoneyLogs=response.tuiMoneyLogs;
             this.auditLogs = response.auditLogs;
             this.loadOrderPromotion(orderId);
+            this.loadIntegralLogs(orderId);
         });
      },
+     // 加载积分明细
+     loadIntegralLogs(orderId) {
+       getIntegralLogsByOrderId(orderId).then(response => {
+         this.integralLogs = response.data || [];
+       }).catch(() => {
+         this.integralLogs = [];
+       });
+     },
+     /** 合并字典与本地兜底类型,确保所有已知 logType 都有对应名称 */
+     mergeLogTypeOptions(dictOptions) {
+       // 本地完整映射(与后端 FsUserIntegralLogTypeEnum 对齐),字典缺失时兜底
+       const localMap = {
+         1: '签到获得积分', 2: '消费获得积分', 3: '分享获得积分', 4: '退款扣除积分',
+         5: '购买积分商品', 6: '平台取消售后', 7: '积分过期', 8: '螳螂同步积分',
+         9: '购买课程小节扣除', 10: '观看课程获取积分', 11: '新用户完善就诊人获得积分',
+         12: '新用户填写收货地址获取积分', 13: '浏览商品获得积分', 14: '浏览视频获得积分',
+         15: '首次下单疗法获得积分', 16: '点播看课获得积分', 17: '点播答题获得积分',
+         18: '邀请新用户获取积分', 19: '填写邀请码获取积分', 20: '首次完成手机号绑定注册',
+         21: '首次完成专家咨询', 22: '首次完成积分商城下单', 23: '管理员添加',
+         24: '付费课程订阅', 25: '下单使用积分抵扣', 26: '取消订单退回积分', 27: '退款订单退回积分'
+       };
+       // 字典已有项优先,收集已有 value
+       const result = [...dictOptions];
+       const existValues = new Set(dictOptions.map(o => Number(o.dictValue)));
+       // 补充字典缺失的类型
+       Object.keys(localMap).forEach(key => {
+         const val = Number(key);
+         if (!existValues.has(val)) {
+           result.push({ dictValue: String(val), dictLabel: localMap[val] });
+         }
+       });
+       return result;
+     },
+     /** 将 logType 数值转换为中文名称 */
+     formatLogType(logType) {
+       if (logType == null) return '-';
+       const item = this.integralLogTypeOptions.find(o => Number(o.dictValue) === Number(logType));
+       return item ? item.dictLabel : ('类型' + logType);
+     },
      loadOrderPromotion(orderId) {
        getOrderPromotionDetail(orderId).then(response => {
          this.orderPromotion = response.data || null;

+ 1 - 1
src/views/hisStore/storeOrder/index.vue

@@ -544,7 +544,7 @@
     </el-table>
 
     <pagination
-      v-show="total>0"
+      v-show="total>0"a
       :total="total"
       :page.sync="queryParams.pageNum"
       :limit.sync="queryParams.pageSize"