Files
zt-closet-backend/.codebuddy/plans/修复全部_39_项代码缺陷_f3b9019e.md
T
2026-09-11 01:41:52 +08:00

16 KiB
Raw Blame History

name, overview, todos
name overview todos
修复全部 39 项代码缺陷 根据代码审查报告,修复全部 39 项业务逻辑与技术逻辑 bug。核心是资损链路(退款回退权益、取消后付款自动退款、权益发放幂等化)、安全修复(密钥环境变量化、JWT 校验、错误码映射)与正确性修复(VIP 续费 CASE 求值顺序、Redis 超时单位、配额唯一约束等)。鉴权降级按用户决策维持 fail-open 现状,不改代码。
id content status
fix-dal 修复数据访问层:VIP 续费 CASE 求值顺序、QuotaAddTotal 命中检查、新增 QuotaReduceTotal/UserReduceVIP、配额与用户唯一索引 tag completed
id content status
fix-redis 修复 Redis 数据层:点赞/收藏未命中回源语义、计数 TTL、未读数对账、token 集合 TTL、清理 QuotaRemain 死缓存 completed
id content status dependencies
fix-benefit 修复权益发放幂等化:发放与流水同事务、订单快照、Paid 后凭证恢复、retry_count 原子自增、唯一冲突区分 completed
fix-dal
id content status dependencies
fix-refund 修复退款链路:退款成功回退权益、取消后付款自动微信退款、退款申请与流水幂等、grantQuota 并发建行与流水失真 completed
fix-dal
id content status dependencies
fix-business 修复其他业务逻辑:登录并发与封禁、点赞通知去重、穿搭公开锁、衣物回退竞态、衣柜分享明细、通知已读与 AI 锁错误 completed
fix-redis
id content status
fix-infra 修复基础设施与安全:Redis 超时单位、密钥环境变量化、CORS、错误码映射、端口绑定、优雅关闭、panic 恢复、JWT 校验、COS 与日志等 completed
id content status dependencies
fix-dto-migrate-verify 补充 DTO 校验、新增唯一索引迁移 SQL,并执行 go build 与 go vet 验证全部修复 completed
fix-dal
fix-redis
fix-benefit
fix-refund
fix-business
fix-infra

用户需求

修复此前全量代码审查发现的 39 项 bug(业务逻辑 15 项 + 技术逻辑 24 项),不引入新的架构模式,保持现有 Gin + GORM + Redis 分层风格,修复后项目可正常编译。

关键决策(已确认)

  1. 修复范围:全部 39 项(含低危的边界校验、日志、代码质量问题)。
  2. 退款权益:订单全额退款成功后,回退已发放的会员天数与各类额度。
  • 额度回退:仅回退未使用部分,已用部分不回退(total = max(used, total - n))。
  • VIP 回退:扣减会员天数,但到期时间不倒退到过去(vip_expire = max(now, vip_expire - n*86400))。
  1. 取消后付款:订单本地已取消但用户实际已付款时,自动发起微信退款(调用已存在的 wechat.Refund)。
  2. 鉴权降级:Redis 故障时 access token 会话校验维持 fail-open 现状,不改代码(仅在注释中记录该决策)。

修复原则

  • 优先修复资损与安全高危项(退款回退、权益重复发放、自动退款、密钥、JWT)。
  • 幂等性修复统一采用「唯一索引兜底 + 流水先写 + 累加同事务」的成熟模式。
  • 每个模块的修复保持小步、可编译,不跨模块做无关重构。
  • 新增数据库迁移 SQL 放入 migrations/,不动已有迁移文件。

技术栈

  • Go 1.27、Gin v1.12、GORM v1.31(MySQL + soft_delete 插件)、go-redis v9、golang-jwt/v5、OpenTelemetry、微信支付 apiv3(wechatpay-go 官方 SDK)、腾讯云 COS、viper。

实现方法

采用「分层分模块」修复策略,自底向上:数据访问层 → 资损链路 → 业务逻辑 → 基础设施 → DTO 校验与迁移。核心思路:

