## 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 分布式执行，避免重复执行
- 持久化和可靠性：任务存储在 Redis，Worker 重启不丢失任务
- 监控和管理：通过 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 Scheduler（cmd/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. **可靠性提升**: 任务持久化在 Redis，Worker 重启后自动恢复
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 端点和业务逻辑
