package user import ( "context" "fmt" "time" "bindbox-game/internal/repository/mysql/model" "gorm.io/gorm/clause" ) // StartOrderTimeoutTask 启动订单超时清理任务(每分钟执行) func (s *service) StartOrderTimeoutTask(ctx context.Context) { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: s.cleanupExpiredOrders() } } } // cleanupExpiredOrders 清理超时未支付的订单 // 规则:待支付订单超过30分钟自动取消,并恢复已预扣的优惠券 func (s *service) cleanupExpiredOrders() { ctx := context.Background() cutoff := time.Now().Add(-30 * time.Minute) // 查找所有超时的待支付订单(排除一番赏订单,因为有专门的清理任务) var expiredOrders []struct { ID int64 UserID int64 CouponID int64 OrderNo string PointsAmount int64 } err := s.readDB.Orders.WithContext(ctx).UnderlyingDB().Raw(` SELECT id, user_id, coupon_id, order_no, points_amount FROM orders WHERE status = 1 AND created_at < ? AND source_type != 3 LIMIT 100 `, cutoff).Scan(&expiredOrders).Error if err != nil { fmt.Printf("OrderTimeoutTask: 查询超时订单失败: %v\n", err) return } for _, order := range expiredOrders { s.cancelExpiredOrder(ctx, order.ID, order.UserID, order.CouponID, order.PointsAmount) } if len(expiredOrders) > 0 { fmt.Printf("OrderTimeoutTask: 已处理 %d 个超时订单\n", len(expiredOrders)) } } // cancelExpiredOrder 取消超时订单并恢复优惠券 func (s *service) cancelExpiredOrder(ctx context.Context, orderID int64, userID int64, couponID int64, pointsAmount int64) { // 1. 恢复优惠券 if couponID > 0 { type couponRow struct { AppliedAmount int64 DiscountType int32 } var cr couponRow s.readDB.OrderCoupons.WithContext(ctx).UnderlyingDB().Raw(` SELECT oc.applied_amount, sc.discount_type FROM order_coupons oc JOIN user_coupons uc ON uc.id = oc.user_coupon_id JOIN system_coupons sc ON sc.id = uc.coupon_id WHERE oc.order_id = ? AND oc.user_coupon_id = ? `, orderID, couponID).Scan(&cr) if cr.AppliedAmount > 0 { if cr.DiscountType == 1 { // 金额券:恢复余额 // 此时 coupon 可能 status=1 (Active) 或 2 (Used/Exhausted) // 不需要 status=4 限制,因为下单时已直接扣减 res := s.writeDB.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(` UPDATE user_coupons SET balance_amount = balance_amount + ?, status = 1, used_order_id = NULL, used_at = NULL WHERE id = ? `, cr.AppliedAmount, couponID) if res.RowsAffected > 0 { // 记录流水 s.writeDB.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{ UserID: userID, UserCouponID: couponID, ChangeAmount: cr.AppliedAmount, OrderID: orderID, Action: "timeout_refund", CreatedAt: time.Now(), }) } } else { // 满减/折扣券:恢复状态 s.writeDB.UserCoupons.WithContext(ctx).UnderlyingDB().Exec(` UPDATE user_coupons SET status = 1, used_order_id = NULL, used_at = NULL WHERE id = ? AND status = 4 `, couponID) } } } // 2. 退还积分(如有) if pointsAmount > 0 { existing, _ := s.writeDB.UserPoints.WithContext(ctx). Clauses(clause.Locking{Strength: "UPDATE"}). Where(s.writeDB.UserPoints.UserID.Eq(userID)).First() if existing != nil { s.writeDB.UserPoints.WithContext(ctx). Where(s.writeDB.UserPoints.ID.Eq(existing.ID)). Updates(map[string]any{"points": existing.Points + pointsAmount}) } else { s.writeDB.UserPoints.WithContext(ctx). Omit(s.writeDB.UserPoints.ValidEnd). Create(&model.UserPoints{UserID: userID, Points: pointsAmount}) } s.writeDB.UserPointsLedger.WithContext(ctx).Create(&model.UserPointsLedger{ UserID: userID, Action: "timeout_refund", Points: pointsAmount, RefTable: "orders", RefID: fmt.Sprintf("%d", orderID), Remark: "order_timeout", }) } // 3. 更新订单状态为已取消 res := s.writeDB.Orders.WithContext(ctx).UnderlyingDB().Exec(` UPDATE orders SET status = 3, cancelled_at = NOW() WHERE id = ? AND status = 1 `, orderID) if res.RowsAffected > 0 { fmt.Printf("OrderTimeoutTask: 订单 %d 已超时取消\n", orderID) } }