1. 退款回退权益(A1)

  • 新增 QuotaDao.QuotaReduceTotal:UPDATE user_quotas SET total = GREATEST(used, total - ?) WHERE user_id = ? AND quota_type = ?,检查 RowsAffected 防静默丢失。
  • 新增 UserDao.UserReduceVIP:UPDATE users SET vip_expire = GREATEST(?, vip_expire - ?) WHERE id = ?(now, days*86400)。
  • 幂等:仅在退款回调 SUCCESS 且流水从非成功态流转到成功态时执行回退;回退写 QuotaFlowLog/VIPFlowLog,新增 ChangeType 常量(退款回收),并以 order_no + 回收类型 唯一索引兜底防重复回退。

2. 取消后付款自动退款(A2)

  • PayNotify 的 OrderStatusCancelled 分支:生成 refundNo → 创建 RefundFlowLog(status=Processing) → 调用 wechat.Refund(orderNo, refundNo, payAmount, payAmount, reason) → 记录微信退款单号。
  • 幂等:创建退款流水前按 order_no 查重(已有非失败退款则跳过),并为 refund_flow_logs 增加 order_no 唯一约束(部分退款场景暂不支持,本项目为全额退款)。

3. 权益发放幂等化(A3/A4/A6/B11/B12)

  • 将「不可逆累加」与「幂等凭证写入」放入同一 DB 事务:
  • VIP:事务内先写 VIPFlowLog(唯一索引 order_no + change_type),冲突即视为已发放;成功后再 UserRenewVIPAtomic。
  • 额度:事务内先写 QuotaFlowLog(唯一索引 order_no + quota_type + change_type),冲突即视为已发放;成功后再 QuotaAddTotal。
  • grantOrderBenefits 改用订单快照字段(order.MemberDays)而非实时读 goods(A5);订单需在快照中补充各类额度字段或从 benefit_grant_tasks 的 Amount 取值。
  • 崩溃恢复(A6):PayNotify 的 OrderStatusPaid 幂等分支不再直接 return,而是检查该订单是否已存在 benefit_grant_tasks 凭证,若无则补建并发起发放(幂等)。
  • retry_count 改为 retry_count = retry_count + 1 原子自增(B11)。
  • grantBenefitIdempotent 用错误类型区分「唯一键冲突」与「其他 DB 错误」(B12)。

4. VIP 续费 CASE 修复(B2)

  • UserRenewVIPAtomic 改为原生 SQL(Exec)显式控制 SET 顺序,使 vip_start 先于 vip_expire 求值,读到旧 vip_expire,恢复「过期续费重置 vip_start」语义。

5. 配额与 Redis 修复(A7/B5/B6/B7/B13/B29/B30/B31)

  • QuotaAddTotal 补 RowsAffected 检查;user_quotas 模型加 (user_id, quota_type) 唯一索引 tag,并新增迁移 SQL。
  • grantQuota 创建分支改为「先插入(唯一索引冲突则重查后走累加)」;流水 oldValue/newValue 改为在原子更新后回读真实值写入。
  • 点赞/收藏 SIsMember 在 key 不存在时返回 redis.Nil 语义(或 DAO 返回 (false, redis.Nil)),使 service 层正确回落 MySQL。
  • 访问计数 INCR 配套首次设置 TTL;未读数 key 改用 DB 对账回源;token 集合 TTL 只在首次 SAdd 时设置;删除或补全只写不读的 QuotaRemain。

6. 基础设施与安全(B3/B4/B8/B9/B14-B19/B22-B28/B36)

  • Redis 超时统一单位:代码改 * time.Second,与配置注释「单位s」对齐(B3)。
  • 密钥环境变量化:setting 增加 AutomaticEnv + SetEnvKeyReplacer;config.yaml 敏感值改为占位/移除,保留 config.example.yaml 模板(B4/B25)。
  • CORS 收窄来源白名单(B8);错误码补充 Forbidden→403、TooManyRequests→429、ClassifyFailed→500 等映射(B9/B24)。
  • ListenAndServe 立即错误触发退出(B14);优雅关闭拆分 HTTP 与 OTel context(B15);COS nil 保护(B16);4 个后台 goroutine 加 defer recover()(B17);配置段 nil 校验(B18);JWT 校验 issuer 且拒绝空密钥(B19);日志目录 MkdirAll(B23);url.Parse 错误处理(B22);OTel 失败回滚全局 provider(B26);清理重复 UserDefaultQuota 配置段(B27);时间单位避免 duration 双重乘算(B28);访问日志 query 脱敏(B36)。

