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

179 lines
11 KiB
Markdown
Raw Permalink Blame History

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