Files
zt-closet-backend/.codebuddy/plans/退款申请订单状态流转修复_7d9218ad.md
T
zhengchiliu b94dd6dffb feat(order): 重构退款全流程并新增申请退款中状态
- 订单状态新增「6 申请退款中」,用户申请退款原子流转并落待审核退款流水
- 退款失败回退订单至已支付并恢复被取消的权益发放任务
- 新增虚拟支付退款查单兜底任务,反查超时订单并自动订正
- 会员包来源额度不叠加,扣减/回退/回收口径统一为只取生效一份
- 退款回收流水按吊销前后有效剩余差值记账,避免无效回收记录
- 同步更新 README 与 Stitch 设计文档
2026-09-16 03:44:09 +08:00

10 KiB
Raw Blame History

name, overview, todos
name overview todos
退款申请订单状态流转修复 新增订单状态「6=申请退款中」,修复用户发起退款申请后订单状态仍停留在「已支付」的问题:退款受理与订单状态流转原子化,退款成功流转为「5 已退款」,退款失败回退为「2 已支付」以便重新申请,并同步接口文档与状态机。
id content status
add-status-constant 新增订单状态 6「申请退款中」常量,并同步 mysqlmodel 与两份迁移 SQL 的列注释 completed
id content status dependencies
atomic-refund-apply 实现退款受理原子流转 DAO 并改造 RefundApply,使订单 2→6 与流水落库同事务 completed
add-status-constant
id content status dependencies
settle-and-failure 放行状态 6:退款成功结算 6→5 并建回收任务,退款失败回退 6→2(普通与虚拟支付回调) completed
atomic-refund-apply
id content status dependencies
list-filter-dto 订单列表放开按状态 6 筛选并更新 DTO 注释 completed
add-status-constant
id content status dependencies
refund-tests 用 sqlmock 补充退款受理、成功结算、失败回退的单测 completed
atomic-refund-apply
settle-and-failure
id content status dependencies
sync-api-docs 同步接口文档:枚举、状态机、退款流程说明与前端轮询 FAQ completed
atomic-refund-apply
settle-and-failure

产品概述

修复订单退款流程中的状态缺失问题:用户提交退款申请成功后,订单状态应实时切换为「申请退款中」,而当前订单仍停留在「已支付」,导致用户端无法感知退款进度,运营也无法按状态筛选待处理退款单。

核心功能

  • 新增订单状态「6=申请退款中」:用户通过 POST /order/refund 申请退款成功后,订单立即由「2 已支付」流转为「6 申请退款中」
  • 申请受理保持幂等与并发安全:同一订单重复申请、并发申请均只受理一次;上次退款失败的订单可复用旧退款流水重新申请
  • 退款成功:微信退款回调(普通支付/虚拟支付)到达后订单流转为「5 已退款」,并回收已发放权益、发送退款通知
  • 退款失败:微信回调返回非成功结果时,订单回退为「2 已支付」、退款流水置为失败,用户可重新申请
  • 订单列表支持按新状态筛选:前端可按「申请退款中」过滤订单并轮询进度
  • 已取消但已付款的竞态订单保持「3 已取消」不变(其权益从未发放)
  • 同步更新接口文档:订单状态枚举、状态机、退款模块说明与前端轮询说明

技术栈

  • 语言与框架:Go + Gin + GORM(现有项目架构,不引入新框架)
  • 数据库:MySQL(orders.order_status 为 TINYINT,无枚举约束,无需改列类型);Redis 仅用于回调分布式锁(已有)
  • 测试:go-sqlmock + 现有 global.MysqlEngine 注入模式(参考 internal/service/order_quota_test.go、internal/mysqldao/hardening_test.go)
  • 迁移:golang-migrate(pkg/migration,go:embed,cmd/migrate 手动执行)

实现方案

核心思路

