first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
@@ -0,0 +1,179 @@
---
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` 需在部署时手动执行。