feat: 实现订单超时自动取消功能,支持钱包余额解冻和 Asynq Scheduler 统一调度
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m58s

- 新增 expires_at 字段和复合索引,待支付订单 30 分钟超时自动取消
- 实现 cancelOrder/unfreezeWalletForCancel 钱包余额解冻逻辑
- 创建 Asynq 定时任务(order_expire/alert_check/data_cleanup)
- 将原有 time.Ticker 轮询迁移至 Asynq Scheduler 统一调度
- 同步 delta specs 到 main specs 并归档变更
This commit is contained in:
2026-02-28 17:16:15 +08:00
parent 5bb0ff0ddf
commit e661b59bb9
35 changed files with 1157 additions and 314 deletions

View File

@@ -133,6 +133,16 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
if v, ok := filters["end_time"]; ok {
query = query.Where("created_at <= ?", v)
}
if v, ok := filters["is_expired"]; ok {
isExpired, _ := v.(bool)
if isExpired {
// 已过期expires_at 不为空且小于当前时间,且订单仍为待支付状态
query = query.Where("expires_at IS NOT NULL AND expires_at <= NOW() AND payment_status = ?", model.PaymentStatusPending)
} else {
// 未过期expires_at 为空或 expires_at 大于当前时间
query = query.Where("expires_at IS NULL OR expires_at > NOW()")
}
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
@@ -156,13 +166,17 @@ func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters
return orders, total, nil
}
func (s *OrderStore) UpdatePaymentStatus(ctx context.Context, id uint, status int, paidAt *time.Time) error {
func (s *OrderStore) UpdatePaymentStatus(ctx context.Context, id uint, status int, paidAt *time.Time, expiresAt ...*time.Time) error {
updates := map[string]any{
"payment_status": status,
}
if paidAt != nil {
updates["paid_at"] = paidAt
}
// 支持可选的 expiresAt 参数,用于支付成功后清除过期时间或取消时清除过期时间
if len(expiresAt) > 0 {
updates["expires_at"] = expiresAt[0]
}
return s.db.WithContext(ctx).Model(&model.Order{}).Where("id = ?", id).Updates(updates).Error
}
@@ -171,3 +185,19 @@ func (s *OrderStore) GenerateOrderNo() string {
randomNum := rand.Intn(1000000)
return fmt.Sprintf("ORD%s%06d", now.Format("20060102150405"), randomNum)
}
// FindExpiredOrders 查询已超时的待支付订单
// 查询条件expires_at <= NOW() AND payment_status = 1待支付
// limit 参数限制每次批量处理的数量,避免一次性加载太多数据
func (s *OrderStore) FindExpiredOrders(ctx context.Context, limit int) ([]*model.Order, error) {
var orders []*model.Order
err := s.db.WithContext(ctx).
Where("expires_at IS NOT NULL AND expires_at <= NOW() AND payment_status = ?", model.PaymentStatusPending).
Order("expires_at ASC").
Limit(limit).
Find(&orders).Error
if err != nil {
return nil, err
}
return orders, nil
}