5.9 KiB
5.9 KiB
name, overview, todos
| name | overview | todos | |||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 修复中等与轻微问题6项 | 按二次审查报告修复 6 个问题:M1(退款推送 RefundStatus 空串误回收)、M2(发货校验失败返回成功应答吞资损线索)、M5(补偿任务 Failed 被回调重置)、M14(衣物失败回退与删除并发漏退)、M10(refresh-token access_token 文档矛盾)、M13(Outfit.Public 并发语义冲突)。限流(M11)与上传防护(M12)本次不修复。 |
|
用户需求
根据二次代码审查报告,修复用户确认的 6 个问题(最新调整:限流缺失 M11、上传防护 M12 暂不修复):
- M1 退款推送误回收:
RefundStatus字段为空串或未知值时被误判为「退款成功」并回收权益,修复为仅当明确等于SUCCESS才回退 - M2 发货校验失败静默:发货推送本地校验失败(订单不存在/渠道不符/道具不符/金额不符/环境不符)返回成功应答,微信不重试且资损线索被吞掉,修复为返回失败应答触发重试
- M5 补偿任务重试上限失效:失败任务被支付回调重置(retry_count 清零),绕过重试上限与人工介入机制
- M14 衣物额度漏退窗口:异步处理失败回退与用户删除并发时,可能两个「应回退」事件叠加却零回退,用户损失额度
- M10 refresh-token 文档矛盾:README 标注 access_token 为可选,但代码要求必填,统一为必填
- 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的grantBenefitIdempotentFailed 分支:新增冷却常量benefitRetryCooldownSeconds = 300;当nowUnix()-existing.UpdatedAt < 300时仅打告警跳过,冷却期外才 Reset + executeGrant,恢复重试上限与人工介入机制
3. M14:衣物处理失败回退事务化
internal/service/clothes.go重写markProcessFailed为单事务(复用DeleteByUUID的 FOR UPDATE + 回退模式):
tx.Clauses(clause.Locking{Strength: "UPDATE"})按clothes_uuid查衣物;不存在/已软删(RecordNotFound)或已是失败态则提交并跳过- 置
image_process_status=5+fail_reason - 同事务回退额度:条件更新
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.md686 行:access_token 由「否(可选,向前兼容)」改为「是(必填)」,说明缺少返回 10000docs/代码审查报告-二次check.md:M1/M2/M5/M10/M13/M14 标题后标注「已修复」及一行修复说明;M11/M12 保持未修复状态
验证
go build ./...全量编译通过go test ./pkg/wechat/通过read_lints检查修改文件无新增告警