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