Files
zt-closet-backend/.codebuddy/plans/修复中等与轻微问题8项_e909def3(未完成).md
T
2026-09-11 01:41:52 +08:00

7.9 KiB

name, overview, todos
name overview todos
修复中等与轻微问题8项 按二次审查报告修复 8 个问题:M1(退款推送 RefundStatus 空串误回收)、M2(发货校验失败返回成功应答吞资损线索)、M5(补偿任务 Failed 被回调重置)、M14(衣物失败回退与删除并发漏退)、M10(refresh-token access_token 文档矛盾)、M11(公开接口无限流)、M12(上传接口无 body 大小限制)、M13(Outfit.Public 并发语义冲突)。
id content status
fix-xpay-notify 修复虚拟支付推送链路:新增 XPayRefundStatusSuccess 常量,handleRefundNotify 改白名单判断,handleGoodsDeliver 五个校验失败分支返回 70009 触发微信重试 pending
id content status
fix-benefit-reset 修复补偿任务重置:BenefitGrantTaskReset 保留 retry_count,失败任务增加 5 分钟冷却期后才允许回调重新触发 pending
id content status
fix-clothes-refund-tx 修复衣物回退漏退:markProcessFailed 重写为行锁单事务(置失败+回退+流水),删除 refundIfExists 与 ClothesMarkProcessFailedIfNotFailed pending
id content status
fix-outfit-public-lock 修复穿搭公开竞态:redismodel 新增 KeyLockOutfitPublic,OutfitService.Public 全流程加 Redis 锁串行化 pending
id content status
add-ratelimit-upload-guard 新增登录/刷新限流中间件(Redis 固定窗口 fail-open)并接入路由,上传接口加 MaxBytesReader 请求体限制 pending
id content status dependencies
sync-docs 同步文档:README 修正 refresh-token access_token 为必填,审查报告 M1/M2/M5/M10/M11/M12/M13/M14 标注已修复并验证编译测试 pending
fix-xpay-notify
fix-benefit-reset
fix-clothes-refund-tx
fix-outfit-public-lock
add-ratelimit-upload-guard

产品概述

基于二次审查报告,修复用户确认的 8 个问题:虚拟支付退款误回收(M1)、发货校验失败静默(M2)、补偿任务重试上限失效(M5)、衣物额度漏退窗口(M14),以及限流缺失(M11)、上传接口无请求体限制(M12)、refresh-token 文档矛盾(M10)、穿搭公开并发语义冲突(M13)。全部为后端 Go 代码修复,不涉及 UI。

核心功能

  • M1 退款推送白名单:仅当 RefundStatus == "SUCCESS" 才回收权益,空串/未知状态仅记录日志,杜绝误回收
  • M2 发货校验失败应答:发货推送本地校验失败(订单不存在/渠道不符/道具不符/金额不符/环境不符)返回失败应答触发微信重试,不再静默吞掉资损线索;并发锁竞争分支保持成功应答防重试风暴
  • M5 补偿任务冷却保护:任务重置不再清零重试计数,且失败任务 5 分钟冷却期外才允许被回调重新触发,重试上限与人工介入机制恢复有效
  • M14 衣物回退事务化:处理失败标记与额度回退合并为单事务(行锁互斥),消除与删除并发时的漏退窗口,保证恰好回退一次
  • M11 登录/刷新限流:新增基于 Redis 固定窗口的限流中间件(按 IP,Redis 故障 fail-open),仅应用于登录与刷新两个公开接口,回调接口不限流
  • M12 上传请求体限制:上传接口在 multipart 解析前用 MaxBytesReader 按配置上限拦截超大请求体
  • M13 穿搭公开串行化:公开/取消公开流程加 per-outfit Redis 锁,消除并发下「回退公开但另一请求误报成功」的语义冲突
  • M10 文档修正:README 中 refresh-token 的 access_token 改为必填,与代码实现一致

技术方案

1. M1 + M2:虚拟支付推送链路修复

  • pkg/wechat/virtualpay.go:在退款推送结构附近新增常量 XPayRefundStatusSuccess = "SUCCESS"
  • internal/service/virtualpay.go handleRefundNotify:条件改为 if refund.RefundStatus != wechat.XPayRefundStatusSuccess { 记录日志; return nil },仅成功状态走 settleRefundSuccess;订单不存在分支保持 return nil(退款重试无意义)
  • handleGoodsDeliver:order not found / pay_channel 不符 / product mismatch / amount mismatch / env mismatch 五个分支由 return nil 改为 return errcode.OrderRealityCheckFailed(70009 已存在),使 XPayNotify 回 ErrCode:1 让微信重试;锁竞争失败分支保持 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 同模式,已 import gorm/clause):
  1. tx.Clauses(clause.Locking{Strength: "UPDATE"}) 按 clothes_uuid 查衣物;不存在/已软删(RecordNotFound)或已是失败态则 return nil(删除路径已回退或已处理)
  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, outfitUUID), 10s),失败返回 errcode.TooManyRequests;成功后 defer 解锁;锁内保留现有「条件公开 → 扣额 → 失败条件回退」流程,串行化后并发请求语义正确

5. M11:限流中间件

  • 新增 internal/middleware/ratelimit.go:RateLimit(limit int, window time.Duration) gin.HandlerFunc
  • Redis 固定窗口:key = redismodel.KeyRateLimit(IP),首次 INCR 后设 EXPIRE window,超过 limit 返回 errcode.TooManyRequests(10003,已映射 HTTP 429)
  • Redis 异常时 fail-open 放行(与项目降级策略一致),仅告警
  • internal/redismodel/ 新增 key 常量(如 KeyRateLimit = RedisPrefix + "ratelimit:%s")
  • internal/routers/router.go:对 /user/wxlogin、/user/refresh-token 两个路由应用,默认阈值常量 每 IP 每 60 秒 30 次;微信支付/xpay 回调路由不接限流(避免误伤真实回调)

6. M12:上传请求体限制

  • internal/routers/api/v1/clothes.go Upload:在 c.FormFile 前设置 c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, int64(global.UploadSetting.MaxSizeMB+2)*1024*1024)(留 multipart 边界余量)
  • FormFile 失败时:错误信息含 "request body too large" 返回 errcode.UploadFileTooLarge(30002),否则保持 errcode.UploadFileEmpty

7. M10 + 文档同步

  • README.md 686 行:access_token 由「否(可选,向前兼容)」改为「是(必填)」,说明缺少返回 10000
  • docs/代码审查报告-二次check.md:M1/M2/M5/M10/M11/M12/M13/M14 标题后标注「已修复」及一行修复说明

验证

  • go build ./... 全量编译通过
  • go test ./pkg/wechat/ 通过
  • read_lints 检查修改文件无新增告警

关键代码结构

限流中间件签名:

// RateLimit 基于 Redis 固定窗口的按 IP 限流中间件(Redis 故障 fail-open)。
func RateLimit(limit int, window time.Duration) gin.HandlerFunc

衣物失败回退事务核心流程:

tx 开始
  SELECT ... FOR UPDATE clothes(不存在/已删/已失败 → 提交并跳过)
  UPDATE clothes SET image_process_status=5, fail_reason=?
  UPDATE quota SET used=used-1 WHERE used>=1(条件失败 → 跳过流水)
  回读 quota → 写回退流水(唯一索引兜底)
tx 提交