在订单状态枚举中新增 OrderStatusRefunding int8 = 6 // 申请退款中;将「受理退款申请」改造为事务内的「锁定订单 → 校验已支付 → 订单流转 2→6 → 写入/复用退款流水」原子操作,使订单状态与退款流水严格一致;再把退款结果回调链路(成功置 5、失败回退 2)与权益回收判断同步到新状态 6。

关键决策与权衡

  1. 用订单行锁 + 条件流转作为并发闸门:现有 RefundApply 注释称「order_no 唯一索引兜底并发」,但 refund_flow_logs 实际只有 uk_refund_flow_no(refund_no 唯一)与普通索引 idx_refund_flow_order(order_id),无 order_no 唯一约束,并发申请存在双写风险。改为在 DAO 中新增事务方法(SELECT ... FOR UPDATE 锁订单行,沿用 RefundSettleWithRevokeTask 的既有范式),只有拿到「2 已支付」的请求才能流转为 6 并落流水,天然互斥且幂等;顺手修正误导性注释。
  2. 失败回退为「已支付」(已与用户确认):与现有「失败可复用旧流水重新申请」逻辑闭环;注意运营在线下直接拒绝(未发起微信退款)不会产生回调,文档中明确提示该场景订单会停留在 6,需运营退款或人工订正。
  3. 已取消但已付款的自动退款单不改状态(已与用户确认):autoRefundCancelledOrder 生成待审核退款流水后保持订单 3 已取消,避免误创建权益回收任务(该单权益从未发放)。
  4. RefundSettleWithRevokeTask 必须放行 6:否则退款成功回调会静默 return,订单无法置 5;且状态 6 表示权益已发放过,必须创建 benefit_revoke_tasks 回收任务(现仅 order_status == 2 时创建,需扩展为 2 或 6)。
  5. 错误码不新增:复用 errcode.RefundNotAllowed(71001)、RefundApplyFailed(71003)、DatabaseError(19001),避免破坏前端约定。

性能与可靠性

  • 受理路径新增一次行锁事务,单次申请成本为 1 次主键/唯一索引行锁 + 2 条写语句,量级可忽略;无 N+1、无热点表全扫。
  • 状态流转全部使用条件更新(WHERE order_status = from),并发/乱序回调天然幂等,不依赖额外分布式锁。
  • 回调链路保持原有 Redis 锁 + 条件流转双重防重,不改变现有时序。

实现要点

  • internal/service/order.go:RefundApply 改用新的原子 DAO 方法,成功后返回 dto.RefundResponse(status=0 待处理,订单状态由前端 GET /order 轮询);RefundNotify 失败分支新增订单回退 6→2(条件更新,失败仅记日志不回滚已更新的流水状态,保证幂等)。
  • 日志沿用 slog.InfoContext/WarnContext/ErrorContext 与关键字段(order_no/user_id/out_refund_no/from_status/to_status),不打印敏感信息。
  • 兼容性:executeGrant 已按 order_status != 2 跳过发放,状态 6 自动阻止继续发权益,无需改动;OrderExpireByTime(1→4)、OrderListVirtualUnconfirmed(IN 1,4)、OrderListPaidMissingBenefitTasks(=2)、Cancel(仅待支付)均不受影响。
  • 新增迁移只改列注释(ALTER TABLE orders MODIFY COLUMN order_status ...),不涉及数据回填;存量已进入退款流程但订单仍为 2 的老数据,可后续人工订正,不在本次自动改写范围。

架构设计

沿用现有分层:routers → service → mysqldao → mysqlmodel,不新增架构模式。

flowchart LR
    A[POST /order/refund] --> B[OrderService.RefundApply]
    B --> C[RefundFlowLogDao.RefundApplyWithOrderStatus 事务<br/>锁订单行 + 2→6 + 写/复用流水]
    C --> D[(orders.order_status=6)]
    D --> E{微信退款回调}
    E -->|SUCCESS| F[RefundSettleWithRevokeTask<br/>6→5 + 取消发放 + 建回收任务]
    E -->|非 SUCCESS| G[流水置失败 + 订单回退 6→2]

