Files
zt-closet-backend/.codebuddy/plans/fix-remaining-issues_32b47301.md
T
2026-09-11 01:41:52 +08:00

11 KiB
Raw Blame History

name, overview, todos
name overview todos
fix-remaining-issues 修复代码审查中除「问题2:唯一约束依赖数据库 Schema」外的全部技术/业务漏洞,共 9 项:退款改人工审核、access token 中间件校验、权益发放补偿流水+定时重试、订单超时过期、refundQuota 检查 RowsAffected、上传 magic bytes 校验、LLM prompt 注入防护、回调原始报文持久化、死代码清理。
id content status
refund-manual-review 在 order.go 与 wechat/pay.go 实现退款人工审核并持久化支付/退款回调报文 completed
id content status dependencies
benefit-compensation 新增 benefit_grant_task 表/模型/DAO 与 DDL,改造幂等发放并加补偿定时重试 completed
refund-manual-review
id content status dependencies
order-expire-scheduler 在 OrderDao 新增批量过期方法并新增订单超时过期定时任务 completed
benefit-compensation
id content status
token-session-check 在 lzcwyjwt.go 中间件增加 access token 会话存在性校验 completed
id content status
quota-refund-rows 修改 QuotaDecreaseUsed 返回影响行数并在 refundQuota 校验,同步检查调用点 completed
id content status
security-hardening 在 upload/clothes/tokenhub 增加图片 magic bytes 校验与 prompt 注入清洗 completed
id content status
dead-code-cleanup 删除 redisdao/quota.go 中未使用的 QuotaRemainDecr/QuotaRemainGet/脚本 completed

产品概述

对现有后端(Go + Gin + GORM + Redis)进行 9 项安全与资损漏洞修复,唯一约束依赖数据库 Schema 一项按用户要求保持不变。核心目标:堵住退款白嫖权益的资损漏洞、让权益发放可补偿、让 access token 可被会话撤销,并补齐订单过期、额度回退一致性、上传内容校验、prompt 注入防护、回调审计与死代码清理。

核心功能

  1. 退款改为人工审核:用户申请退款仅创建待审核流水,不再自动调用微信退款;运营人工退款后由回调更新订单状态。
  2. 权益发放补偿:支付成功发放权益失败时写入补偿任务表,后台定时任务幂等重试,超过上限后告警。
  3. access token 会话校验:JWTAuth 中间件校验 token 是否仍存在于 Redis 会话,登出/失效后立即拒绝。
  4. 订单超时自动过期:后台定时任务将超时未支付订单批量置为已过期。
  5. 额度回退一致性:回退配额时校验实际影响行数,无效回退不再误写成功流水。
  6. 上传内容校验:读取文件头 magic bytes 校验真实图片类型,防止伪造扩展名绕过。
  7. LLM prompt 注入防护:清洗拼入 prompt 的衣物名称等用户可控字段。
  8. 回调报文持久化:支付/退款回调原始报文写入订单与退款流水,便于审计对账。
  9. 死代码清理:删除未使用的 Redis 配额函数与脚本。

Tech Stack

  • 语言与框架:Go 1.27、Gin、GORM、go-redis、log/slog、OpenTelemetry
  • 复用现有架构:Service 层返回 (*dto.XxxResponse, *errcode.Error);DAO 返回底层 error;定时任务沿用 pkg/tokenhub/scheduler.go 的 go func + ticker + Redis 分布式锁模式

Implementation Approach

1. 退款人工审核(问题1)

internal/service/order.go 的 RefundApply 删除 wechat.Refund 调用与即时成功分支,仅创建 RefundFlowLog(Status: RefundStatusPending)并返回。保留 RefundNotify/settleRefundSuccess,运营在微信后台手动退款后由回调完成订单置为已退款、写 refund_time/refund_amount、发通知。RefundResponse.Status 已支持 0=待处理,无需改 DTO。

