120 lines
3.2 KiB
Go
120 lines
3.2 KiB
Go
package user
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"bindbox-game/internal/pkg/logger"
|
||
"bindbox-game/internal/repository/mysql"
|
||
"bindbox-game/internal/repository/mysql/dao"
|
||
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
const (
|
||
autoCancelInterval = 30 * time.Second // 每 30 秒扫描一次
|
||
autoCancelThreshold = 15 * time.Minute // 超过 15 分钟未支付则取消
|
||
autoCancelReason = "auto_cancel_timeout"
|
||
autoCancelBatchSize = 100 // 每次最多处理 100 条,避免一次性处理过多
|
||
)
|
||
|
||
// StartAutoCancelWorker 启动订单自动取消后台任务(包级别函数,与 StartExpirationCheck 风格一致)
|
||
func StartAutoCancelWorker(l logger.CustomLogger, repo mysql.Repo) {
|
||
svc := &service{
|
||
logger: l,
|
||
readDB: dao.Use(repo.GetDbR()),
|
||
writeDB: dao.Use(repo.GetDbW()),
|
||
repo: repo,
|
||
}
|
||
go svc.runAutoCancelLoop(context.Background())
|
||
}
|
||
|
||
// StartAutoCancelWorker 也作为 service 的方法提供,以满足 Service interface
|
||
func (s *service) StartAutoCancelWorker(ctx context.Context) {
|
||
go s.runAutoCancelLoop(ctx)
|
||
}
|
||
|
||
func (s *service) runAutoCancelLoop(ctx context.Context) {
|
||
ticker := time.NewTicker(autoCancelInterval)
|
||
defer ticker.Stop()
|
||
|
||
s.logger.Info("[AutoCancel] 订单自动取消 worker 已启动",
|
||
zap.Duration("interval", autoCancelInterval),
|
||
zap.Duration("threshold", autoCancelThreshold),
|
||
)
|
||
|
||
// 启动后立即执行一次,不等第一个 tick
|
||
s.scanAndCancelExpiredOrders(ctx)
|
||
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
s.logger.Info("[AutoCancel] 订单自动取消 worker 已停止")
|
||
return
|
||
case <-ticker.C:
|
||
s.scanAndCancelExpiredOrders(ctx)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *service) scanAndCancelExpiredOrders(ctx context.Context) {
|
||
deadline := time.Now().Add(-autoCancelThreshold)
|
||
|
||
// 查询 status=1(待支付)且 created_at < now-15min 的订单
|
||
// 使用主库(writeDB)避免主从延迟导致漏扫刚写入的超时订单
|
||
// ORDER BY created_at ASC 保证最老订单优先处理,避免新订单持续涌入时老订单被跳过
|
||
orders, err := s.writeDB.Orders.WithContext(ctx).
|
||
Where(
|
||
s.writeDB.Orders.Status.Eq(1),
|
||
s.writeDB.Orders.CreatedAt.Lt(deadline),
|
||
).
|
||
Order(s.writeDB.Orders.CreatedAt).
|
||
Limit(autoCancelBatchSize).
|
||
Find()
|
||
|
||
if err != nil {
|
||
s.logger.Error("[AutoCancel] 查询超时订单失败", zap.Error(err))
|
||
return
|
||
}
|
||
|
||
if len(orders) == 0 {
|
||
return
|
||
}
|
||
|
||
s.logger.Info("[AutoCancel] 发现超时待支付订单,开始逐条取消",
|
||
zap.Int("count", len(orders)),
|
||
zap.Time("deadline", deadline),
|
||
)
|
||
|
||
successCount := 0
|
||
failCount := 0
|
||
|
||
for _, order := range orders {
|
||
_, err := s.CancelOrder(ctx, order.UserID, order.ID, autoCancelReason)
|
||
if err != nil {
|
||
// 单条失败不影响其他订单,记录错误继续
|
||
s.logger.Error("[AutoCancel] 取消订单失败",
|
||
zap.Int64("order_id", order.ID),
|
||
zap.Int64("user_id", order.UserID),
|
||
zap.String("order_no", order.OrderNo),
|
||
zap.Error(err),
|
||
)
|
||
failCount++
|
||
continue
|
||
}
|
||
|
||
s.logger.Info("[AutoCancel] 订单已自动取消",
|
||
zap.Int64("order_id", order.ID),
|
||
zap.Int64("user_id", order.UserID),
|
||
zap.String("order_no", order.OrderNo),
|
||
zap.Time("created_at", order.CreatedAt),
|
||
)
|
||
successCount++
|
||
}
|
||
|
||
s.logger.Info("[AutoCancel] 本轮取消完成",
|
||
zap.Int("success", successCount),
|
||
zap.Int("failed", failCount),
|
||
)
|
||
}
|