7.9 KiB
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 并发语义冲突)。 |
|
产品概述
基于二次审查报告,修复用户确认的 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.gohandleRefundNotify:条件改为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.goBenefitGrantTaskReset:删除retry_count: 0,保留原重试计数internal/service/order.gograntBenefitIdempotent的 Failed 分支:新增冷却常量benefitRetryCooldownSeconds = 300;当nowUnix() - existing.UpdatedAt < 300时仅打告警日志跳过,冷却期外才 Reset + executeGrant,防止微信回调重试风暴无限重置任务
3. M14:衣物处理失败回退事务化
internal/service/clothes.go重写markProcessFailed为单事务(与DeleteByUUID同模式,已 import gorm/clause):
tx.Clauses(clause.Locking{Strength: "UPDATE"})按 clothes_uuid 查衣物;不存在/已软删(RecordNotFound)或已是失败态则 return nil(删除路径已回退或已处理)- 置
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.goPublic:进入后先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.goUpload:在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.md686 行:access_token 由「否(可选,向前兼容)」改为「是(必填)」,说明缺少返回 10000docs/代码审查报告-二次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 提交