Files
junhong_cmp_fiber/openspec/specs/order-expiration/spec.md
huang e661b59bb9
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m58s
feat: 实现订单超时自动取消功能,支持钱包余额解冻和 Asynq Scheduler 统一调度
- 新增 expires_at 字段和复合索引,待支付订单 30 分钟超时自动取消
- 实现 cancelOrder/unfreezeWalletForCancel 钱包余额解冻逻辑
- 创建 Asynq 定时任务(order_expire/alert_check/data_cleanup)
- 将原有 time.Ticker 轮询迁移至 Asynq Scheduler 统一调度
- 同步 delta specs 到 main specs 并归档变更
2026-02-28 17:16:15 +08:00

238 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Order Expiration
## Purpose
自动管理订单的超时失效,确保待支付订单在超时后自动取消,防止"僵尸订单"堆积,并自动释放已冻结的资源(如钱包余额)。
This capability supports:
- 订单超时时间配置和管理
- 定时扫描和自动取消超时订单
- 钱包余额自动解冻
- 过期订单查询和筛选
## ADDED Requirements
### Requirement: 订单过期时间字段
系统 SHALL 为每个订单设置过期时间字段(`expires_at`),用于判断订单是否超时。
**字段定义**
- `expires_at`订单过期时间TIMESTAMP可为 NULL
- 创建时自动设置:`expires_at = created_at + 30分钟`(仅待支付订单)
- 已支付/已取消/已退款订单的 `expires_at` 为 NULL
**索引设计**
- 复合索引:`idx_order_expires(expires_at, payment_status)` 优化定时任务查询
#### Scenario: 创建待支付订单时设置过期时间
- **WHEN** 用户创建订单,支付方式为 wechat 或 alipay订单状态为待支付payment_status = 1
- **THEN** 系统设置 `expires_at = created_at + 30分钟`
#### Scenario: 创建钱包支付订单(后台)不设置过期时间
- **WHEN** 代理在后台创建订单,支付方式为 wallet订单立即支付成功payment_status = 2
- **THEN** 系统不设置 `expires_at`,字段值为 NULL
#### Scenario: 订单支付成功后清除过期时间
- **WHEN** 待支付订单支付成功状态变更为已支付payment_status = 2
- **THEN** 系统将 `expires_at` 设置为 NULL
#### Scenario: 订单取消后清除过期时间
- **WHEN** 订单被取消payment_status = 3
- **THEN** 系统将 `expires_at` 设置为 NULL
---
### Requirement: 订单超时自动取消
系统 SHALL 通过定时任务自动扫描并取消超时订单。任务每分钟执行一次,批量处理超时订单。
**任务配置**
- 任务类型:`TaskTypeOrderExpire = "order:expire"`
- 执行频率:每分钟
- 单批处理量:最多 100 条
- 超时时间:`OrderExpireTimeout = 30 * time.Minute`
**任务逻辑**
1. 查询条件:`expires_at <= NOW() AND payment_status = 1`
2. 批量取消订单:更新 `payment_status = 3``expires_at = NULL`
3. 钱包余额解冻(如果订单涉及钱包预扣)
4. 记录日志
#### Scenario: 定时任务扫描超时订单
- **WHEN** 定时任务执行,当前时间为 2026-02-28 10:30:00
- **THEN** 系统查询 `expires_at <= '2026-02-28 10:30:00' AND payment_status = 1` 的订单,最多 100 条
#### Scenario: 批量取消超时订单
- **WHEN** 查询到 50 条超时订单
- **THEN** 系统批量更新订单状态为已取消payment_status = 3`expires_at = NULL`
#### Scenario: 钱包余额解冻(混合支付)
- **WHEN** 超时订单使用了混合支付,钱包预扣 2000 分
- **THEN** 系统解冻钱包余额 2000 分(`frozen_balance` 减少 2000
#### Scenario: 钱包余额解冻纯钱包支付H5 端)
- **WHEN** 超时订单使用了钱包支付H5 端创建待支付订单),钱包预扣 3000 分
- **THEN** 系统解冻钱包余额 3000 分
#### Scenario: 无需解冻钱包(在线支付)
- **WHEN** 超时订单使用了纯在线支付wechat/alipay没有钱包预扣
- **THEN** 系统不执行钱包解冻操作
#### Scenario: 任务执行日志
- **WHEN** 定时任务执行完成
- **THEN** 系统记录日志:处理订单数量、解冻钱包次数、执行耗时
---
### Requirement: 订单过期状态查询
系统 SHALL 支持按过期状态筛选订单,便于运营人员查询和分析超时订单。
**查询条件**(新增):
- `is_expired`(布尔值):
- `true`:查询已过期的待支付订单(`expires_at <= NOW() AND payment_status = 1`
- `false`:查询未过期的待支付订单(`expires_at > NOW() AND payment_status = 1`
- 不传:不按过期状态筛选
#### Scenario: 查询已过期的待支付订单
- **WHEN** 运营人员查询订单列表,筛选 `is_expired = true`
- **THEN** 系统返回 `expires_at <= NOW() AND payment_status = 1` 的订单列表
#### Scenario: 查询未过期的待支付订单
- **WHEN** 运营人员查询订单列表,筛选 `is_expired = false`
- **THEN** 系统返回 `expires_at > NOW() AND payment_status = 1` 的订单列表
#### Scenario: 订单详情显示过期状态
- **WHEN** 查询订单详情,订单为待支付且已超时
- **THEN** 响应包含 `is_expired = true``expires_at` 字段显示过期时间
#### Scenario: 订单列表响应包含过期时间
- **WHEN** 查询订单列表
- **THEN** 每个订单响应包含 `expires_at` 字段(可为 NULL
---
### Requirement: 钱包余额解冻逻辑
系统 SHALL 在订单取消(手动或自动)时,根据支付方式自动解冻钱包余额。
**解冻规则**
- 钱包支付H5 端待支付订单):解冻 `total_amount`
- 混合支付:解冻 `wallet_payment_amount`
- 纯在线支付:无需解冻
- 后台钱包一步支付:无需解冻(订单创建时已完成支付)
#### Scenario: 手动取消订单,解冻钱包
- **WHEN** 用户手动取消待支付订单,订单使用混合支付,钱包预扣 2000 分
- **THEN** 系统解冻钱包余额 2000 分,订单状态变更为已取消
#### Scenario: 自动取消订单,解冻钱包
- **WHEN** 定时任务自动取消超时订单,订单使用钱包支付,钱包预扣 3000 分
- **THEN** 系统解冻钱包余额 3000 分,订单状态变更为已取消
#### Scenario: 取消订单,无钱包预扣
- **WHEN** 用户取消待支付订单订单使用纯在线支付wechat
- **THEN** 系统不执行钱包解冻操作
#### Scenario: 钱包解冻事务保证
- **WHEN** 订单取消涉及钱包解冻
- **THEN** 订单状态更新和钱包余额解冻在同一事务中完成,任一失败则全部回滚
---
### Requirement: 超时配置常量
系统 SHALL 定义订单超时相关常量,统一管理超时时间和任务类型。
**常量定义**`pkg/constants/constants.go`
- `OrderExpireTimeout = 30 * time.Minute`订单超时时间30 分钟)
- `TaskTypeOrderExpire = "order:expire"`:订单超时取消任务类型
#### Scenario: 使用常量设置过期时间
- **WHEN** 创建待支付订单
- **THEN** 系统使用 `constants.OrderExpireTimeout` 计算 `expires_at`
#### Scenario: 使用常量注册任务
- **WHEN** 注册 Asynq 定时任务
- **THEN** 系统使用 `constants.TaskTypeOrderExpire` 作为任务类型
---
### Requirement: 性能优化
系统 SHALL 通过索引优化和批量处理确保超时任务的性能符合要求。
**性能指标**
- 定时任务查询耗时 < 50ms
- 单批次处理耗时 < 5s
- 单批处理量100 条
**优化措施**
- 使用复合索引 `idx_order_expires(expires_at, payment_status)` 优化查询
- 批量更新订单状态(单 SQL 语句)
- 钱包解冻支持批量操作(单事务)
#### Scenario: 复合索引优化查询
- **WHEN** 定时任务查询超时订单
- **THEN** 数据库使用 `idx_order_expires` 索引,查询耗时 < 50ms
#### Scenario: 批量处理限制
- **WHEN** 超时订单数量超过 100 条
- **THEN** 系统单次最多处理 100 条,剩余订单下次执行时处理
#### Scenario: 任务执行时间限制
- **WHEN** 定时任务执行
- **THEN** 单批次处理耗时 < 5s包括查询、更新、解冻、日志记录
---
### Requirement: 数据库迁移
系统 SHALL 提供数据库迁移脚本,添加 `expires_at` 字段和索引。
**迁移内容**
- 添加字段:`ALTER TABLE tb_order ADD COLUMN expires_at TIMESTAMP NULL COMMENT '订单过期时间'`
- 添加索引:`CREATE INDEX idx_order_expires ON tb_order(expires_at, payment_status)`
**回滚脚本**
- 删除索引:`DROP INDEX idx_order_expires ON tb_order`
- 删除字段:`ALTER TABLE tb_order DROP COLUMN expires_at`
#### Scenario: 迁移脚本执行成功
- **WHEN** 执行 `migrate up`
- **THEN** `tb_order` 表新增 `expires_at` 字段和 `idx_order_expires` 索引
#### Scenario: 回滚脚本执行成功
- **WHEN** 执行 `migrate down`
- **THEN** `tb_order` 表删除 `expires_at` 字段和 `idx_order_expires` 索引
#### Scenario: 迁移对现有数据的影响
- **WHEN** 执行迁移脚本
- **THEN** 已存在的订单 `expires_at` 字段值为 NULL不影响现有业务