first commit
This commit is contained in:
@@ -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` 需在部署时手动执行。
|
||||
Reference in New Issue
Block a user