11 KiB
name, overview, todos
| name | overview | todos | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fix-remaining-issues | 修复代码审查中除「问题2:唯一约束依赖数据库 Schema」外的全部技术/业务漏洞,共 9 项:退款改人工审核、access token 中间件校验、权益发放补偿流水+定时重试、订单超时过期、refundQuota 检查 RowsAffected、上传 magic bytes 校验、LLM prompt 注入防护、回调原始报文持久化、死代码清理。 |
|
产品概述
对现有后端(Go + Gin + GORM + Redis)进行 9 项安全与资损漏洞修复,唯一约束依赖数据库 Schema 一项按用户要求保持不变。核心目标:堵住退款白嫖权益的资损漏洞、让权益发放可补偿、让 access token 可被会话撤销,并补齐订单过期、额度回退一致性、上传内容校验、prompt 注入防护、回调审计与死代码清理。
核心功能
- 退款改为人工审核:用户申请退款仅创建待审核流水,不再自动调用微信退款;运营人工退款后由回调更新订单状态。
- 权益发放补偿:支付成功发放权益失败时写入补偿任务表,后台定时任务幂等重试,超过上限后告警。
- access token 会话校验:JWTAuth 中间件校验 token 是否仍存在于 Redis 会话,登出/失效后立即拒绝。
- 订单超时自动过期:后台定时任务将超时未支付订单批量置为已过期。
- 额度回退一致性:回退配额时校验实际影响行数,无效回退不再误写成功流水。
- 上传内容校验:读取文件头 magic bytes 校验真实图片类型,防止伪造扩展名绕过。
- LLM prompt 注入防护:清洗拼入 prompt 的衣物名称等用户可控字段。
- 回调报文持久化:支付/退款回调原始报文写入订单与退款流水,便于审计对账。
- 死代码清理:删除未使用的 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) 作为幂等凭证,防止重复发放。
发放改造为幂等流程:
- 对每个权益,先尝试 INSERT 一条
status=pending的补偿任务记录; - INSERT 冲突说明该权益已有记录:查询状态,
success跳过,pending交由定时任务,failed重置为 pending; - INSERT 成功则执行实际发放(VIP 延长/额度增加),成功后置
success,失败保留pending并写last_error; - 定时任务(单实例 Redis 锁)扫描
pending且retry_count < max_retry的任务,重试前先查quota_flow_logs/vip_flow_logs中是否已有同order_no的发放流水,已发放则直接置success,未发放才执行发放,从而把「发放成功但未置 success」崩溃窗口的重复发放风险降至最低; - 重试失败
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需在部署时手动执行。