7. 其余业务逻辑(A8-A13/B10/B20/B35/B37/B38/B39)

  • 登录并发注册:wx_open_id 唯一索引 + duplicate 冲突后重查继续登录;登录拦截临时/永久封禁(A8/A13)。
  • 点赞通知去重(A9);穿搭公开加分布式锁 + 条件更新(A10);衣物删除回退改原子判定(A11);衣柜分享明细失败返回错误(A12)。
  • 刷新令牌强制携带旧 access token 并吊销(B10);通知已读条件更新防重复扣减(B20);登录与鉴权降级策略统一为 fail-open 并注释(B35);AI 锁错误区分处理(B37);穿搭明细更新包事务(B38);通知未读数递增错误处理(B39)。

架构设计

保持现有分层(routers → service → mysqldao/redisdao → mysqlmodel/redismodel),不新增架构层。核心修复后的资金/权益流转如下:

flowchart TD
    A[微信支付回调 PayNotify] --> B{订单状态}
    B -->|Pending| C[条件更新 Pending->Paid]
    C --> D[创建权益发放凭证 benefit_grant_tasks]
    D --> E[事务: 写流水唯一索引 + 累加权益]
    E --> F[权益到账]
    B -->|Cancelled| G[创建退款流水 + 调 wechat.Refund]
    G --> H[退款回调 RefundNotify]
    B -->|Paid 但无凭证| D
    H -->|SUCCESS| I[事务: 回退额度 GREATEST + 回退VIP天数]
    I --> J[写回退流水 幂等防重]
    J --> K[订单置 Refunded]

目录结构

以下为本次修复涉及的文件(MODIFY 表示修改,NEW 表示新增):

/root/zt-closet-backend
├── main.go                                    # [MODIFY] B14/B15/B16/B17/B18/B23/B28 端口绑定/优雅关闭/panic恢复/配置nil/日志目录/时间单位
├── configs/
│   ├── config.yaml                            # [MODIFY] B3/B4/B27 超时单位、密钥占位、清理重复段
│   └── config.example.yaml                    # [MODIFY] 同步模板更新
├── global/                                    # 视 B18 需要调整设置读取
├── migrations/
│   └── 20260907_bugfix_constraints.sql        # [NEW] 唯一索引:user_quotas、quota_flow_logs、vip_flow_logs、refund_flow_logs、users.wx_open_id
├── internal/
│   ├── mysqlmodel/
│   │   ├── quota.go                           # [MODIFY] A7 唯一索引 tag
│   │   ├── user.go                            # [MODIFY] A8 wx_open_id 唯一索引 tag
│   │   ├── quotaflowlog.go                    # [MODIFY] A4 唯一索引 tag
│   │   ├── vipflowlog.go                      # [MODIFY] A3 唯一索引 tag
│   │   └── refundflowlog.go                   # [MODIFY] A14 唯一索引 tag
│   ├── mysqldao/
│   │   ├── user.go                            # [MODIFY] B2 VIP CASE、新增 UserReduceVIP
│   │   └── quota.go                           # [MODIFY] B5/A7、新增 QuotaReduceTotal
│   ├── redisdao/
│   │   ├── like.go / collect.go               # [MODIFY] B6 未命中语义
│   │   ├── closet.go / outfit.go              # [MODIFY] B7 计数 TTL
│   │   ├── quota.go                           # [MODIFY] B29 死缓存清理
│   │   ├── notification.go                    # [MODIFY] B30 未读数对账
│   │   └── auth.go                            # [MODIFY] B31 token 集合 TTL
│   ├── redismodel/
│   │   └── keys.go                            # [MODIFY] B3 超时单位
│   ├── service/
│   │   ├── order.go                           # [MODIFY] A1/A2/A5/A6/A14/A15/B12/B21 退款回退/自动退款/快照/退款幂等
│   │   ├── benefit_compensation.go            # [MODIFY] A4/B11 幂等发放/retry_count
│   │   ├── service.go                         # [MODIFY] A7/B13 grantQuota 并发与流水
│   │   ├── constants.go                       # [MODIFY] 新增退款回收 ChangeType 常量
│   │   ├── user.go                            # [MODIFY] A8/A13/B10/B35 登录/封禁/刷新/降级
│   │   ├── like.go                            # [MODIFY] A9 点赞通知去重
│   │   ├── outfit.go                          # [MODIFY] A10/B38 公开锁/明细事务
│   │   ├── clothes.go                         # [MODIFY] A11 删除回退竞态
│   │   ├── closet.go                          # [MODIFY] A12 明细失败处理
│   │   ├── notification.go                    # [MODIFY] B20/B39 已读幂等/未读数
│   │   └── airecommend.go                     # [MODIFY] B37 锁错误处理
│   ├── dto/
│   │   ├── closet.go                          # [MODIFY] B32 过期时间校验
│   │   ├── user.go                            # [MODIFY] B33 binding tag
│   │   └── outfit.go / clothes.go             # [MODIFY] B34 文本长度上限
│   └── middleware/
│       ├── cors.go                            # [MODIFY] B8 来源白名单
│       ├── logger.go                          # [MODIFY] B36 query 脱敏
│       └── lzcwyjwt.go                        # [MODIFY] B1 仅注释说明 fail-open 决策
├── pkg/
│   ├── setting/setting.go                     # [MODIFY] B25 环境变量支持
│   ├── lzcwyjwt/lzcwyjwt.go                   # [MODIFY] B19 issuer/密钥校验
│   ├── errcode/errcode.go                     # [MODIFY] B9/B24 状态码映射
│   ├── cos/cosclient.go                       # [MODIFY] B22 url.Parse 错误
│   ├── tracing/provider.go                    # [MODIFY] B26 失败回滚
│   └── upload/upload.go                       # [MODIFY] B16 COS nil 保护
└── pkg/tokenhub/scheduler.go / fortune.go     # [MODIFY] B17 panic 恢复

