--- name: 修复中等与轻微问题6项 overview: 按二次审查报告修复 6 个问题:M1(退款推送 RefundStatus 空串误回收)、M2(发货校验失败返回成功应答吞资损线索)、M5(补偿任务 Failed 被回调重置)、M14(衣物失败回退与删除并发漏退)、M10(refresh-token access_token 文档矛盾)、M13(Outfit.Public 并发语义冲突)。限流(M11)与上传防护(M12)本次不修复。 todos: - id: fix-xpay-notify content: 修复虚拟支付推送链路:新增 XPayRefundStatusSuccess 常量,handleRefundNotify 改白名单判断,handleGoodsDeliver 五个校验失败分支返回 70009 触发微信重试 status: completed - id: fix-benefit-reset content: 修复补偿任务重置:BenefitGrantTaskReset 保留 retry_count,失败任务增加 5 分钟冷却期后才允许回调重新触发 status: completed - id: fix-clothes-refund-tx content: 修复衣物回退漏退:markProcessFailed 重写为行锁单事务,删除 refundIfExists 与 ClothesMarkProcessFailedIfNotFailed status: completed - id: fix-outfit-public-lock content: 修复穿搭公开竞态:redismodel 新增 KeyLockOutfitPublic,OutfitService.Public 全流程加 Redis 锁串行化 status: completed - id: sync-docs content: 同步文档:README 修正 refresh-token access_token 为必填,审查报告 M1/M2/M5/M10/M13/M14 标注已修复 status: completed dependencies: - fix-xpay-notify - fix-benefit-reset - fix-clothes-refund-tx - fix-outfit-public-lock - id: verify content: 验证 go build 与 go test 通过,read_lints 无新增告警 status: completed dependencies: - sync-docs --- ## 用户需求 根据二次代码审查报告,修复用户确认的 6 个问题(最新调整:限流缺失 M11、上传防护 M12 暂不修复): 1. **M1 退款推送误回收**:`RefundStatus` 字段为空串或未知值时被误判为「退款成功」并回收权益,修复为仅当明确等于 `SUCCESS` 才回退 2. **M2 发货校验失败静默**:发货推送本地校验失败(订单不存在/渠道不符/道具不符/金额不符/环境不符)返回成功应答,微信不重试且资损线索被吞掉,修复为返回失败应答触发重试 3. **M5 补偿任务重试上限失效**:失败任务被支付回调重置(retry_count 清零),绕过重试上限与人工介入机制 4. **M14 衣物额度漏退窗口**:异步处理失败回退与用户删除并发时,可能两个「应回退」事件叠加却零回退,用户损失额度 5. **M10 refresh-token 文档矛盾**:README 标注 access_token 为可选,但代码要求必填,统一为必填 6. **M13 穿搭公开并发语义冲突**:并发公开时,一个请求回退公开态后,另一请求已误报成功 ## 约束 - 保持现有代码风格(slog 结构化日志、errcode 错误码、Redis fail-open 降级) - 修复后 `go build ./...` 与 `go test ./pkg/wechat/` 必须通过,read_lints 无新增告警 - 不修改普通微信支付 V3 回调链路;M3/M4/M6-M9/M11/M12/M15 及未点名问题不在本次范围 ## 技术方案 ### 1. M1 + M2:虚拟支付推送链路修复 **M1 退款推送白名单** - `pkg/wechat/virtualpay.go`:新增常量 `XPayRefundStatusSuccess = "SUCCESS"` - `internal/service/virtualpay.go` 的 `handleRefundNotify`:改为 `if refund.RefundStatus != wechat.XPayRefundStatusSuccess { slog.WarnContext(...); return nil }`,仅成功状态走 `settleRefundSuccess`;订单不存在分支保持 `return nil` **M2 发货校验失败应答** - `handleGoodsDeliver` 中以下五个分支由 `return nil` 改为 `return errcode.OrderRealityCheckFailed`(70009 已存在),使 `XPayNotify` 回 `ErrCode:1` 让微信重试: - 订单不存在 - `pay_channel != PayChannelVirtual` - 道具 ID 不匹配 - 实付金额不匹配 - 环境不匹配 - 锁竞争失败分支保持 `return nil`(并发回调交由持锁者处理,防重试风暴) ### 2. M5:补偿任务重置保护 - `internal/mysqldao/benefit_grant_task.go` 的 `BenefitGrantTaskReset`:删除 `retry_count: 0`,保留原重试计数 - `internal/service/order.go` 的 `grantBenefitIdempotent` Failed 分支:新增冷却常量 `benefitRetryCooldownSeconds = 300`;当 `nowUnix()-existing.UpdatedAt < 300` 时仅打告警跳过,冷却期外才 Reset + executeGrant,恢复重试上限与人工介入机制 ### 3. M14:衣物处理失败回退事务化 - `internal/service/clothes.go` 重写 `markProcessFailed` 为单事务(复用 `DeleteByUUID` 的 FOR UPDATE + 回退模式): 1. `tx.Clauses(clause.Locking{Strength: "UPDATE"})` 按 `clothes_uuid` 查衣物;不存在/已软删(RecordNotFound)或已是失败态则提交并跳过 2. 置 `image_process_status=5` + `fail_reason` 3. 同事务回退额度:条件更新 `used - 1`(used>=1),回读真实值,写回退流水(Reason="衣物处理失败回退",OrderNo=流水 UUID 兜底) - 删除 `refundIfExists` 方法与 `internal/mysqldao/clothes.go` 的 `ClothesMarkProcessFailedIfNotFailed`(已确认仅此一处调用) - `DeleteByUUID` 保持不变:行锁读到失败态即跳过回退,与失败事务通过行锁互斥,两路径恰好回退一次 ### 4. M13:穿搭公开加 Redis 锁 - `internal/redismodel/lock.go`:新增 `KeyLockOutfitPublic = RedisPrefix + "lock:outfit:public:%s"`(outfit_uuid) - `internal/service/outfit.go` 的 `Public`:进入后先 `redisdao.TryLock(fmtLockKey(KeyLockOutfitPublic, request.OutfitUUID), 10s)`,失败返回 `errcode.TooManyRequests`;成功 `defer` 解锁;锁内保留现有「条件公开 → 扣额 → 失败回退」流程,串行化后并发请求语义正确 ### 5. M10 + 文档同步 - `README.md` 686 行:access_token 由「否(可选,向前兼容)」改为「是(必填)」,说明缺少返回 10000 - `docs/代码审查报告-二次check.md`:M1/M2/M5/M10/M13/M14 标题后标注「已修复」及一行修复说明;M11/M12 保持未修复状态 ### 验证 - `go build ./...` 全量编译通过 - `go test ./pkg/wechat/` 通过 - `read_lints` 检查修改文件无新增告警