Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d9263ceef |
+14
-14
@@ -38,20 +38,20 @@ type (
|
|||||||
Id int64 `form:"id" validate:"required"`
|
Id int64 `form:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
UpdateUserBasiceInfoRequest {
|
UpdateUserBasiceInfoRequest {
|
||||||
UserId int64 `json:"user_id" validate:"required"`
|
UserId int64 `json:"user_id" validate:"required"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Balance int64 `json:"balance"`
|
Balance *int64 `json:"balance"`
|
||||||
Commission int64 `json:"commission"`
|
Commission *int64 `json:"commission"`
|
||||||
ReferralPercentage uint8 `json:"referral_percentage"`
|
ReferralPercentage uint8 `json:"referral_percentage"`
|
||||||
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
||||||
GiftAmount int64 `json:"gift_amount"`
|
GiftAmount *int64 `json:"gift_amount"`
|
||||||
Telegram int64 `json:"telegram"`
|
Telegram int64 `json:"telegram"`
|
||||||
ReferCode string `json:"refer_code"`
|
ReferCode string `json:"refer_code"`
|
||||||
RefererId int64 `json:"referer_id"`
|
RefererId *int64 `json:"referer_id"`
|
||||||
Enable *bool `json:"enable"`
|
Enable *bool `json:"enable"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
UpdateUserNotifySettingRequest {
|
UpdateUserNotifySettingRequest {
|
||||||
UserId int64 `json:"user_id" validate:"required"`
|
UserId int64 `json:"user_id" validate:"required"`
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||||
if userInfo.Balance != req.Balance {
|
if req.Balance != nil && userInfo.Balance != *req.Balance {
|
||||||
change := req.Balance - userInfo.Balance
|
change := *req.Balance - userInfo.Balance
|
||||||
balanceLog := log.Balance{
|
balanceLog := log.Balance{
|
||||||
Type: log.BalanceTypeAdjust,
|
Type: log.BalanceTypeAdjust,
|
||||||
Amount: change,
|
Amount: change,
|
||||||
OrderNo: "",
|
OrderNo: "",
|
||||||
Balance: req.Balance,
|
Balance: *req.Balance,
|
||||||
Timestamp: time.Now().UnixMilli(),
|
Timestamp: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
content, _ := balanceLog.Marshal()
|
content, _ := balanceLog.Marshal()
|
||||||
@@ -66,14 +66,14 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInfo.Balance = req.Balance
|
userInfo.Balance = *req.Balance
|
||||||
}
|
}
|
||||||
|
|
||||||
if userInfo.GiftAmount != req.GiftAmount {
|
if req.GiftAmount != nil && userInfo.GiftAmount != *req.GiftAmount {
|
||||||
change := req.GiftAmount - userInfo.GiftAmount
|
change := *req.GiftAmount - userInfo.GiftAmount
|
||||||
if change != 0 {
|
if change != 0 {
|
||||||
var changeType uint16
|
var changeType uint16
|
||||||
if userInfo.GiftAmount < req.GiftAmount {
|
if userInfo.GiftAmount < *req.GiftAmount {
|
||||||
changeType = log.GiftTypeIncrease
|
changeType = log.GiftTypeIncrease
|
||||||
} else {
|
} else {
|
||||||
changeType = log.GiftTypeReduce
|
changeType = log.GiftTypeReduce
|
||||||
@@ -81,7 +81,7 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
giftLog := log.Gift{
|
giftLog := log.Gift{
|
||||||
Type: changeType,
|
Type: changeType,
|
||||||
Amount: change,
|
Amount: change,
|
||||||
Balance: req.GiftAmount,
|
Balance: *req.GiftAmount,
|
||||||
Remark: "Admin adjustment",
|
Remark: "Admin adjustment",
|
||||||
Timestamp: time.Now().UnixMilli(),
|
Timestamp: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
@@ -96,23 +96,23 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInfo.GiftAmount = req.GiftAmount
|
userInfo.GiftAmount = *req.GiftAmount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Commission != userInfo.Commission {
|
if req.Commission != nil && *req.Commission != userInfo.Commission {
|
||||||
if isWithdrawalScene(req.Remark) {
|
if isWithdrawalScene(req.Remark) {
|
||||||
logWithdrawalGuard(l.Logger, userInfo.Id)
|
logWithdrawalGuard(l.Logger, userInfo.Id)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "commission overwrite is blocked in withdrawal scene")
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "commission overwrite is blocked in withdrawal scene")
|
||||||
}
|
}
|
||||||
change := req.Commission - userInfo.Commission
|
change := *req.Commission - userInfo.Commission
|
||||||
if err = l.svcCtx.UserModel.UpdateCommission(l.ctx, userInfo.Id, change, tx); err != nil {
|
if err = l.svcCtx.UserModel.UpdateCommission(l.ctx, userInfo.Id, change, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = logicCommon.WriteCommissionLog(tx, userInfo.Id, log.CommissionTypeAdjust, change, ""); err != nil {
|
if err = logicCommon.WriteCommissionLog(tx, userInfo.Id, log.CommissionTypeAdjust, change, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInfo.Commission = req.Commission
|
userInfo.Commission = *req.Commission
|
||||||
}
|
}
|
||||||
if req.Avatar != "" {
|
if req.Avatar != "" {
|
||||||
userInfo.Avatar = req.Avatar
|
userInfo.Avatar = req.Avatar
|
||||||
@@ -120,7 +120,9 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
if req.ReferCode != "" {
|
if req.ReferCode != "" {
|
||||||
userInfo.ReferCode = req.ReferCode
|
userInfo.ReferCode = req.ReferCode
|
||||||
}
|
}
|
||||||
userInfo.RefererId = req.RefererId
|
if req.RefererId != nil {
|
||||||
|
userInfo.RefererId = *req.RefererId
|
||||||
|
}
|
||||||
if req.Enable != nil {
|
if req.Enable != nil {
|
||||||
userInfo.Enable = req.Enable
|
userInfo.Enable = req.Enable
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -3223,20 +3223,20 @@ type UpdateUserAuthMethodRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserBasiceInfoRequest struct {
|
type UpdateUserBasiceInfoRequest struct {
|
||||||
UserId int64 `json:"user_id" validate:"required"`
|
UserId int64 `json:"user_id" validate:"required"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Balance int64 `json:"balance"`
|
Balance *int64 `json:"balance"`
|
||||||
Commission int64 `json:"commission"`
|
Commission *int64 `json:"commission"`
|
||||||
ReferralPercentage uint8 `json:"referral_percentage"`
|
ReferralPercentage uint8 `json:"referral_percentage"`
|
||||||
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
||||||
GiftAmount int64 `json:"gift_amount"`
|
GiftAmount *int64 `json:"gift_amount"`
|
||||||
Telegram int64 `json:"telegram"`
|
Telegram int64 `json:"telegram"`
|
||||||
ReferCode string `json:"refer_code"`
|
ReferCode string `json:"refer_code"`
|
||||||
RefererId int64 `json:"referer_id"`
|
RefererId *int64 `json:"referer_id"`
|
||||||
Enable *bool `json:"enable"`
|
Enable *bool `json:"enable"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserNotifyRequest struct {
|
type UpdateUserNotifyRequest struct {
|
||||||
|
|||||||
@@ -48,7 +48,4 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) {
|
|||||||
// Apple IAP 对账(第二层:5min 扫描 + 第三层:日终全量)
|
// Apple IAP 对账(第二层:5min 扫描 + 第三层:日终全量)
|
||||||
mux.Handle(types.SchedulerIAPReconcile, iapLogic.NewReconcileLogic(serverCtx))
|
mux.Handle(types.SchedulerIAPReconcile, iapLogic.NewReconcileLogic(serverCtx))
|
||||||
mux.Handle(types.SchedulerIAPDailyReconcile, iapLogic.NewDailyReconcileLogic(serverCtx))
|
mux.Handle(types.SchedulerIAPDailyReconcile, iapLogic.NewDailyReconcileLogic(serverCtx))
|
||||||
|
|
||||||
// Stuck order recovery
|
|
||||||
mux.Handle(types.SchedulerStuckOrderRecovery, orderLogic.NewStuckOrderRecoveryLogic(serverCtx))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/group"
|
"github.com/perfect-panel/server/internal/logic/admin/group"
|
||||||
@@ -47,7 +46,7 @@ const (
|
|||||||
OrderStatusPaid = 2 // Order paid and ready for processing
|
OrderStatusPaid = 2 // Order paid and ready for processing
|
||||||
OrderStatusClose = 3 // Order closed/cancelled
|
OrderStatusClose = 3 // Order closed/cancelled
|
||||||
OrderStatusFailed = 4 // Order processing failed
|
OrderStatusFailed = 4 // Order processing failed
|
||||||
OrderStatusClaimed = 6 // Internal transient claim while a worker processes the order
|
OrderStatusClaimed = 4 // Internal transient claim while a worker processes the order
|
||||||
OrderStatusFinished = 5 // Order successfully completed
|
OrderStatusFinished = 5 // Order successfully completed
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,12 +92,8 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
|
|
||||||
orderInfo, err := l.claimAndGetOrder(ctx, payload.OrderNo)
|
orderInfo, err := l.claimAndGetOrder(ctx, payload.OrderNo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 如果订单不存在或状态不对,不重试
|
||||||
if errors.Is(err, ErrInvalidOrderStatus) {
|
if errors.Is(err, ErrInvalidOrderStatus) {
|
||||||
if strings.Contains(err.Error(), "stuck in claimed") {
|
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 订单卡在 claimed,将重试",
|
|
||||||
logger.Field("order_no", payload.OrderNo))
|
|
||||||
return err // 返回错误触发 asynq 重试
|
|
||||||
}
|
|
||||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单状态不是已支付,跳过",
|
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单状态不是已支付,跳过",
|
||||||
logger.Field("order_no", payload.OrderNo))
|
logger.Field("order_no", payload.OrderNo))
|
||||||
return nil
|
return nil
|
||||||
@@ -121,12 +116,7 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err = l.processOrderByType(ctx, orderInfo, payload.IAPExpireAt); err != nil {
|
if err = l.processOrderByType(ctx, orderInfo, payload.IAPExpireAt); err != nil {
|
||||||
if releaseErr := l.releaseClaim(ctx, orderInfo.OrderNo); releaseErr != nil {
|
l.releaseClaim(ctx, orderInfo.OrderNo)
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] releaseClaim also failed, stuck recovery will handle",
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("release_error", releaseErr.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 处理订单失败,将重试",
|
logger.WithContext(ctx).Error("[ActivateOrderLogic] 处理订单失败,将重试",
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
logger.Field("order_type", orderInfo.Type),
|
logger.Field("order_type", orderInfo.Type),
|
||||||
@@ -135,12 +125,7 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = l.reconcilePostOrderSubscriptions(ctx, orderInfo); err != nil {
|
if err = l.reconcilePostOrderSubscriptions(ctx, orderInfo); err != nil {
|
||||||
if releaseErr := l.releaseClaim(ctx, orderInfo.OrderNo); releaseErr != nil {
|
l.releaseClaim(ctx, orderInfo.OrderNo)
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] releaseClaim also failed, stuck recovery will handle",
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("release_error", releaseErr.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 订单订阅兜底合并失败,将重试",
|
logger.WithContext(ctx).Error("[ActivateOrderLogic] 订单订阅兜底合并失败,将重试",
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
logger.Field("order_type", orderInfo.Type),
|
logger.Field("order_type", orderInfo.Type),
|
||||||
@@ -191,15 +176,6 @@ func (l *ActivateOrderLogic) claimAndGetOrder(ctx context.Context, orderNo strin
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect stuck claimed order — return retryable error so asynq re-tries
|
|
||||||
if orderInfo.Status == OrderStatusClaimed {
|
|
||||||
logger.WithContext(ctx).Error("Order stuck in claimed status",
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("status", orderInfo.Status),
|
|
||||||
)
|
|
||||||
return nil, fmt.Errorf("order %s stuck in claimed status: %w", orderNo, ErrInvalidOrderStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
if orderInfo.Status != OrderStatusPaid {
|
if orderInfo.Status != OrderStatusPaid {
|
||||||
logger.WithContext(ctx).Error("Order status error",
|
logger.WithContext(ctx).Error("Order status error",
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
@@ -225,7 +201,7 @@ func (l *ActivateOrderLogic) claimAndGetOrder(ctx context.Context, orderNo strin
|
|||||||
return &orderInfo, nil
|
return &orderInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ActivateOrderLogic) releaseClaim(ctx context.Context, orderNo string) error {
|
func (l *ActivateOrderLogic) releaseClaim(ctx context.Context, orderNo string) {
|
||||||
if err := l.svc.DB.WithContext(ctx).
|
if err := l.svc.DB.WithContext(ctx).
|
||||||
Model(&order.Order{}).
|
Model(&order.Order{}).
|
||||||
Where("order_no = ? AND status = ?", orderNo, OrderStatusClaimed).
|
Where("order_no = ? AND status = ?", orderNo, OrderStatusClaimed).
|
||||||
@@ -234,9 +210,7 @@ func (l *ActivateOrderLogic) releaseClaim(ctx context.Context, orderNo string) e
|
|||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
logger.Field("order_no", orderNo),
|
logger.Field("order_no", orderNo),
|
||||||
)
|
)
|
||||||
return fmt.Errorf("release claim failed for order %s: %w", orderNo, err)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// processOrderByType routes order processing based on the order type
|
// processOrderByType routes order processing based on the order type
|
||||||
@@ -589,27 +563,14 @@ func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateOrderStatus uses WHERE status < target, which blocks claimed(6)→finished(5).
|
// Update order status using state-guarded UpdateOrderStatus to prevent double finalization
|
||||||
// Use a direct update matching the exact claimed status, then update the full record
|
if err := l.svc.OrderModel.UpdateOrderStatus(ctx, orderInfo.OrderNo, OrderStatusFinished); err != nil {
|
||||||
// via the model layer to properly invalidate the cache.
|
logger.WithContext(ctx).Error("Update order status failed",
|
||||||
result := l.svc.DB.WithContext(ctx).
|
|
||||||
Model(&order.Order{}).
|
|
||||||
Where("order_no = ? AND status = ?", orderInfo.OrderNo, OrderStatusClaimed).
|
|
||||||
Update("status", OrderStatusFinished)
|
|
||||||
if result.Error != nil {
|
|
||||||
logger.WithContext(ctx).Error("Update order status from claimed to finished failed",
|
|
||||||
logger.Field("error", result.Error.Error()),
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Invalidate order cache regardless of whether the DB update succeeded
|
|
||||||
orderInfo.Status = OrderStatusFinished
|
|
||||||
if err := l.svc.OrderModel.Update(ctx, orderInfo); err != nil {
|
|
||||||
logger.WithContext(ctx).Error("Update order cache after finalization failed",
|
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
orderInfo.Status = OrderStatusFinished
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "order_status_finished",
|
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "order_status_finished",
|
||||||
"[SubscriptionFlow] order status updated to finished",
|
"[SubscriptionFlow] order status updated to finished",
|
||||||
commonLogic.OrderTraceFields(orderInfo)...,
|
commonLogic.OrderTraceFields(orderInfo)...,
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package orderLogic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
|
||||||
"github.com/perfect-panel/server/internal/model/order"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
queueTypes "github.com/perfect-panel/server/queue/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StuckOrderRecoveryLogic scans orders stuck in claimed status and re-queues them for processing.
|
|
||||||
type StuckOrderRecoveryLogic struct {
|
|
||||||
svc *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStuckOrderRecoveryLogic(svc *svc.ServiceContext) *StuckOrderRecoveryLogic {
|
|
||||||
return &StuckOrderRecoveryLogic{svc: svc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessTask scans for orders stuck in claimed status for over 10 minutes,
|
|
||||||
// resets them to paid, and re-enqueues the activate task so they are retried
|
|
||||||
// independently of asynq's original retry counter (which may be exhausted).
|
|
||||||
func (l *StuckOrderRecoveryLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error {
|
|
||||||
cutoff := time.Now().Add(-10 * time.Minute)
|
|
||||||
|
|
||||||
var stuckOrders []order.Order
|
|
||||||
if err := l.svc.DB.WithContext(ctx).
|
|
||||||
Model(&order.Order{}).
|
|
||||||
Where("status = ? AND updated_at < ?", OrderStatusClaimed, cutoff).
|
|
||||||
Find(&stuckOrders).Error; err != nil {
|
|
||||||
logger.WithContext(ctx).Error("[StuckOrderRecovery] Failed to query stuck orders",
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(stuckOrders) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
orderNos := make([]string, 0, len(stuckOrders))
|
|
||||||
for i := range stuckOrders {
|
|
||||||
orderNos = append(orderNos, stuckOrders[i].OrderNo)
|
|
||||||
}
|
|
||||||
logger.WithContext(ctx).Error("[StuckOrderRecovery] Found stuck claimed orders, recovering",
|
|
||||||
logger.Field("count", len(stuckOrders)),
|
|
||||||
logger.Field("order_nos", orderNos),
|
|
||||||
)
|
|
||||||
|
|
||||||
for i := range stuckOrders {
|
|
||||||
o := &stuckOrders[i]
|
|
||||||
|
|
||||||
result := l.svc.DB.WithContext(ctx).
|
|
||||||
Model(&order.Order{}).
|
|
||||||
Where("order_no = ? AND status = ?", o.OrderNo, OrderStatusClaimed).
|
|
||||||
Update("status", OrderStatusPaid)
|
|
||||||
if result.Error != nil {
|
|
||||||
logger.WithContext(ctx).Error("[StuckOrderRecovery] Failed to reset order status",
|
|
||||||
logger.Field("order_no", o.OrderNo),
|
|
||||||
logger.Field("error", result.Error.Error()),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
// Another process already handled this order
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate order cache
|
|
||||||
o.Status = OrderStatusPaid
|
|
||||||
if err := l.svc.OrderModel.Update(ctx, o); err != nil {
|
|
||||||
logger.WithContext(ctx).Error("[StuckOrderRecovery] Failed to update order cache",
|
|
||||||
logger.Field("order_no", o.OrderNo),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-enqueue activate task so the order gets processed regardless of asynq retry state
|
|
||||||
payload, err := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: o.OrderNo})
|
|
||||||
if err != nil {
|
|
||||||
logger.WithContext(ctx).Error("[StuckOrderRecovery] Failed to marshal task payload",
|
|
||||||
logger.Field("order_no", o.OrderNo),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, err = l.svc.Queue.EnqueueContext(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)); err != nil {
|
|
||||||
logger.WithContext(ctx).Error("[StuckOrderRecovery] Failed to re-enqueue activate task",
|
|
||||||
logger.Field("order_no", o.OrderNo),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
logger.WithContext(ctx).Info("[StuckOrderRecovery] Re-enqueued activate task",
|
|
||||||
logger.Field("order_no", o.OrderNo),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ const (
|
|||||||
SchedulerTotalServerData = "scheduler:total:server"
|
SchedulerTotalServerData = "scheduler:total:server"
|
||||||
SchedulerResetTraffic = "scheduler:reset:traffic"
|
SchedulerResetTraffic = "scheduler:reset:traffic"
|
||||||
SchedulerTrafficStat = "scheduler:traffic:stat"
|
SchedulerTrafficStat = "scheduler:traffic:stat"
|
||||||
SchedulerIAPReconcile = "scheduler:iap:reconcile" // 第二层:每 5 分钟扫描待支付 IAP 订单
|
SchedulerIAPReconcile = "scheduler:iap:reconcile" // 第二层:每 5 分钟扫描待支付 IAP 订单
|
||||||
SchedulerIAPDailyReconcile = "scheduler:iap:daily:reconcile" // 第三层:日终全量对账
|
SchedulerIAPDailyReconcile = "scheduler:iap:daily:reconcile" // 第三层:日终全量对账
|
||||||
SchedulerStuckOrderRecovery = "scheduler:stuck:order:recovery" // 扫描并恢复超时 claimed 订单
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -64,12 +64,6 @@ func (m *Service) Start() {
|
|||||||
logger.Errorf("register iap daily reconcile task failed: %s", err.Error())
|
logger.Errorf("register iap daily reconcile task failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// schedule stuck order recovery: every 10 minutes
|
|
||||||
stuckOrderTask := asynq.NewTask(types.SchedulerStuckOrderRecovery, nil)
|
|
||||||
if _, err := m.server.Register("@every 10m", stuckOrderTask, asynq.MaxRetry(1)); err != nil {
|
|
||||||
logger.Errorf("register stuck order recovery task failed: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.server.Run(); err != nil {
|
if err := m.server.Run(); err != nil {
|
||||||
logger.Errorf("run scheduler failed: %s", err.Error())
|
logger.Errorf("run scheduler failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user