Files
junhong_cmp_fiber/openspec/changes/implement-order-expiration/design.md
huang 5bb0ff0ddf
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
fix: 修复代理钱包订单创建逻辑,拆分后台/H5端下单方法并归档变更
- 拆分订单创建为 CreateAdminOrder(后台一步支付)和 CreateH5Order(H5 两步支付)
- 新增 CreateAdminOrderRequest DTO,后台仅允许 wallet/offline 支付方式
- 同步 delta specs 到主规格(order-payment 更新 + admin-order-creation 新增)
- 归档 fix-agent-wallet-order-creation 变更
- 新增 implement-order-expiration 变更提案
2026-02-28 16:31:31 +08:00

678 lines
23 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.
## Context
当前系统中待支付订单创建后不会自动失效,虽然 `iot-order``order-payment` 规格文档中提到了超时取消机制,但实际代码中完全未实现。这导致:
1. **数据库膨胀**:大量"僵尸订单"(待支付但永不支付)占用存储空间
2. **用户体验差**:无法明确订单是否有效,用户可能尝试支付已过期订单
3. **资源浪费**:钱包余额被冻结但订单永不完成(混合支付场景)
4. **数据质量低**:订单统计数据不准确(包含大量永不完成的订单)
**现有实现**
- `tb_order` 表缺少 `expires_at` 字段
- 无超时相关的 Asynq 定时任务
- `OrderService.Cancel()` 方法不支持钱包解冻
- 无超时相关常量定义
**技术栈**
- Asynq v0.24.x 任务队列(已用于佣金计算、轮询等异步任务)
- GORM v1.25.x ORM
- PostgreSQL 14+(已有索引优化经验)
- Redis 6.0+(已用于分布式锁、缓存)
## Goals / Non-Goals
**Goals:**
1. 实现订单 30 分钟超时自动取消机制
2. 支持钱包余额自动解冻(混合支付/H5 钱包支付场景)
3. 提供过期状态查询和筛选功能
4. 性能符合要求(定时任务查询 < 50ms单批处理 < 5s
5. 支持数据库迁移和回滚
6. 不影响现有订单业务逻辑
**Non-Goals:**
1. ❌ 不支持可配置的超时时间(固定 30 分钟)
2. ❌ 不支持订单续期(延长过期时间)
3. ❌ 不发送超时提醒通知(后续可扩展)
4. ❌ 不处理已支付订单的退款超时(不在本次范围)
5. ❌ 不修改第三方支付回调逻辑(已有幂等保证)
## Decisions
### Decision 1: 数据库字段设计
**选择**: 新增 `expires_at TIMESTAMP NULL` 字段到 `tb_order`
**理由**:
- `NULL` 语义:已支付/已取消/已退款订单无需过期时间,设为 NULL 节省存储
- `TIMESTAMP` 类型:支持时区,精度到秒(超时 30 分钟,秒级精度足够)
- 索引设计:复合索引 `idx_order_expires(expires_at, payment_status)` 优化定时任务查询
**替代方案**:
- ~~使用 `expired_at` 字段名~~不符合业务语义expires_at 表示"何时过期"expired_at 表示"何时已过期"
- ~~使用 INT 存储 Unix 时间戳~~:可读性差,不利于 SQL 调试
- ~~使用单列索引 `idx_expires_at`~~性能不如复合索引WHERE 条件包含 payment_status
**数据迁移策略**:
- 迁移时已存在的订单 `expires_at` 初始化为 NULL
- 不对历史待支付订单设置过期时间(避免批量取消历史订单)
- 新创建的待支付订单才设置过期时间
---
### Decision 2: 定时任务实现方式
**选择**: 使用 Asynq 的 Scheduler周期任务调度器每分钟执行一次
**理由**:
- **架构统一性**:项目已使用 Asynq 作为任务队列基础设施,定时任务也应统一使用 Asynq Scheduler而非 `time.Ticker`
- **分布式支持**:多 Worker 部署时,通过 Redis 分布式锁确保任务只执行一次,避免重复处理超时订单
- **任务持久化**:任务记录在 Redis支持查询执行历史、监控失败率
- **自动重试**:支持任务失败自动重试(可配置重试次数和延迟)
- **无额外依赖**:复用现有 Redis 基础设施
- **未来扩展性**:为项目中现有的 `time.Ticker` 定时任务(告警检查器、数据清理)迁移到 Asynq 提供范例
**替代方案**:
- ~~使用 `time.Ticker`/`time.Timer`~~:虽然简单,但多 Worker 部署时会重复执行,且无任务持久化和执行历史
- ~~使用 PostgreSQL pg_cron 扩展~~:增加数据库负载,不符合项目架构(业务逻辑在应用层)
- ~~使用独立的 Cron 服务~~:增加运维复杂度,技术栈碎片化
**实现步骤**:
1. **创建 Asynq Scheduler 实例**`cmd/worker/main.go`
```go
// 创建 Asynq Scheduler
asynqScheduler := asynq.NewScheduler(
asynq.RedisClientOpt{
Addr: redisAddr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
},
&asynq.SchedulerOpts{
Location: time.Local, // 使用本地时区
},
)
```
2. **注册周期任务**
```go
// 注册订单超时检查任务(每分钟执行)
_, err := asynqScheduler.Register(
"@every 1m", // cron 表达式:每分钟
asynq.NewTask(constants.TaskTypeOrderExpire, nil),
asynq.Queue(constants.QueueDefault),
)
if err != nil {
appLogger.Fatal("注册订单超时任务失败", zap.Error(err))
}
```
3. **启动 Scheduler**
```go
if err := asynqScheduler.Start(); err != nil {
appLogger.Fatal("启动 Asynq Scheduler 失败", zap.Error(err))
}
defer asynqScheduler.Shutdown()
```
4. **创建 Task Handler**`internal/task/order_expire.go`
```go
type OrderExpireHandler struct {
orderService *order.Service
logger *zap.Logger
}
func (h *OrderExpireHandler) HandleOrderExpire(ctx context.Context, task *asynq.Task) error {
count, err := h.orderService.CancelExpiredOrders(ctx)
if err != nil {
h.logger.Error("取消超时订单失败", zap.Error(err))
return err // 返回错误Asynq 自动重试
}
if count > 0 {
h.logger.Info("成功取消超时订单", zap.Int("count", count))
}
return nil
}
```
5. **注册 Handler**`pkg/queue/handler.go`
```go
func (h *Handler) registerOrderExpireHandler() {
orderExpireHandler := task.NewOrderExpireHandler(
h.workerResult.Services.OrderService,
h.logger,
)
h.mux.HandleFunc(constants.TaskTypeOrderExpire, orderExpireHandler.HandleOrderExpire)
h.logger.Info("注册订单超时检查任务处理器", zap.String("task_type", constants.TaskTypeOrderExpire))
}
```
---
### Decision 3: 批量处理策略
**选择**: 单次最多处理 100 条订单,使用事务批量更新
**理由**:
- 避免单次处理时间过长(单批 < 5s
- 事务保证订单状态更新和钱包解冻的原子性
- 超过 100 条的订单在下次任务执行时处理(每分钟执行,延迟可接受)
**替代方案**:
- ~~使用 LIMIT 1000~~:单批处理时间可能超过 5s影响任务调度
- ~~使用分页循环处理~~:复杂度高,事务范围难控制
- ~~不使用事务~~:订单状态更新和钱包解冻可能不一致
**实现细节**:
```go
// 单批处理逻辑
func (s *Service) CancelExpiredOrders(ctx context.Context) (int, error) {
// 1. 查询超时订单(最多 100 条)
orders, err := s.orderStore.FindExpiredOrders(ctx, 100)
// 2. 开启事务
return len(orders), s.db.Transaction(func(tx *gorm.DB) error {
// 3. 批量更新订单状态
// 4. 批量解冻钱包余额(如需)
})
}
```
---
### Decision 4: 钱包余额解冻逻辑
**选择**: 在 `OrderService.Cancel()` 方法中统一处理解冻逻辑,支持手动取消和自动取消两种场景
**理由**:
- 代码复用:手动取消和自动取消共用同一解冻逻辑
- 事务保证:订单状态更新和钱包解冻在同一事务中
- 支持多种支付方式:钱包支付、混合支付
**解冻规则**:
| 支付方式 | 是否解冻 | 解冻金额 |
|---------|---------|---------|
| 钱包支付H5 端待支付) | ✅ | `total_amount` |
| 混合支付 | ✅ | `wallet_payment_amount` |
| 纯在线支付wechat/alipay | ❌ | - |
| 后台钱包一步支付 | ❌ | - (订单创建时已完成支付) |
**替代方案**:
- ~~在定时任务中直接解冻钱包~~:代码重复,手动取消时需重复实现
- ~~不在事务中解冻~~:可能导致订单已取消但钱包未解冻
**实现细节**:
```go
func (s *Service) Cancel(ctx context.Context, orderID uint) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 1. 查询订单
order, err := s.orderStore.GetByID(ctx, orderID)
// 2. 校验状态(只能取消待支付订单)
if order.PaymentStatus != model.PaymentStatusPending {
return errors.New(errors.CodeInvalidParam, "只能取消待支付订单")
}
// 3. 更新订单状态
order.PaymentStatus = model.PaymentStatusCancelled
order.ExpiresAt = nil
// 4. 解冻钱包余额(如需)
if needUnfreeze(order) {
amount := getUnfreezeAmount(order)
err := s.walletService.Unfreeze(ctx, tx, order.BuyerType, order.BuyerID, amount)
}
return s.orderStore.Update(ctx, tx, order)
})
}
```
---
### Decision 5: 订单创建流程修改
**选择**: 在 `OrderService.Create()` 方法中,仅对待支付订单设置 `expires_at`
**理由**:
- 后台钱包一步支付订单创建时立即完成支付(`payment_status = 2`),无需过期时间
- 线下支付订单offline创建时立即标记为已支付无需过期时间
- 只有 H5 端或后台创建的待支付订单需要设置过期时间
**设置规则**:
| 场景 | 订单状态 | 是否设置 `expires_at` |
|------|---------|---------------------|
| H5 端创建钱包支付订单 | `payment_status = 1` | ✅ `now + 30min` |
| H5 端创建在线支付订单wechat/alipay | `payment_status = 1` | ✅ `now + 30min` |
| H5 端创建混合支付订单 | `payment_status = 1` | ✅ `now + 30min` |
| 后台创建钱包支付订单 | `payment_status = 2` | ❌ NULL |
| 后台创建线下支付订单 | `payment_status = 2` | ❌ NULL |
**实现细节**:
```go
func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest) (*model.Order, error) {
order := &model.Order{
// ... 其他字段
PaymentStatus: model.PaymentStatusPending,
}
// 仅待支付订单设置过期时间
if order.PaymentStatus == model.PaymentStatusPending {
expiresAt := time.Now().Add(constants.OrderExpireTimeout)
order.ExpiresAt = &expiresAt
}
// 后台钱包一步支付逻辑
if req.PaymentMethod == "wallet" && isAdminContext(ctx) {
// 立即扣款并支付
order.PaymentStatus = model.PaymentStatusPaid
order.ExpiresAt = nil // 已支付订单无需过期时间
}
}
```
---
### Decision 6: 订单支付成功后清除过期时间
**选择**: 在订单支付成功时(`payment_status` 变更为 2将 `expires_at` 设置为 NULL
**理由**:
- 已支付订单不需要过期时间
- 避免查询混淆(`expires_at IS NOT NULL` 可快速筛选待支付订单)
- 节省存储NULL 值不占用索引空间)
**实现位置**:
- `OrderService.WalletPay()` - H5 端钱包支付成功
- `OrderService.HandlePaymentCallback()` - 第三方支付回调成功
**实现细节**:
```go
func (s *Service) WalletPay(ctx context.Context, orderID uint) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// ... 扣款逻辑
// 更新订单状态并清除过期时间
err := s.orderStore.UpdatePaymentStatus(ctx, tx, orderID, model.PaymentStatusPaid, time.Now(), nil)
})
}
```
---
### Decision 7: 查询过期状态实现方式
**选择**: 在 DTO 响应中动态计算 `is_expired` 字段,不存储在数据库
**理由**:
- 避免数据冗余(`is_expired` 可由 `expires_at` 和当前时间计算得出)
- 避免定时任务更新 `is_expired` 字段(增加数据库写负载)
- 支持按过期状态筛选(查询时使用 SQL 条件 `expires_at <= NOW()`
**替代方案**:
- ~~在数据库中存储 `is_expired` 布尔字段~~:需要定时更新,增加数据库负载
- ~~使用数据库视图~~:不符合项目架构(不使用视图)
**实现细节**:
```go
// DTO 响应
type OrderResponse struct {
// ... 其他字段
ExpiresAt *time.Time `json:"expires_at"`
IsExpired bool `json:"is_expired"` // 动态计算
}
// 动态计算逻辑
func buildOrderResponse(order *model.Order) *dto.OrderResponse {
resp := &dto.OrderResponse{
ExpiresAt: order.ExpiresAt,
}
// 动态计算是否过期
if order.ExpiresAt != nil && order.PaymentStatus == model.PaymentStatusPending {
resp.IsExpired = time.Now().After(*order.ExpiresAt)
}
return resp
}
// 查询过期订单的 SQL 条件
// WHERE expires_at <= NOW() AND payment_status = 1
```
---
### Decision 8: 性能优化策略
**选择**: 使用复合索引 + 批量操作 + 事务优化
**优化措施**:
1. **索引优化**: 复合索引 `idx_order_expires(expires_at, payment_status)` 覆盖查询条件
2. **批量更新**: 单 SQL 语句批量更新订单状态(避免 N 次数据库调用)
3. **批量解冻**: 钱包解冻支持批量操作(单事务中处理多个钱包)
4. **限制批次大小**: 单次最多处理 100 条,避免长事务
**性能指标**:
- 定时任务查询耗时:< 50ms
- 单批次处理耗时:< 5s
- 数据库连接池无阻塞
**监控指标**:
- 每次任务处理的订单数量
- 任务执行耗时
- 钱包解冻次数
- 失败订单数量
---
### Decision 9: 错误处理和重试策略
**选择**: 使用 Asynq 的重试机制,最多重试 3 次
**重试策略**:
- 可重试错误数据库连接失败、Redis 连接失败、钱包服务暂时不可用
- 不可重试错误:数据不一致(如钱包不存在)、业务逻辑错误
**实现细节**:
```go
func (h *OrderExpireHandler) HandleOrderExpire(ctx context.Context, task *asynq.Task) error {
count, err := h.service.CancelExpiredOrders(ctx)
if err != nil {
h.logger.Error("取消超时订单失败", zap.Error(err))
// 判断是否可重试
if isRetryableError(err) {
return err // 返回错误Asynq 自动重试
}
return asynq.SkipRetry // 不可重试错误,跳过重试
}
h.logger.Info("取消超时订单成功", zap.Int("count", count))
return nil
}
```
---
### Decision 10: 常量定义
**选择**: 在 `pkg/constants/constants.go` 中定义超时相关常量
**常量列表**:
```go
// 订单超时时间30 分钟)
const OrderExpireTimeout = 30 * time.Minute
// 订单超时取消任务类型
const TaskTypeOrderExpire = "order:expire"
// 单批处理订单数量上限
const OrderExpireBatchSize = 100
```
**理由**:
- 统一管理常量,避免硬编码
- 便于后续调整(如修改超时时间)
- 符合项目规范(所有常量定义在 `pkg/constants/`
---
### Decision 11: 重构现有定时任务为 Asynq Scheduler
**选择**: 将现有的 `time.Ticker`/`time.Timer` 定时任务迁移到 Asynq Scheduler
**理由**:
- 统一任务调度机制:项目架构设计初衷就是用 Asynq 承载所有任务和定时功能
- 分布式支持Asynq Scheduler 原生支持多 Worker 分布式执行,避免重复执行
- 持久化和可靠性:任务存储在 RedisWorker 重启不丢失任务
- 监控和管理:通过 Asynq Dashboard 统一监控所有定时任务执行状态
- 代码一致性:避免混用多种定时任务实现方式
**迁移范围**:
| 定时任务 | 当前实现 | 迁移后 |
|---------|---------|--------|
| 告警检查器 (`startAlertChecker`) | `time.NewTicker(1 * time.Minute)` | Asynq Scheduler `@every 1m` + `TaskTypeAlertCheck` |
| 数据清理定时任务 (`startCleanupScheduler`) | `time.NewTimer` (每天凌晨2点) | Asynq Scheduler `0 2 * * *` + `TaskTypeDataCleanup` |
**对比分析**:
| 特性 | time.Ticker/Timer | Asynq Scheduler |
|-----|------------------|-----------------|
| 分布式支持 | ❌ 多 Worker 重复执行 | ✅ 自动去重,单次执行 |
| 任务持久化 | ❌ Worker 重启丢失 | ✅ 存储在 Redis |
| 监控和管理 | ❌ 无统一界面 | ✅ Asynq Dashboard |
| 错误重试 | ❌ 需手动实现 | ✅ 内置重试机制 |
| 代码复杂度 | 中等(需手动管理 goroutine | 低(声明式配置) |
| 依赖 | 无Go 标准库) | Redis |
**实现细节**:
```go
// 告警检查任务 Handler
type AlertCheckHandler struct {
service *pollingSvc.AlertService
logger *zap.Logger
}
func (h *AlertCheckHandler) HandleAlertCheck(ctx context.Context, task *asynq.Task) error {
if err := h.service.CheckAlerts(ctx); err != nil {
h.logger.Error("告警检查失败", zap.Error(err))
return err // Asynq 自动重试
}
h.logger.Info("告警检查成功")
return nil
}
// 数据清理任务 Handler
type DataCleanupHandler struct {
service *pollingSvc.CleanupService
logger *zap.Logger
}
func (h *DataCleanupHandler) HandleDataCleanup(ctx context.Context, task *asynq.Task) error {
if err := h.service.RunScheduledCleanup(ctx); err != nil {
h.logger.Error("数据清理失败", zap.Error(err))
return err
}
h.logger.Info("数据清理成功")
return nil
}
// 注册到 Asynq Schedulercmd/worker/main.go
scheduler.Register("@every 1m", asynq.NewTask(constants.TaskTypeAlertCheck, nil))
scheduler.Register("0 2 * * *", asynq.NewTask(constants.TaskTypeDataCleanup, nil))
```
**Cron 表达式说明**:
- `@every 1m` - 每分钟执行(告警检查)
- `0 2 * * *` - 每天凌晨 2:00 执行(数据清理)
**迁移后的优势**:
1. **统一架构**: 所有定时任务都使用 Asynq Scheduler代码风格一致
2. **易于管理**: 通过 Asynq Dashboard 查看所有定时任务的执行历史和状态
3. **易于扩展**: 新增定时任务只需注册 Cron 表达式,无需管理 goroutine
4. **可靠性提升**: 任务持久化在 RedisWorker 重启后自动恢复
5. **分布式友好**: 多 Worker 部署时自动避免重复执行
**风险和缓解**:
- **Redis 依赖**: 如果 Redis 故障,定时任务无法执行
- 缓解Redis 高可用部署(主从 + 哨兵)
- **迁移风险**: 迁移过程中可能遗漏某些任务
- 缓解:保留旧代码注释,测试验证所有任务正常执行后再删除
## Risks / Trade-offs
### Risk 1: 定时任务延迟导致订单超时时间不精确
**风险**: 定时任务每分钟执行一次,订单实际取消时间可能晚于过期时间 1 分钟
**影响**: 低。30 分钟超时容忍 1 分钟误差(最多 3.3% 误差)
**缓解措施**:
- 在用户支付时检查订单是否过期(前端 + 后端双重校验)
- 在订单详情中显示过期时间,提示用户尽快支付
---
### Risk 2: 批量处理可能导致部分订单取消失败
**风险**: 批量处理 100 条订单时,如果某个订单的钱包解冻失败,整个事务回滚
**影响**: 中。失败的订单会在下次任务执行时重新处理,但可能延迟 1 分钟
**缓解措施**:
- 使用 Asynq 重试机制(最多重试 3 次)
- 记录失败日志,便于排查问题
- 后续优化:考虑单个订单失败不影响其他订单(分批事务)
---
### Risk 3: 钱包余额解冻失败导致用户损失
**风险**: 订单取消成功但钱包解冻失败(如钱包不存在、冻结余额不足)
**影响**: 高。用户钱包余额永久冻结
**缓解措施**:
- 在同一事务中处理订单取消和钱包解冻,任一失败则全部回滚
- 记录详细日志,包含订单 ID、钱包 ID、解冻金额
- 提供人工介入机制(运营后台手动解冻)
---
### Risk 4: 数据库索引失效导致查询性能下降
**风险**: 随着订单数量增长,索引选择性下降,查询性能降低
**影响**: 中。定时任务查询耗时超过 50ms
**缓解措施**:
- 定期监控查询耗时
- 定期归档历史订单(如 6 个月前的已完成/已取消订单)
- 必要时调整索引策略(如分区表)
---
### Risk 5: Redis 故障导致定时任务无法执行
**风险**: Redis 故障导致 Asynq 任务调度失败,超时订单无法取消
**影响**: 高。订单堆积,数据库膨胀
**缓解措施**:
- Redis 高可用部署(主从复制 + 哨兵)
- 监控 Redis 可用性和 Asynq 任务执行状态
- 提供手动触发取消超时订单的 API运营后台
---
### Trade-off: 性能 vs 准确性
**选择**: 优先保证性能(每分钟执行,单批 100 条),牺牲部分准确性(延迟 1 分钟)
**理由**: 30 分钟超时场景下1 分钟延迟影响可接受;性能更重要(避免数据库负载过高)
---
### Trade-off: 代码复用 vs 逻辑独立
**选择**: `Cancel()` 方法同时支持手动取消和自动取消,逻辑复用
**理由**: 避免代码重复,降低维护成本;风险是逻辑耦合,但通过参数区分场景(手动 vs 自动)可缓解
## Migration Plan
### Phase 1: 数据库迁移(不影响业务)
1. 执行迁移脚本 `migrations/000xxx_add_order_expiration.up.sql`
```sql
ALTER TABLE tb_order ADD COLUMN expires_at TIMESTAMP NULL COMMENT '订单过期时间';
CREATE INDEX idx_order_expires ON tb_order(expires_at, payment_status);
```
2. 验证迁移成功:
```sql
SHOW INDEX FROM tb_order WHERE Key_name = 'idx_order_expires';
```
3. 已存在的订单 `expires_at` 为 NULL不影响现有业务
**回滚方案**:
```sql
DROP INDEX idx_order_expires ON tb_order;
ALTER TABLE tb_order DROP COLUMN expires_at;
```
---
### Phase 2: 代码部署API 服务)
1. 部署修改后的 API 服务(包含 `Create()` 和 `Cancel()` 逻辑)
2. 验证新创建的订单 `expires_at` 字段正确设置
3. 验证手动取消订单时钱包解冻正常
**验证步骤**:
- 创建待支付订单,检查 `expires_at` 是否为 `created_at + 30min`
- 手动取消混合支付订单,检查钱包余额是否解冻
- 监控错误日志,确认无异常
---
### Phase 3: 定时任务部署Worker 服务)
1. 部署修改后的 Worker 服务(包含定时任务)
2. 在 `cmd/worker/main.go` 中注册周期任务
3. 验证定时任务执行正常
**验证步骤**:
- 检查 Asynq 日志,确认任务每分钟执行
- 人工创建过期订单(修改 `expires_at` 为过去时间),等待 1 分钟后检查订单状态
- 监控任务执行耗时和处理订单数量
---
### Phase 4: 监控和告警
1. 配置 Prometheus 监控指标(任务执行次数、耗时、处理订单数)
2. 配置告警规则(任务执行失败、耗时超过 5s
3. 定期检查定时任务执行日志
**监控指标**:
- `order_expire_task_duration_seconds` - 任务执行耗时
- `order_expire_task_processed_total` - 处理订单总数
- `order_expire_task_failed_total` - 失败次数
---
### Rollback Strategy
**如果出现严重问题,按以下顺序回滚**:
1. **立即停止 Worker 服务**(停止定时任务执行)
2. **回滚 API 服务代码**(恢复到未修改的版本)
3. **回滚数据库**(执行 `migrations/000xxx_add_order_expiration.down.sql`
**触发回滚的条件**:
- 定时任务导致大量订单误取消
- 钱包余额解冻失败率 > 5%
- 数据库性能严重下降(查询耗时 > 500ms
## Open Questions
1. **是否需要发送订单超时通知?**
- 当前不发送通知Non-Goal
- 后续可扩展(如微信模板消息、短信提醒)
2. **是否支持可配置的超时时间?**
- 当前固定 30 分钟Non-Goal
- 后续可考虑按订单类型配置不同超时时间(如大额订单 1 小时)
3. **历史待支付订单如何处理?**
- 当前不处理(`expires_at` 为 NULL不会被定时任务取消
- 建议:运营后台提供批量取消功能,人工清理历史订单
4. **是否需要订单超时后自动重建订单?**
- 当前不支持Non-Goal
- 用户需要手动重新创建订单
5. **是否需要支持订单续期?**
- 当前不支持Non-Goal
- 如需支持,需增加 API 端点和业务逻辑