142 lines
4.1 KiB
Go
Executable File
142 lines
4.1 KiB
Go
Executable File
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)
|
||
}
|
||
}
|