2. 回调报文持久化(问题10)

pkg/wechat/pay.go 的 VerifyAndDecryptPayNotify/VerifyAndDecryptRefundNotify 中,notifyHandler.ParseNotifyRequest 会消费 request.Body,需先 io.ReadAll(request.Body) 再用 io.NopCloser(bytes.NewReader(raw)) 恢复,并把 raw 通过返回结构新增的 Raw string 字段带回。order.go 的 PayNotify 将 raw 写入 orders.callback_raw,RefundNotify 写入 refund_flow_logs.callback_raw(字段已存在,仅需落库)。

3. 权益补偿流水 + 定时重试(问题4)

新增 benefit_grant_task 表(无 AutoMigrate,需提供 DDL)。唯一索引 (order_no, benefit_type) 作为幂等凭证,防止重复发放。

发放改造为幂等流程:

  1. 对每个权益,先尝试 INSERT 一条 status=pending 的补偿任务记录;
  2. INSERT 冲突说明该权益已有记录:查询状态,success 跳过,pending 交由定时任务,failed 重置为 pending;
  3. INSERT 成功则执行实际发放(VIP 延长/额度增加),成功后置 success,失败保留 pending 并写 last_error;
  4. 定时任务(单实例 Redis 锁)扫描 pending 且 retry_count < max_retry 的任务,重试前先查 quota_flow_logs/vip_flow_logs 中是否已有同 order_no 的发放流水,已发放则直接置 success,未发放才执行发放,从而把「发放成功但未置 success」崩溃窗口的重复发放风险降至最低;
  5. 重试失败 retry_count+1,超过 max_retry 置 failed 并记录 Error 告警日志。

4. 订单超时过期(问题5)

internal/mysqldao/order.go 新增批量条件更新方法(WHERE order_status=待支付 AND expire_time < now 置为已过期),条件更新天然幂等、多实例安全;新增定时任务每 1 分钟执行一次,main.go 启动。

5. access token 会话校验(问题3)

internal/middleware/lzcwyjwt.go 的 JWTAuth 在 ParseAccessToken 成功后,调用 redisdao.AuthUserIDByAccess:redis.Nil 或 uid 与 claims 不一致 → 拒绝;Redis 其他错误 → warn 日志并放行(与现有 UserInfoGet 降级风格一致)。

6. refundQuota 检查影响行数(问题7)

internal/mysqldao/quota.go 的 QuotaDecreaseUsed 改为返回 (bool, error);internal/service/service.go 的 refundQuota 在 RowsAffected==0 时记录 warn 且不写成功流水;同步检查 internal/service/clothes.go 中的调用点。

7. 上传 magic bytes 校验(问题8)

pkg/upload/upload.go 新增基于文件头字节的真实类型识别(jpeg/png/webp/gif);internal/service/clothes.go 的 Upload 在已读取的 imageData 之后、扣额度之前校验,不匹配返回 errcode.UploadFileExtNotAllow。

8. LLM prompt 注入防护(问题9)

pkg/tokenhub/outfit_recommend.go 新增 sanitizePromptField 辅助(截断长度、移除换行与控制字符、过滤可能改变指令语义的字符),应用到 FormatClothesInfo 的每个字段输出。

9. 死代码清理(问题11)

删除 internal/redisdao/quota.go 中未使用的 QuotaRemainDecr、QuotaRemainGet、quotaRemainDecrScript;保留仍在使用的 quotaDailyIncrScript。

Architecture Design

补偿发放幂等状态流转:

flowchart TD
    A[支付成功] --> B[逐权益尝试 INSERT 补偿任务]
    B -->|INSERT 成功| C[执行发放]
    B -->|唯一冲突| D{查询现有状态}
    D -->|success| E[跳过]
    D -->|pending| F[等待定时任务]
    D -->|failed| G[重置 pending 后走重试]
    C -->|成功| H[置 success]
    C -->|失败| I[保留 pending 写 last_error]
    I --> J[定时任务扫描 pending]
    J --> K{查发放流水是否已存在}
    K -->|已存在| H
    K -->|不存在| C
    J -->|retry 超限| L[置 failed 并告警]