关键代码结构

新增 DAO 方法(接口级定义,实现时遵循现有 DAO 风格):

// QuotaReduceTotal 原子扣减总配额(退款回收),保证 total 不低于 used,返回是否命中。
func (d *QuotaDao) QuotaReduceTotal(ctx context.Context, userID int64, quotaType int8, n int64) (bool, error)

// UserReduceVIP 原子回退会员到期时间,保证不早于 now,返回是否命中。
func (d *UserDao) UserReduceVIP(ctx context.Context, userID int64, now int64, memberDays int64) (bool, error)

实施注意事项

  • 兼容性:新增唯一索引前需评估存量数据是否已有重复(user_quotas、quota_flow_logs、vip_flow_logs、refund_flow_logs),迁移 SQL 需先清理/合并重复再建索引。
  • 事务边界:GORM 事务使用 db.Transaction(func(tx *gorm.DB) error {...}),避免跨 service 的裸事务。
  • 回退幂等:退款回退与权益发放的所有「写流水 + 累加/回退」必须在同一事务内,唯一索引冲突通过错误类型识别并视为已处理。
  • 日志与安全:环境变量化后,禁止在日志打印 openid/金额/密钥;config.yaml 敏感值改为 ${ENV_VAR} 占位。
  • 验证:修复完成后执行 go build ./... 与 go vet ./...,确保可编译且无新增告警;不强制写测试,但不破坏现有逻辑。

使用的扩展

SubAgent

  • code-explorer
  • 用途:在执行各模块修复前,快速确认相关文件的完整实现、字段定义与调用点(如 grantQuota/doGrantBenefit/RefundNotify 的全部调用方),确保修改不遗漏、不引入冲突。
  • 预期产出:每个修复模块的精确修改点清单与调用关系,支撑小步、可编译的改动。

Skill

  • lsp-code-analysis
  • 用途:修复后对关键函数(consumeQuota、grantQuota、doGrantBenefit、UserRenewVIPAtomic、QuotaReduceTotal、UserReduceVIP)做定义/引用/调用层次追踪,验证幂等与回退链路没有遗漏调用点或破坏现有引用。
  • 预期产出:关键函数完整调用链,确认修复后无断链、无重复发放/回退缺口。