fix(order): prevent duplicate subscriptions and repair invite gifts
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/logic/admin/group"
|
||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||
"github.com/perfect-panel/server/internal/model/log"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
@@ -44,6 +45,7 @@ const (
|
||||
OrderStatusPaid = 2 // Order paid and ready for processing
|
||||
OrderStatusClose = 3 // Order closed/cancelled
|
||||
OrderStatusFailed = 4 // Order processing failed
|
||||
OrderStatusClaimed = 4 // Internal transient claim while a worker processes the order
|
||||
OrderStatusFinished = 5 // Order successfully completed
|
||||
)
|
||||
|
||||
@@ -82,7 +84,7 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
logger.WithContext(ctx).Info("[ActivateOrderLogic] 正在验证订单",
|
||||
logger.Field("order_no", payload.OrderNo))
|
||||
|
||||
orderInfo, err := l.validateAndGetOrder(ctx, payload.OrderNo)
|
||||
orderInfo, err := l.claimAndGetOrder(ctx, payload.OrderNo)
|
||||
if err != nil {
|
||||
// 如果订单不存在或状态不对,不重试
|
||||
if errors.Is(err, ErrInvalidOrderStatus) {
|
||||
@@ -108,6 +110,7 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
logger.Field("user_id", orderInfo.UserId))
|
||||
|
||||
if err = l.processOrderByType(ctx, orderInfo, payload.IAPExpireAt); err != nil {
|
||||
l.releaseClaim(ctx, orderInfo.OrderNo)
|
||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 处理订单失败,将重试",
|
||||
logger.Field("order_no", orderInfo.OrderNo),
|
||||
logger.Field("order_type", orderInfo.Type),
|
||||
@@ -137,10 +140,11 @@ func (l *ActivateOrderLogic) parsePayload(ctx context.Context, payload []byte) (
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// validateAndGetOrder retrieves an order by order number and validates its status
|
||||
// claimAndGetOrder retrieves an order by order number and atomically claims paid orders.
|
||||
// Returns error if order is not found or not in paid status
|
||||
func (l *ActivateOrderLogic) validateAndGetOrder(ctx context.Context, orderNo string) (*order.Order, error) {
|
||||
orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, orderNo)
|
||||
func (l *ActivateOrderLogic) claimAndGetOrder(ctx context.Context, orderNo string) (*order.Order, error) {
|
||||
var orderInfo order.Order
|
||||
err := l.svc.DB.WithContext(ctx).Model(&order.Order{}).Where("order_no = ?", orderNo).First(&orderInfo).Error
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("Find order failed",
|
||||
logger.Field("error", err.Error()),
|
||||
@@ -165,7 +169,33 @@ func (l *ActivateOrderLogic) validateAndGetOrder(ctx context.Context, orderNo st
|
||||
return nil, ErrInvalidOrderStatus
|
||||
}
|
||||
|
||||
return orderInfo, nil
|
||||
result := l.svc.DB.WithContext(ctx).
|
||||
Model(&order.Order{}).
|
||||
Where("order_no = ? AND status = ?", orderNo, OrderStatusPaid).
|
||||
Update("status", OrderStatusClaimed)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
logger.WithContext(ctx).Info("Order already claimed by another worker, skip processing",
|
||||
logger.Field("order_no", orderNo),
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
orderInfo.Status = OrderStatusClaimed
|
||||
return &orderInfo, nil
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) releaseClaim(ctx context.Context, orderNo string) {
|
||||
if err := l.svc.DB.WithContext(ctx).
|
||||
Model(&order.Order{}).
|
||||
Where("order_no = ? AND status = ?", orderNo, OrderStatusClaimed).
|
||||
Update("status", OrderStatusPaid).Error; err != nil {
|
||||
logger.WithContext(ctx).Error("Release order claim failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("order_no", orderNo),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// processOrderByType routes order processing based on the order type
|
||||
@@ -274,20 +304,24 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
||||
)
|
||||
}
|
||||
|
||||
// 如果没有合并已购订阅,再尝试合并赠送订阅(order_id=0)
|
||||
if userSub == nil {
|
||||
giftSub, giftErr := l.findGiftSubscription(ctx, singleModeUserId, orderInfo.SubscribeId)
|
||||
if giftErr == nil && giftSub != nil {
|
||||
// 在赠送订阅上延长时间,保持 token 不变
|
||||
userSub, err = l.extendGiftSubscription(ctx, giftSub, orderInfo, sub)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("Extend gift subscription failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("gift_subscribe_id", giftSub.Id),
|
||||
)
|
||||
// 合并失败时回退到创建新订阅
|
||||
userSub = nil
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有合并已购订阅,再尝试合并赠送订阅(order_id=0)。
|
||||
// 全局单订阅口径下,非 SingleModel 也不能让试用订阅和付费订阅并存。
|
||||
if userSub == nil {
|
||||
effectiveOwner := orderInfo.UserId
|
||||
if orderInfo.SubscriptionUserId > 0 {
|
||||
effectiveOwner = orderInfo.SubscriptionUserId
|
||||
}
|
||||
giftSub, giftErr := l.findGiftSubscription(ctx, effectiveOwner, orderInfo.SubscribeId)
|
||||
if giftErr == nil && giftSub != nil {
|
||||
userSub, err = l.extendGiftSubscription(ctx, giftSub, orderInfo, sub)
|
||||
if err != nil {
|
||||
logger.WithContext(ctx).Error("Extend gift subscription failed",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("gift_subscribe_id", giftSub.Id),
|
||||
)
|
||||
userSub = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,8 +336,10 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
|
||||
}
|
||||
var existingSub user.Subscribe
|
||||
if findErr := l.svc.DB.Model(&user.Subscribe{}).
|
||||
Where("user_id IN ? AND subscribe_id = ?", candidateUserIds, orderInfo.SubscribeId).
|
||||
Where("user_id IN ? AND token != ''", candidateUserIds).
|
||||
Order("expire_time DESC").
|
||||
Order("updated_at DESC").
|
||||
Order("id DESC").
|
||||
First(&existingSub).Error; findErr == nil {
|
||||
// 家庭组场景:订阅 owner 可能变更(如成员注册的试用 → 被家主收归),
|
||||
// 续期前把 user_id 校正为当前订单的 SubscriptionUserId
|
||||
@@ -514,7 +550,7 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn
|
||||
// Check quota limit before creating subscription (final safeguard)
|
||||
if sub.Quota > 0 {
|
||||
var count int64
|
||||
if err := l.svc.DB.Model(&user.Subscribe{}).Where("user_id = ? AND subscribe_id = ?", orderInfo.UserId, orderInfo.SubscribeId).Count(&count).Error; err != nil {
|
||||
if err := l.svc.DB.Model(&user.Subscribe{}).Where("user_id = ?", subscriptionUserId).Count(&count).Error; err != nil {
|
||||
logger.WithContext(ctx).Error("Count user subscribe failed", logger.Field("error", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
@@ -602,7 +638,7 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
||||
if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) {
|
||||
// 普通用户路径(佣金比例=0):只有首单才双方赠N天
|
||||
if orderInfo.IsNew {
|
||||
l.grantGiftDaysToBothParties(ctx, userInfo, orderInfo.OrderNo)
|
||||
l.grantGiftDaysToBothParties(ctx, userInfo, orderInfo)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -692,16 +728,18 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
|
||||
|
||||
// 有佣金路径:邀请人拿佣金,被邀请用户(首单)拿天数
|
||||
if orderInfo.IsNew {
|
||||
_ = l.grantGiftDays(ctx, userInfo, int(l.svc.Config.Invite.GiftDays), orderInfo.OrderNo, "邀请赠送")
|
||||
giftTarget := l.resolveGiftTargetUser(ctx, userInfo, orderInfo.SubscriptionUserId)
|
||||
_ = l.grantGiftDays(ctx, giftTarget, int(l.svc.Config.Invite.GiftDays), orderInfo.OrderNo, "邀请赠送")
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, referee *user.User, orderNo string) {
|
||||
func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, referee *user.User, orderInfo *order.Order) {
|
||||
giftDays := l.svc.Config.Invite.GiftDays
|
||||
if giftDays <= 0 || referee == nil || referee.Id == 0 || referee.RefererId == 0 {
|
||||
if giftDays <= 0 || referee == nil || referee.Id == 0 || referee.RefererId == 0 || orderInfo == nil {
|
||||
return
|
||||
}
|
||||
_ = l.grantGiftDays(ctx, referee, int(giftDays), orderNo, "邀请赠送")
|
||||
refereeTarget := l.resolveGiftTargetUser(ctx, referee, orderInfo.SubscriptionUserId)
|
||||
_ = l.grantGiftDays(ctx, refereeTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送")
|
||||
if referee.RefererId == 0 {
|
||||
return
|
||||
}
|
||||
@@ -709,7 +747,32 @@ func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, ref
|
||||
if err != nil || referer == nil {
|
||||
return
|
||||
}
|
||||
_ = l.grantGiftDays(ctx, referer, int(giftDays), orderNo, "邀请赠送")
|
||||
refererTarget := l.resolveGiftTargetUser(ctx, referer, 0)
|
||||
_ = l.grantGiftDays(ctx, refererTarget, int(giftDays), orderInfo.OrderNo, "邀请赠送")
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) resolveGiftTargetUser(ctx context.Context, source *user.User, forcedOwnerID int64) *user.User {
|
||||
if source == nil || source.Id == 0 {
|
||||
return source
|
||||
}
|
||||
targetID := source.Id
|
||||
if forcedOwnerID > 0 {
|
||||
targetID = forcedOwnerID
|
||||
} else if entitlement, err := commonLogic.ResolveEntitlementUser(ctx, l.svc.DB, source.Id); err == nil && entitlement != nil && entitlement.EffectiveUserID > 0 {
|
||||
targetID = entitlement.EffectiveUserID
|
||||
}
|
||||
if targetID == source.Id {
|
||||
return source
|
||||
}
|
||||
target, err := l.svc.UserModel.FindOne(ctx, targetID)
|
||||
if err != nil || target == nil {
|
||||
logger.WithContext(ctx).Error("Resolve gift target owner failed",
|
||||
logger.Field("source_user_id", source.Id),
|
||||
logger.Field("target_user_id", targetID),
|
||||
)
|
||||
return source
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, days int, orderNo string, remark string) error {
|
||||
@@ -736,7 +799,22 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
|
||||
activeSubscribe, err := l.svc.UserModel.FindActiveSubscribe(ctx, u.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
giftLog := &log.Gift{
|
||||
Type: log.GiftTypeIncrease,
|
||||
OrderNo: orderNo,
|
||||
SubscribeId: 0,
|
||||
Amount: int64(days),
|
||||
Balance: u.Balance,
|
||||
Remark: remark + " skipped: no active subscription",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
content, _ := giftLog.Marshal()
|
||||
return l.svc.LogModel.Insert(ctx, &log.SystemLog{
|
||||
Type: log.TypeGift.Uint8(),
|
||||
Date: time.Now().Format("2006-01-02"),
|
||||
ObjectID: u.Id,
|
||||
Content: string(content),
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user