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