目录结构

zt-closet-backend/
├── internal/
│   ├── service/
│   │   ├── constants.go          # [MODIFY] 新增 OrderStatusRefunding int8 = 6「申请退款中」;补充退款状态注释
│   │   ├── order.go              # [MODIFY] RefundApply 改为调用原子受理 DAO 方法并修正并发注释;RefundNotify 失败分支回退订单 6→2
│   │   └── virtualpay.go         # [MODIFY] handleRefundNotify:非 SUCCESS 时回退订单 6→2 并尽力置流水失败;SUCCESS 时尽力将已存在的流水置成功(保持与订单终态一致)
│   ├── mysqldao/
│   │   ├── order.go              # [MODIFY] RefundFlowLogDao 新增 RefundApplyWithOrderStatus:事务内 SELECT ... FOR UPDATE 锁订单、校验 2 已支付、条件流转 2→6、新建或复用(上次失败)退款流水;返回受理结果区分「不可退款/并发冲突/DB 错误」
│   │   └── benefit_revoke.go     # [MODIFY] RefundSettleWithRevokeTask:前置状态放行 6;状态为 2 或 6 时创建 benefit_revoke_tasks 回收任务
│   ├── dto/
│   │   └── order.go              # [MODIFY] OrderResponse.OrderStatus 注释补 6;OrderListRequest.Status 校验改为 oneof=1 2 3 4 5 6
│   └── mysqlmodel/
│       └── order.go              # [MODIFY] order_status 字段注释补 6=申请退款中
├── pkg/migration/
│   ├── 000001_init.up.sql        # [MODIFY] orders.order_status 列注释补 6=申请退款中(新库一致)
│   ├── 000002_order_status_refunding.up.sql    # [NEW] ALTER TABLE orders MODIFY COLUMN order_status 更新列注释(存量库)
│   └── 000002_order_status_refunding.down.sql  # [NEW] 回滚列注释为旧文案
├── internal/mysqldao/
│   └── order_refund_test.go      # [NEW] sqlmock 单测:受理成功 2→6 且流水落库;订单非已支付时拒绝且无写入;退款成功 6→5 且创建回收任务;失败回退 6→2 条件流转(复用 hardening_test.go / order_quota_test.go 的 setup 范式)
└── 接口文档.md                    # [MODIFY] order_status 枚举补 6(字段表、5.12 模块、错误码章节附近说明)、状态机图与退款流程说明、前端轮询表与 FAQ(申请后订单=6 申请退款中、失败回退 2)

关键代码结构(示意,接口级)

// internal/service/constants.go
const (
    // ...现有 1~5 不变
    OrderStatusRefunding int8 = 6 // 申请退款中(已受理退款申请,待运营审核/退款)
)

// internal/mysqldao/order.go
// RefundApplyWithOrderStatus 原子受理退款申请:锁定订单行并校验「2 已支付」,
// 流转为「6 申请退款中」,同时新建退款流水或复用上次失败的流水。
// 返回 allowed=false 表示订单状态不满足或并发冲突(映射 errcode.RefundNotAllowed)。
func (d *RefundFlowLogDao) RefundApplyWithOrderStatus(
    ctx context.Context, orderNo, refundNo, reason string, applyTime int64,
) (flow *mysqlmodel.RefundFlowLog, allowed bool, err error)

Agent Extensions

Skill

  • lsp-code-analysis
  • Purpose: 在改动前对 OrderStatusPaid/OrderStatusRefunded/order_status 做引用与影响面分析,确认所有消费订单状态的分支(含 DAO 查询、补偿任务、DTO 校验)都已覆盖状态 6
  • Expected outcome: 输出完整的引用清单与需同步修改的位置,避免遗漏消费点导致状态机断裂