bindbox-game/internal/service/user/orders_auto_cancel_worker.go

120 lines
3.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
)
}