feat(v2): reconcile Runner 骨架 + 订单级过期清理(超 TTL pending 单/零尝试孤儿单自动关闭)
This commit is contained in:
@@ -3,6 +3,7 @@ package store
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -94,3 +95,34 @@ func (s *OrderStore) ApplyRefundToOrder(outTradeNo string, fully bool) (bool, er
|
||||
}
|
||||
return res.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
// ExpireStaleOrders closes pending orders whose created_at predates cutoff
|
||||
// (TTL 到期未付),条件 UPDATE 只翻 status=pending 的行——与并发 settle 翻 paid
|
||||
// 互斥(谁先谁赢,另一方 RowsAffected=0),故幂等且崩溃安全。含"零尝试孤儿单"
|
||||
// (建单后 CreateAttempt 失败、无 attempt 的 pending 单):它 status 仍是 pending,
|
||||
// 同样被扫到关闭(P2-T4 复审记录的缺口)。cutoff 由调用方用注入时钟算,store 不碰时钟。
|
||||
//
|
||||
// 注:与 ExpirePendingAttempts 语义不同——那个是 attempt 级(标 attempt expired,
|
||||
// 订单不动,retry 前用);这个是 order 级(标整张订单 expired)。
|
||||
func (s *OrderStore) ExpireStaleOrders(cutoff time.Time, limit int) (int64, error) {
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 500
|
||||
}
|
||||
// 先选主键再批量 UPDATE:回避 "UPDATE ... ORDER BY LIMIT" 的方言差异(sqlite/mysql)。
|
||||
var ids []uint64
|
||||
if err := s.db.Model(&model.OrderV2{}).
|
||||
Where("status = ? AND created_at < ?", model.OrderPendingV2, cutoff).
|
||||
Order("id ASC").Limit(limit).Pluck("id", &ids).Error; err != nil {
|
||||
return 0, fmt.Errorf("store.ExpireStaleOrders select: %w", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
res := s.db.Model(&model.OrderV2{}).
|
||||
Where("id IN ? AND status = ?", ids, model.OrderPendingV2). // status 守卫兜住 select→update 间的并发翻转
|
||||
Update("status", model.OrderExpiredV2)
|
||||
if res.Error != nil {
|
||||
return 0, fmt.Errorf("store.ExpireStaleOrders update: %w", res.Error)
|
||||
}
|
||||
return res.RowsAffected, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user