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

142 lines
4.1 KiB
Go
Executable File
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 {
// 统一回退逻辑:无论券种,统统将预扣金额加回余额,并重置状态为 1 (未使用/有余额)
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 = ? AND status = 4
`, cr.AppliedAmount, couponID)
if res.RowsAffected > 0 {
// 记录流水
s.writeDB.UserCouponLedger.WithContext(ctx).Create(&model.UserCouponLedger{
UserID: userID,
UserCouponID: couponID,
ChangeAmount: cr.AppliedAmount,
BalanceAfter: 0, // 异步流水无法实时算最新,标记 0 或查询后填入,这里暂保持 Action
OrderID: orderID,
Action: "timeout_refund",
CreatedAt: time.Now(),
})
}
}
}
// 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)
}
}