fix: 修复代理钱包订单创建逻辑,拆分后台/H5端下单方法并归档变更
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
- 拆分订单创建为 CreateAdminOrder(后台一步支付)和 CreateH5Order(H5 两步支付) - 新增 CreateAdminOrderRequest DTO,后台仅允许 wallet/offline 支付方式 - 同步 delta specs 到主规格(order-payment 更新 + admin-order-creation 新增) - 归档 fix-agent-wallet-order-creation 变更 - 新增 implement-order-expiration 变更提案
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-28
|
||||
677
openspec/changes/implement-order-expiration/design.md
Normal file
677
openspec/changes/implement-order-expiration/design.md
Normal file
@@ -0,0 +1,677 @@
|
||||
## 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 端点和业务逻辑
|
||||
74
openspec/changes/implement-order-expiration/proposal.md
Normal file
74
openspec/changes/implement-order-expiration/proposal.md
Normal file
@@ -0,0 +1,74 @@
|
||||
## Why
|
||||
|
||||
当前系统中待支付订单创建后不会自动失效,导致大量"僵尸订单"占用数据库空间,且用户体验不佳(无法明确订单是否有效)。虽然现有规格文档(`iot-order`、`order-payment`)中提到了订单超时取消机制,但实际代码中完全未实现:缺少超时时间字段、定时任务、钱包解冻逻辑等。这是一个关键缺失功能,影响系统可用性和数据质量。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 订单超时自动失效(主要功能)
|
||||
|
||||
- 新增订单超时自动失效机制,待支付订单 30 分钟后自动取消
|
||||
- 新增数据库字段:`tb_order.expires_at`(订单过期时间)
|
||||
- 新增 Asynq 定时任务:每分钟扫描并取消超时订单
|
||||
- 新增常量定义:`OrderExpireTimeout`、`TaskTypeOrderExpire`
|
||||
- 完善订单取消逻辑:支持钱包余额自动解冻(混合支付场景)
|
||||
- 新增订单列表查询条件:过期状态筛选
|
||||
- 完善订单创建流程:自动设置 `expires_at = created_at + 30分钟`
|
||||
|
||||
### 架构优化:重构现有定时任务为 Asynq Scheduler
|
||||
|
||||
- 将现有的 `time.Ticker`/`time.Timer` 定时任务迁移到 Asynq Scheduler
|
||||
- 重构告警检查器(`startAlertChecker`)为 Asynq 周期任务(`@every 1m`)
|
||||
- 重构数据清理定时任务(`startCleanupScheduler`)为 Asynq 周期任务(每天凌晨2点)
|
||||
- 新增常量定义:`TaskTypeAlertCheck`、`TaskTypeDataCleanup`
|
||||
- 移除 `cmd/worker/main.go` 中的原生定时任务实现(`startAlertChecker`、`startCleanupScheduler`)
|
||||
- 统一所有定时任务调度机制为 Asynq Scheduler
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `order-expiration`:订单超时自动失效机制。包含:超时时间配置、定时扫描任务、自动取消逻辑、钱包余额解冻、过期状态查询。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `iot-order`:补充订单超时失效的需求(原规格中提到但未详细定义)
|
||||
- `order-payment`:补充钱包支付订单取消时的余额解冻需求
|
||||
|
||||
## Impact
|
||||
|
||||
**数据模型**:
|
||||
- `tb_order` 表新增字段:`expires_at TIMESTAMP`
|
||||
- 新增索引:`idx_order_expires(expires_at, payment_status)`
|
||||
|
||||
**代码影响**:
|
||||
- `internal/model/order.go`:新增 `ExpiresAt` 字段
|
||||
- `internal/service/order/service.go`:
|
||||
- `Create()` 方法设置过期时间
|
||||
- `Cancel()` 方法支持钱包解冻
|
||||
- 新增 `CancelExpiredOrders()` 方法
|
||||
- `internal/task/`:新增 `order_expire.go`、`alert_check.go`、`data_cleanup.go` 定时任务 Handler
|
||||
- `pkg/constants/constants.go`:新增超时和任务类型相关常量(`TaskTypeOrderExpire`、`TaskTypeAlertCheck`、`TaskTypeDataCleanup`)
|
||||
- `internal/store/postgres/order_store.go`:新增批量查询超时订单方法
|
||||
- `cmd/worker/main.go`:
|
||||
- 创建和启动 Asynq Scheduler 实例
|
||||
- 注册 3 个周期任务(订单超时、告警检查、数据清理)
|
||||
- 移除原生定时任务实现(`startAlertChecker`、`startCleanupScheduler`)
|
||||
- `pkg/queue/handler.go`:注册 3 个定时任务 Handler
|
||||
|
||||
**API 影响**:
|
||||
- 订单列表 API(`GET /api/admin/orders`、`GET /api/h5/orders`):新增过期状态筛选条件
|
||||
|
||||
**依赖**:
|
||||
- Asynq 任务队列(已有)
|
||||
- Redis(已有,用于任务调度)
|
||||
- 钱包服务(`internal/service/wallet/`,已有)
|
||||
|
||||
**性能考虑**:
|
||||
- 定时任务每分钟执行一次,批量处理超时订单(单次最多 100 条)
|
||||
- 使用复合索引 `idx_order_expires(expires_at, payment_status)` 优化查询
|
||||
- 预估查询耗时 < 50ms,单批次处理耗时 < 5s
|
||||
|
||||
**数据库迁移**:
|
||||
- 需要执行迁移脚本:`migrations/000xxx_add_order_expiration.up.sql`
|
||||
- 需要回滚脚本:`migrations/000xxx_add_order_expiration.down.sql`
|
||||
- 对现有数据的影响:已存在的待支付订单 `expires_at` 初始化为 `NULL`(需手动处理或忽略)
|
||||
@@ -0,0 +1,54 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 订单状态流转
|
||||
|
||||
系统 SHALL 管理订单的状态流转,确保状态变更符合业务规则。**新增订单超时自动取消的详细场景。**
|
||||
|
||||
**状态定义**:
|
||||
- **1-待支付**: 订单已创建,等待用户支付
|
||||
- **2-已支付**: 用户已支付,等待系统处理
|
||||
- **3-已完成**: 订单已完成(激活/发货等)
|
||||
- **4-已取消**: 订单已取消
|
||||
- **5-已退款**: 订单已退款
|
||||
|
||||
**状态流转规则**:
|
||||
- 待支付(1) → 已支付(2): 用户完成支付
|
||||
- 待支付(1) → 已取消(4): 用户手动取消订单或订单超时(30 分钟)
|
||||
- 已支付(2) → 已完成(3): 系统完成订单处理(激活/发货)
|
||||
- 已支付(2) → 已退款(5): 用户申请退款且审核通过
|
||||
- 已完成(3) → 已退款(5): 用户申请退款且审核通过(特殊情况)
|
||||
|
||||
#### Scenario: 用户支付订单
|
||||
|
||||
- **WHEN** 用户支付待支付订单(ID 为 10001),支付金额为 30.00 元
|
||||
- **THEN** 系统将订单状态从 1(待支付) 变更为 2(已支付),`paid_at` 记录支付时间
|
||||
|
||||
#### Scenario: 单卡套餐订单完成
|
||||
|
||||
- **WHEN** 系统处理完单卡套餐订单(ID 为 10001),激活 IoT 卡并分配套餐
|
||||
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
|
||||
|
||||
#### Scenario: 设备级套餐订单完成
|
||||
|
||||
- **WHEN** 系统处理完设备级套餐订单(ID 为 10002),为设备绑定的所有 IoT 卡分配套餐
|
||||
- **THEN** 系统将订单状态从 2(已支付) 变更为 3(已完成),`completed_at` 记录完成时间
|
||||
|
||||
#### Scenario: 用户手动取消订单
|
||||
|
||||
- **WHEN** 用户手动取消待支付订单(ID 为 10003)
|
||||
- **THEN** 系统将订单状态从 1(待支付) 变更为 4(已取消),`expires_at` 设置为 NULL,如有钱包预扣则解冻余额
|
||||
|
||||
#### Scenario: 订单超时自动取消
|
||||
|
||||
- **WHEN** 订单创建后 30 分钟未支付,定时任务扫描到该订单
|
||||
- **THEN** 系统自动将订单状态从 1(待支付) 变更为 4(已取消),`expires_at` 设置为 NULL,如有钱包预扣则解冻余额
|
||||
|
||||
#### Scenario: 订单超时自动取消(混合支付)
|
||||
|
||||
- **WHEN** 混合支付订单创建后 30 分钟未完成在线支付,钱包已预扣 2000 分
|
||||
- **THEN** 系统自动取消订单,解冻钱包余额 2000 分
|
||||
|
||||
#### Scenario: 订单超时自动取消(纯在线支付)
|
||||
|
||||
- **WHEN** 纯在线支付订单创建后 30 分钟未支付
|
||||
- **THEN** 系统自动取消订单,无需钱包解冻操作
|
||||
@@ -0,0 +1,237 @@
|
||||
# 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,不影响现有业务
|
||||
@@ -0,0 +1,67 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 订单支付处理
|
||||
|
||||
系统 SHALL 根据支付方式正确处理订单支付,包括钱包扣款、在线支付、混合支付等。**新增订单取消(手动或自动)时的钱包余额解冻逻辑。**
|
||||
|
||||
**钱包支付流程**:
|
||||
1. 检查钱包可用余额是否充足
|
||||
2. 冻结钱包余额(`frozen_balance` 增加)
|
||||
3. 创建订单,状态为"待支付"
|
||||
4. 订单完成后,扣减钱包余额(`balance` 减少,`frozen_balance` 减少),创建钱包明细记录
|
||||
5. 订单取消时(手动或自动),解冻钱包余额(`frozen_balance` 减少)
|
||||
|
||||
**在线支付流程**:
|
||||
1. 创建订单,状态为"待支付"
|
||||
2. 调用第三方支付接口
|
||||
3. 用户完成支付后,订单状态变更为"已支付"
|
||||
4. 订单完成后,订单状态变更为"已完成"
|
||||
|
||||
**混合支付流程**:
|
||||
1. 检查钱包可用余额是否充足(钱包支付部分)
|
||||
2. 冻结钱包余额
|
||||
3. 创建订单,状态为"待支付"
|
||||
4. 调用第三方支付接口(在线支付部分)
|
||||
5. 用户完成在线支付后,扣减钱包余额,订单状态变更为"已支付"
|
||||
6. 订单完成后,订单状态变更为"已完成"
|
||||
7. 订单取消时(手动或自动),解冻钱包余额
|
||||
|
||||
#### Scenario: 钱包支付订单完成
|
||||
|
||||
- **WHEN** 用户使用钱包支付购买套餐,订单金额为 3000 分
|
||||
- **THEN** 系统:
|
||||
1. 创建订单,状态为"待支付",冻结钱包余额 3000 分
|
||||
2. 订单处理完成后,扣减钱包余额 3000 分,解冻 3000 分,创建钱包明细记录(类型为"扣款"),订单状态变更为"已完成"
|
||||
|
||||
#### Scenario: 混合支付订单完成
|
||||
|
||||
- **WHEN** 用户使用混合支付购买套餐,钱包支付 2000 分 + 在线支付 3000 分
|
||||
- **THEN** 系统:
|
||||
1. 创建订单,状态为"待支付",冻结钱包余额 2000 分
|
||||
2. 用户完成在线支付 3000 分后,扣减钱包余额 2000 分,解冻 2000 分,创建钱包明细记录,订单状态变更为"已支付"
|
||||
3. 订单处理完成后,订单状态变更为"已完成"
|
||||
|
||||
#### Scenario: 订单手动取消,解冻钱包余额
|
||||
|
||||
- **WHEN** 用户使用钱包支付创建订单,订单金额为 3000 分,然后手动取消订单
|
||||
- **THEN** 系统解冻钱包余额 3000 分(`frozen_balance` 减少 3000),订单状态变更为"已取消"
|
||||
|
||||
#### Scenario: 订单超时自动取消,解冻钱包余额
|
||||
|
||||
- **WHEN** 用户使用混合支付创建订单,钱包预扣 2000 分,30 分钟后订单超时
|
||||
- **THEN** 系统自动取消订单,解冻钱包余额 2000 分(`frozen_balance` 减少 2000),订单状态变更为"已取消"
|
||||
|
||||
#### Scenario: 订单取消(纯在线支付),无需解冻
|
||||
|
||||
- **WHEN** 用户使用纯在线支付创建订单,30 分钟后订单超时
|
||||
- **THEN** 系统自动取消订单,不执行钱包解冻操作(因为没有钱包预扣)
|
||||
|
||||
#### Scenario: 钱包解冻事务保证
|
||||
|
||||
- **WHEN** 订单取<E58D95><E58F96>涉及钱包解冻
|
||||
- **THEN** 订单状态更新(`payment_status = 3`、`expires_at = NULL`)和钱包余额解冻在同一事务中完成,任一失败则全部回滚
|
||||
|
||||
#### Scenario: 钱包解冻失败回滚
|
||||
|
||||
- **WHEN** 订单取消时,钱包解冻失败(如钱包不存在、冻结余额不足)
|
||||
- **THEN** 事务回滚,订单状态不变,返回错误信息"订单取消失败"
|
||||
184
openspec/changes/implement-order-expiration/tasks.md
Normal file
184
openspec/changes/implement-order-expiration/tasks.md
Normal file
@@ -0,0 +1,184 @@
|
||||
## 1. 数据库迁移
|
||||
|
||||
- [ ] 1.1 创建迁移文件 `migrations/000xxx_add_order_expiration.up.sql`:添加 `expires_at` 字段和复合索引 `idx_order_expires(expires_at, payment_status)`
|
||||
- [ ] 1.2 创建回滚文件 `migrations/000xxx_add_order_expiration.down.sql`:删除索引和字段
|
||||
- [ ] 1.3 执行迁移验证:运行 `migrate up` 并检查表结构,确认字段和索引创建成功
|
||||
- [ ] 1.4 测试回滚:运行 `migrate down` 并验证字段和索引删除成功,然后重新 `migrate up`
|
||||
|
||||
## 2. 常量定义
|
||||
|
||||
- [ ] 2.1 在 `pkg/constants/constants.go` 中添加订单超时时间常量 `OrderExpireTimeout = 30 * time.Minute`
|
||||
- [ ] 2.2 在 `pkg/constants/constants.go` 中添加任务类型常量 `TaskTypeOrderExpire = "order:expire"`
|
||||
- [ ] 2.3 在 `pkg/constants/constants.go` 中添加批量处理数量常量 `OrderExpireBatchSize = 100`
|
||||
- [ ] 2.4 验证编译:运行 `go build ./...` 确认无编译错误
|
||||
|
||||
## 3. Model 层修改
|
||||
|
||||
- [ ] 3.1 在 `internal/model/order.go` 中的 `Order` 结构体添加 `ExpiresAt *time.Time` 字段(指针类型,支持 NULL)
|
||||
- [ ] 3.2 在 `internal/model/dto/order_dto.go` 中的 `OrderResponse` 添加 `ExpiresAt *time.Time` 和 `IsExpired bool` 字段
|
||||
- [ ] 3.3 验证编译:运行 `go build ./internal/model/...` 确认无编译错误
|
||||
|
||||
## 4. Store 层新增方法
|
||||
|
||||
- [ ] 4.1 在 `internal/store/postgres/order_store.go` 添加 `FindExpiredOrders(ctx, limit int) ([]*model.Order, error)` 方法:查询 `expires_at <= NOW() AND payment_status = 1` 的订单
|
||||
- [ ] 4.2 在 `internal/store/postgres/order_store.go` 的 `UpdatePaymentStatus()` 方法中添加 `expiresAt *time.Time` 参数,支持更新过期时间
|
||||
- [ ] 4.3 验证编译:运行 `go build ./internal/store/...` 确认无编译错误
|
||||
- [ ] 4.4 使用 PostgreSQL MCP 工具验证查询:执行 `FindExpiredOrders` 的 SQL,确认索引使用正确且查询耗时 < 50ms
|
||||
|
||||
## 5. Service 层修改 - 订单创建
|
||||
|
||||
- [ ] 5.1 修改 `internal/service/order/service.go` 的 `Create()` 方法:待支付订单设置 `expires_at = now + 30min`
|
||||
- [ ] 5.2 修改 `Create()` 方法:后台钱包一步支付订单和线下支付订单 `expires_at = nil`
|
||||
- [ ] 5.3 验证编译:运行 `go build ./internal/service/order/...` 确认无编译错误
|
||||
|
||||
## 6. Service 层修改 - 订单取消和钱包解冻
|
||||
|
||||
- [ ] 6.1 修改 `internal/service/order/service.go` 的 `Cancel()` 方法:添加钱包解冻逻辑(判断支付方式,计算解冻金额)
|
||||
- [ ] 6.2 在 `Cancel()` 方法中添加事务处理:订单状态更新(`payment_status = 3`, `expires_at = nil`)和钱包解冻在同一事务
|
||||
- [ ] 6.3 在 `Cancel()` 方法中添加解冻规则判断逻辑:钱包支付(H5)、混合支付需解冻,纯在线支付不解冻
|
||||
- [ ] 6.4 验证编译:运行 `go build ./internal/service/order/...` 确认无编译错误
|
||||
|
||||
## 7. Service 层新增方法 - 批量取消超时订单
|
||||
|
||||
- [ ] 7.1 在 `internal/service/order/service.go` 添加 `CancelExpiredOrders(ctx context.Context) (int, error)` 方法
|
||||
- [ ] 7.2 实现 `CancelExpiredOrders()` 逻辑:调用 `FindExpiredOrders()` 查询超时订单(最多 100 条)
|
||||
- [ ] 7.3 实现批量取消逻辑:遍历订单,调用 `Cancel()` 方法(复用钱包解冻逻辑)
|
||||
- [ ] 7.4 添加日志记录:处理订单数量、解冻钱包次数、执行耗时
|
||||
- [ ] 7.5 验证编译:运行 `go build ./internal/service/order/...` 确认无编译错误
|
||||
|
||||
## 8. Service 层修改 - 支付成功清除过期时间
|
||||
|
||||
- [ ] 8.1 修改 `internal/service/order/service.go` 的 `WalletPay()` 方法:调用 `UpdatePaymentStatus()` 时传入 `expiresAt = nil`
|
||||
- [ ] 8.2 修改 `HandlePaymentCallback()` 方法:调用 `UpdatePaymentStatus()` 时传入 `expiresAt = nil`
|
||||
- [ ] 8.3 验证编译:运行 `go build ./internal/service/order/...` 确认无编译错误
|
||||
|
||||
## 9. Task 层新增定时任务
|
||||
|
||||
- [ ] 9.1 创建 `internal/task/order_expire.go` 文件,定义 `OrderExpireHandler` 结构体
|
||||
- [ ] 9.2 实现 `NewOrderExpireHandler()` 构造函数,依赖注入 `db`, `orderService`, `logger`
|
||||
- [ ] 9.3 实现 `HandleOrderExpire(ctx context.Context, task *asynq.Task) error` 方法,调用 `orderService.CancelExpiredOrders()`
|
||||
- [ ] 9.4 添加错误处理和重试逻辑:可重试错误返回 `err`,不可重试错误返回 `asynq.SkipRetry`
|
||||
- [ ] 9.5 添加日志记录:任务开始、成功处理订单数、失败错误
|
||||
- [ ] 9.6 验证编译:运行 `go build ./internal/task/...` 确认无编译错误
|
||||
|
||||
## 10. Worker 注册定时任务 Handler
|
||||
|
||||
- [ ] 10.1 在 `pkg/queue/handler.go` 的 `RegisterHandlers()` 方法中调用 `registerOrderExpireHandler()`
|
||||
- [ ] 10.2 实现 `registerOrderExpireHandler()` 方法:创建 `OrderExpireHandler` 并注册到 `mux.HandleFunc(constants.TaskTypeOrderExpire, ...)`
|
||||
- [ ] 10.3 验证编译:运行 `go build ./pkg/queue/...` 确认无编译错误
|
||||
|
||||
## 11. Worker 创建和启动 Asynq Scheduler
|
||||
|
||||
- [ ] 11.1 在 `cmd/worker/main.go` 中创建 Asynq Scheduler 实例:`asynq.NewScheduler(redisOpt, &asynq.SchedulerOpts{Location: time.Local})`
|
||||
- [ ] 11.2 注册订单超时周期任务:`scheduler.Register("@every 1m", asynq.NewTask(constants.TaskTypeOrderExpire, nil), asynq.Queue(constants.QueueDefault))`
|
||||
- [ ] 11.3 启动 Scheduler:`scheduler.Start()`,并在 defer 中调用 `scheduler.Shutdown()`
|
||||
- [ ] 11.4 验证编译:运行 `go build ./cmd/worker/...` 确认无编译错误
|
||||
|
||||
## 12. Handler 层修改 - DTO 响应
|
||||
|
||||
- [ ] 12.1 修改 `internal/handler/admin/order.go` 和 `internal/handler/h5/order.go` 的订单响应构建逻辑:添加 `ExpiresAt` 字段
|
||||
- [ ] 12.2 实现 `IsExpired` 动态计算逻辑:`if expiresAt != nil && paymentStatus == 1 { isExpired = now.After(expiresAt) }`
|
||||
- [ ] 12.3 验证编译:运行 `go build ./internal/handler/...` 确认无编译错误
|
||||
|
||||
## 13. Handler 层修改 - 查询过期状态
|
||||
|
||||
- [ ] 13.1 修改 `internal/model/dto/order_dto.go` 的 `ListOrderRequest` 添加 `IsExpired *bool` 查询参数(可选)
|
||||
- [ ] 13.2 修改 `internal/store/postgres/order_store.go` 的 `List()` 方法:添加过期状态筛选条件(`is_expired = true` 映射为 `expires_at <= NOW() AND payment_status = 1`)
|
||||
- [ ] 12.3 验证编译:运行 `go build ./...` 确认无编译错误
|
||||
|
||||
## 14. 功能验证 - 订单创建
|
||||
|
||||
- [ ] 14.1 启动 API 服务,使用 Postman/curl 创建待支付订单(H5 端,支付方式 wechat),验证 `expires_at` 字段设置正确(约 `now + 30min`)
|
||||
- [ ] 14.2 使用 PostgreSQL MCP 工具查询订单:`SELECT id, expires_at, payment_status FROM tb_order WHERE id = ?`,确认 `expires_at` 不为 NULL
|
||||
- [ ] 14.3 创建后台钱包支付订单,验证 `expires_at` 为 NULL(订单立即支付成功)
|
||||
|
||||
## 15. 功能验证 - 订单取消和钱包解冻
|
||||
|
||||
- [ ] 15.1 创建混合支付待支付订单(钱包预扣 2000 分),使用 PostgreSQL MCP 查询钱包冻结余额
|
||||
- [ ] 15.2 调用取消订单 API,验证订单状态变更为已取消(`payment_status = 3`),`expires_at` 变更为 NULL
|
||||
- [ ] 15.3 使用 PostgreSQL MCP 查询钱包:确认冻结余额减少 2000 分
|
||||
- [ ] 15.4 创建纯在线支付订单(wechat),取消订单,确认不执行钱包解冻操作
|
||||
|
||||
## 16. 功能验证 - 支付成功清除过期时间
|
||||
|
||||
- [ ] 16.1 创建待支付订单(wechat),确认 `expires_at` 不为 NULL
|
||||
- [ ] 16.2 模拟第三方支付回调成功,验证订单状态变更为已支付(`payment_status = 2`),`expires_at` 变更为 NULL
|
||||
- [ ] 16.3 使用 PostgreSQL MCP 查询订单:`SELECT id, expires_at, payment_status FROM tb_order WHERE id = ?`,确认 `expires_at` 为 NULL
|
||||
|
||||
## 17. 功能验证 - 定时任务自动取消
|
||||
|
||||
- [ ] 17.1 使用 PostgreSQL MCP 手动修改订单的 `expires_at` 为过去时间:`UPDATE tb_order SET expires_at = NOW() - INTERVAL '1 minute' WHERE id = ?`
|
||||
- [ ] 17.2 启动 Worker 服务,等待 1 分钟后检查日志,确认定时任务执行成功
|
||||
- [ ] 17.3 使用 PostgreSQL MCP 查询订单:确认订单状态变更为已取消,`expires_at` 变更为 NULL
|
||||
- [ ] 17.4 如果是混合支付订单,使用 PostgreSQL MCP 查询钱包:确认冻结余额解冻
|
||||
|
||||
## 18. 功能验证 - 查询过期状态
|
||||
|
||||
- [ ] 18.1 使用 Postman/curl 调用订单列表 API,筛选 `is_expired = true`,验证返回已过期的待支付订单
|
||||
- [ ] 18.2 调用订单列表 API,筛选 `is_expired = false`,验证返回未过期的待支付订单
|
||||
- [ ] 18.3 调用订单详情 API,验证响应包含 `is_expired` 字段且计算正确
|
||||
|
||||
## 19. 性能验证
|
||||
|
||||
- [ ] 19.1 使用 PostgreSQL MCP 的 `explain_query` 工具分析 `FindExpiredOrders` 查询:确认使用 `idx_order_expires` 索引
|
||||
- [ ] 19.2 验证查询耗时:在订单数量 > 10000 的情况下,查询耗时 < 50ms
|
||||
- [ ] 19.3 验证定时任务处理耗时:单批次处理 100 条订单,总耗时 < 5s
|
||||
- [ ] 19.4 使用 PostgreSQL MCP 检查数据库连接池状态:确认无连接池阻塞
|
||||
|
||||
## 20. 错误处理验证
|
||||
|
||||
- [ ] 20.1 模拟数据库连接失败场景:确认定时任务返回可重试错误,Asynq 自动重试
|
||||
- [ ] 20.2 模拟钱包不存在场景:确认订单取消失败,事务回滚,订单状态不变
|
||||
- [ ] 20.3 模拟冻结余额不足场景:确认订单取消失败,事务回滚,记录错误日志
|
||||
- [ ] 20.4 检查日志:确认所有错误场景都记录了详细日志(包含订单 ID、错误原因)
|
||||
|
||||
## 21. 代码质量检查
|
||||
|
||||
- [ ] 21.1 运行 `gofmt -s -w .` 格式化代码
|
||||
- [ ] 21.2 运行 `go vet ./...` 检查代码问题
|
||||
- [ ] 21.3 运行 `go build ./...` 确认全部编译通过
|
||||
- [ ] 21.4 检查所有新增代码的中文注释:确认符合注释规范(导出符号有文档注释,复杂逻辑有实现注释)
|
||||
|
||||
## 22. 文档更新
|
||||
|
||||
- [ ] 22.1 创建功能总结文档 `docs/order-expiration/功能总结.md`:说明超时机制、钱包解冻、查询过期状态
|
||||
- [ ] 22.2 更新 `README.md`:在"已实现功能"部分添加"订单超时自动失效"
|
||||
- [ ] 22.3 更新 `openspec/specs/iot-order/spec.md`:同步 delta spec 到主规格文档(归档后)
|
||||
- [ ] 22.4 更新 `openspec/specs/order-payment/spec.md`:同步 delta spec 到主规格文档(归档后)
|
||||
|
||||
## 23. 最终验证
|
||||
|
||||
- [ ] 23.1 在开发环境完整测试一次完整流程:创建订单 → 超时自动取消 → 钱包解冻
|
||||
- [ ] 23.2 检查所有日志输出:确认日志级别正确(Info/Error),日志内容完整
|
||||
- [ ] 23.3 检查数据库:确认无脏数据(如订单已取消但钱包未解冻)
|
||||
- [ ] 23.4 使用 Postman 导出 API 测试用例集(包含订单创建、取消、查询过期状态)
|
||||
|
||||
## 24. 重构现有定时任务为 Asynq Scheduler
|
||||
|
||||
- [ ] 24.1 在 `pkg/constants/constants.go` 中添加告警检查任务类型常量 `TaskTypeAlertCheck = "alert:check"`
|
||||
- [ ] 24.2 在 `pkg/constants/constants.go` 中添加数据清理任务类型常量 `TaskTypeDataCleanup = "data:cleanup"`
|
||||
- [ ] 24.3 创建 `internal/task/alert_check.go` 文件,定义 `AlertCheckHandler` 结构体
|
||||
- [ ] 24.4 实现 `NewAlertCheckHandler()` 构造函数,依赖注入 `alertService`, `logger`
|
||||
- [ ] 24.5 实现 `HandleAlertCheck(ctx context.Context, task *asynq.Task) error` 方法,调用 `alertService.CheckAlerts()`
|
||||
- [ ] 24.6 创建 `internal/task/data_cleanup.go` 文件,定义 `DataCleanupHandler` 结构体
|
||||
- [ ] 24.7 实现 `NewDataCleanupHandler()` 构造函数,依赖注入 `cleanupService`, `logger`
|
||||
- [ ] 24.8 实现 `HandleDataCleanup(ctx context.Context, task *asynq.Task) error` 方法,调用 `cleanupService.RunScheduledCleanup()`
|
||||
- [ ] 24.9 在 `pkg/queue/handler.go` 的 `RegisterHandlers()` 方法中调用 `registerAlertCheckHandler()`
|
||||
- [ ] 24.10 实现 `registerAlertCheckHandler()` 方法:创建 `AlertCheckHandler` 并注册到 `mux.HandleFunc(constants.TaskTypeAlertCheck, ...)`
|
||||
- [ ] 24.11 在 `pkg/queue/handler.go` 的 `RegisterHandlers()` 方法中调用 `registerDataCleanupHandler()`
|
||||
- [ ] 24.12 实现 `registerDataCleanupHandler()` 方法:创建 `DataCleanupHandler` 并注册到 `mux.HandleFunc(constants.TaskTypeDataCleanup, ...)`
|
||||
- [ ] 24.13 在 `cmd/worker/main.go` 的 Asynq Scheduler 中注册告警检查周期任务:`scheduler.Register("@every 1m", asynq.NewTask(constants.TaskTypeAlertCheck, nil))`
|
||||
- [ ] 24.14 在 `cmd/worker/main.go` 的 Asynq Scheduler 中注册数据清理周期任务:`scheduler.Register("0 2 * * *", asynq.NewTask(constants.TaskTypeDataCleanup, nil))` (每天凌晨2点)
|
||||
- [ ] 24.15 移除 `cmd/worker/main.go` 中的 `startAlertChecker` 函数定义(第 239-265 行)
|
||||
- [ ] 24.16 移除 `cmd/worker/main.go` 中的 `startCleanupScheduler` 函数定义(第 267-303 行)
|
||||
- [ ] 24.17 移除 `cmd/worker/main.go` 中对 `startAlertChecker` 和 `startCleanupScheduler` 的调用和相关代码
|
||||
- [ ] 24.18 验证编译:运行 `go build ./cmd/worker/...` 确认无编译错误
|
||||
- [ ] 24.19 验证编译:运行 `go build ./internal/task/...` 确认无编译错误
|
||||
- [ ] 24.20 验证编译:运行 `go build ./pkg/queue/...` 确认无编译错误
|
||||
|
||||
## 25. 提交和归档
|
||||
|
||||
- [ ] 25.1 使用 `/commit` 创建 Git commit,提交消息:"实现订单超时自动失效机制并重构定时任务为 Asynq Scheduler"
|
||||
- [ ] 25.2 使用 `/opsx:verify` 验证实现与规格一致
|
||||
- [ ] 25.3 使用 `/opsx:archive` 归档变更,同步 delta specs 到主规格文档
|
||||
- [ ] 25.4 确认归档后 `openspec/specs/iot-order/spec.md` 和 `openspec/specs/order-payment/spec.md` 已更新
|
||||
Reference in New Issue
Block a user