bindbox-game/internal/service/user/order_timeout.go
2026-01-27 01:33:32 +08:00

154 lines
4.3 KiB
Go
Raw 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"
"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)
}
}