7.5 KiB
7.5 KiB
name, overview, todos
| name | overview | todos | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fix-code-review-bugs | 修复全量代码审查确认的 5 项高/中优先级业务与技术 bug:支付回调与订单过期/取消竞态、grantVIP 并发续费丢失天数、补偿任务 failed 重置后死循环、补偿定时任务分布式锁误删、中间件 Redis 错误误拒服务。 |
|
产品概述
对现有后端进行全量代码审查后的 5 项高/中优先级缺陷修复,核心目标是堵住两个资损漏洞(支付回调竞态、VIP 并发续费丢失),并修复补偿任务死循环、分布式锁误删、鉴权中间件可用性三个技术逻辑缺陷。
核心功能
- 支付回调与订单过期/取消竞态修复:取消订单时同步调用微信关单;支付回调在订单被系统过期后仍能流转为已支付并发放权益,避免用户付款但权益不到账。
- VIP 并发续费原子化:会员续费从「读-改-写」改为单条原子 SQL(CASE 表达式),避免两笔订单并发支付时后写覆盖、丢失续费天数。
- 补偿任务重试恢复:失败补偿任务被重置为待发放时同步清零重试次数,避免因重试次数仍超限而陷入「立即置失败」的死循环。
- 补偿定时任务锁安全:分布式锁释放改为校验持有者 value 后删除,避免任务执行超时后误删其它实例的锁。
- 鉴权中间件容错:Redis 读取用户信息出错时回落到数据库查询,避免 Redis 抖动导致全部鉴权接口被拒绝。
非目标(本次不修改)
RefreshToken 轮换未失效旧 access token、微信登录并发可能重复建用户、创建订单落库失败产生孤儿预支付单、空衣橱快照分享仍扣额度、回调报文未截断长度,上述 5 项作为低优先级建议记录,不在本次修复范围内。
技术栈
- 语言与框架:Go 1.27、Gin、GORM(gorm.io/gorm v1.31.2)、go-redis、log/slog、OpenTelemetry
- 微信支付:官方
github.com/wechatpay-apiv3/wechatpay-go v0.2.21(已确认jsapi.JsapiApiService.CloseOrder与CloseOrderRequest结构) - 复用现有约定:Service 返回
(*dto.X, *errcode.Error)、DAO 返回底层 error、Redis 锁用redisdao.TryLock/Unlock、日志用slog+global.Tracer
实现方案
Bug1 支付回调竞态(资损)
pkg/wechat/pay.go新增CloseOrder(ctx, orderNo string) error:调用jsapi.JsapiApiService{Client: c.client}.CloseOrder(ctx, jsapi.CloseOrderRequest{OutTradeNo: core.String(orderNo), Mchid: core.String(c.mchID)}),直接返回 err。internal/service/order.goCancel:本地条件流转pending->cancelled成功后,best-effort调用wechat.CloseOrder,失败用slog.WarnContext记录(不阻断用户取消响应)。internal/service/order.goPayNotify:将「写支付信息 + 发放权益 + 发订单通知」抽取为可复用私有方法finalizePaidOrder(ctx, order, data);在pending->paid流转返回 false 后重新OrderGetByOrderNo读取订单:若OrderStatusExpired则改为expired->paid流转并调用finalizePaidOrder(用户已付款应发货);若OrderStatusCancelled则记录slog.ErrorContext告警(用户主动取消却仍支付,人工介入)。
Bug2 grantVIP 原子续费(资损)
internal/mysqldao/user.go新增原子方法UserRenewVIPAtomic(ctx, userID int64, now int64, memberDays int64, vipType int8) (int64, error),用一条UPDATE+gorm.Expr原子更新,返回 RowsAffected:vip_expire = (CASE WHEN vip_expire > ? THEN vip_expire ELSE ? END) + ?(参数 now, now, memberDays*86400)vip_type = ?(vipType)vip_start = CASE WHEN vip_type = 1 OR vip_expire <= ? THEN ? ELSE vip_start END(参数 now, now)internal/service/order.gograntVIP改造:先UserGetByID读旧值用于判定changeType/newVIPType/vipStart与流水OldVIPExpire,再调用UserRenewVIPAtomic,随后UserGetByID读回真实newExpire写 VIP 流水(OldVIPExpire=旧值、NewVIPExpire=读回值),最后失效用户缓存。核心收益:vip_expire的累加在数据库层原子完成,并发不丢天数。
Bug3 补偿任务死循环
internal/mysqldao/benefit_grant_task.goBenefitGrantTaskReset的 Updates 增加"retry_count": 0,使 failed 任务重置后真正进入可重试状态。
Bug4 补偿定时任务锁误删
internal/service/benefit_compensation.gorunBenefitCompensation改用redisdao.TryLock(ctx, lockKey, lockVal, time.Minute)+defer redisdao.Unlock(context.Background(), lockKey, lockVal),lockVal := genUUID(),复用现有 lua 校验 value 后才 DEL 的 Unlock 语义。
Bug5 中间件 Redis 错误回落 DB
internal/middleware/lzcwyjwt.goJWTAuth中UserInfoGet返回任何错误(redis.Nil或其它)都回落到mysqldao.NewUserDao().UserGetByID;仅 DB 查询也失败时才拒绝,并保留「DB 用户不存在 → UnauthorizedTokenError」「状态非正常 → UserDisabled」逻辑。
实现注意事项
- SQL 一律值绑定,
gorm.Expr仅用于表达式片段、参数通过占位符绑定,禁止字符串拼接 SQL。 - 所有改动保持向后兼容,不引入事务封装或新依赖;
Cancel关单与finalizePaidOrder为增量修改,不影响现有RefundNotify流程。 - 日志复用现有
slog,回调金额校验、订单归属校验保持不变;不打印 token、支付密钥等敏感信息。 vip_start/vip_type的 CASE 更新与vip_expire在同一 UPDATE 中原子完成,避免字段间短暂不一致;vip_type的续费/开通判定沿用现有memberDays >= 365规则。
目录结构
zt-closet-backend/
├── pkg/
│ └── wechat/
│ └── pay.go # [MODIFY] 新增 CloseOrder 关单方法
├── internal/
│ ├── service/
│ │ ├── order.go # [MODIFY] Cancel 关单、PayNotify 过期/取消竞态处理、grantVIP 原子续费、finalizePaidOrder 抽取
│ │ └── benefit_compensation.go # [MODIFY] 定时任务锁改用 TryLock/Unlock
│ ├── mysqldao/
│ │ ├── user.go # [MODIFY] 新增 UserRenewVIPAtomic 原子续费方法
│ │ └── benefit_grant_task.go # [MODIFY] Reset 增加 retry_count 清零
│ └── middleware/
│ └── lzcwyjwt.go # [MODIFY] UserInfoGet 错误回落 DB
关键代码结构
// internal/mysqldao/user.go
// UserRenewVIPAtomic 原子续费会员:vip_expire 使用 CASE 表达式累加避免并发丢失,
// 同时原子维护 vip_type 与 vip_start,返回受影响行数。
func (d *UserDao) UserRenewVIPAtomic(ctx context.Context, userID int64, now int64, memberDays int64, vipType int8) (int64, error)
// pkg/wechat/pay.go
// CloseOrder 关闭微信侧待支付订单,防止本地取消后用户仍完成支付。
func CloseOrder(ctx context.Context, orderNo string) error