Directory Structure

zt-closet-backend/
├── internal/
│   ├── service/
│   │   ├── order.go                        # [MODIFY] 退款改人工审核;PayNotify/RefundNotify 写回调 raw;grantOrderBenefits/grantVIP 幂等改造
│   │   ├── benefit_compensation.go         # [NEW] 权益补偿定时任务(扫描 pending、幂等重试、超限告警)
│   │   ├── order_expire.go                 # [NEW] 订单超时过期定时任务(每 1 分钟条件更新)
│   │   └── service.go                      # [MODIFY] refundQuota 检查 RowsAffected
│   ├── mysqlmodel/
│   │   └── benefit_grant_task.go           # [NEW] 补偿任务模型(唯一索引 order_no+benefit_type)
│   ├── mysqldao/
│   │   ├── benefit_grant_task.go           # [NEW] 补偿任务 DAO(插入/查询/条件抢占/状态更新)
│   │   ├── order.go                        # [MODIFY] 新增批量过期方法 OrderExpireByTime
│   │   └── quota.go                        # [MODIFY] QuotaDecreaseUsed 返回 (bool, error)
│   ├── middleware/
│   │   └── lzcwyjwt.go                     # [MODIFY] JWTAuth 增加 access token 会话校验
│   └── redisdao/
│       └── quota.go                        # [MODIFY] 删除未使用的 QuotaRemainDecr/QuotaRemainGet/脚本
├── pkg/
│   ├── wechat/
│   │   └── pay.go                          # [MODIFY] 验签前捕获 raw 并在返回结构新增 Raw 字段
│   ├── upload/
│   │   └── upload.go                       # [MODIFY] 新增 magic bytes 真实图片类型校验
│   └── tokenhub/
│       └── outfit_recommend.go             # [MODIFY] 新增 sanitizePromptField 并应用到 FormatClothesInfo
├── migrations/
│   └── benefit_grant_task.sql              # [NEW] 补偿任务表 DDL(含唯一索引)
└── main.go                                 # [MODIFY] 启动权益补偿与订单过期两个定时任务

Key Code Structures

// internal/mysqlmodel/benefit_grant_task.go
type BenefitGrantTask struct {
    Model
    UserID      int64  // 用户ID
    OrderNo     string // 商户订单号(与 benefit_type 组成唯一索引)
    GoodsID     int64  // 商品ID
    BenefitType int8   // 1=vip 2=cos 3=ai 4=closet_share 5=outfit_share
    Amount      int64  // 发放数量
    Status      int8   // 0=pending 1=success 2=failed
    RetryCount  int    // 已重试次数
    MaxRetry    int    // 最大重试次数
    LastError   string // 最近一次失败原因
}

// internal/mysqldao/quota.go
func (d *QuotaDao) QuotaDecreaseUsed(ctx context.Context, userID int64, quotaType int8, n int64) (bool, error)

Implementation Notes

  • 幂等是补偿重试的关键:所有发放先落 benefit_grant_task 唯一记录,重试前查 quota_flow_logs/vip_flow_logs(含 order_no)避免重复发放。
  • 定时任务必须带 Redis 分布式锁与 ctx 超时,避免多实例重复执行;沿用现有 slog + global.Tracer 记录日志与 trace。
  • 回调 raw 落库注意长度限制(订单/退款流水字段类型),必要时截断;验签失败时不落库。
  • SQL 一律值绑定,退款申请与发放不新增越权路径;prompt 清洗与 magic bytes 校验均为输入侧防护,不得引入 panic。
  • 无 AutoMigrate,migrations/benefit_grant_task.sql 需在部署时手动执行。