diff --git a/initialize/migrate/database/02139_invite_gift_days.down.sql b/initialize/migrate/database/02139_invite_gift_days.down.sql index 3203bf9..10db6be 100644 --- a/initialize/migrate/database/02139_invite_gift_days.down.sql +++ b/initialize/migrate/database/02139_invite_gift_days.down.sql @@ -1,4 +1,4 @@ -DELETE +select FROM `system` WHERE `category` = 'invite' AND `key` = 'GiftDays'; diff --git a/internal/logic/public/order/getDiscount.go b/internal/logic/public/order/getDiscount.go index 01f1035..df76128 100644 --- a/internal/logic/public/order/getDiscount.go +++ b/internal/logic/public/order/getDiscount.go @@ -13,3 +13,19 @@ func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 return finalDiscount / float64(100) } + +// isNewUserOnlyForQuantity checks whether the matched discount tier has new_user_only enabled. +func isNewUserOnlyForQuantity(discounts []types.SubscribeDiscount, inputQuantity int64) bool { + var finalDiscount float64 = 100 + var newUserOnly bool + + for _, discount := range discounts { + if inputQuantity >= discount.Quantity && discount.Discount < finalDiscount { + finalDiscount = discount.Discount + newUserOnly = discount.NewUserOnly + } + } + + return newUserOnly +} + diff --git a/internal/logic/public/order/preCreateOrderLogic.go b/internal/logic/public/order/preCreateOrderLogic.go index 2252c6e..711306b 100644 --- a/internal/logic/public/order/preCreateOrderLogic.go +++ b/internal/logic/public/order/preCreateOrderLogic.go @@ -109,8 +109,14 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r } } - // check new user only restriction - if !isSingleModeRenewal && sub.NewUserOnly != nil && *sub.NewUserOnly { + // check new user only restriction (tier-level only) + var newUserOnly bool + if !isSingleModeRenewal && sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + newUserOnly = isNewUserOnlyForQuantity(dis, req.Quantity) + } + if newUserOnly { if time.Since(u.CreatedAt) > 24*time.Hour { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNewUserOnly), "not a new user") } diff --git a/internal/logic/public/order/purchaseLogic.go b/internal/logic/public/order/purchaseLogic.go index 58c2dd8..06daf73 100644 --- a/internal/logic/public/order/purchaseLogic.go +++ b/internal/logic/public/order/purchaseLogic.go @@ -270,20 +270,28 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P } } - // check new user only restriction inside transaction to prevent race condition - if orderInfo.Type == 1 && sub.NewUserOnly != nil && *sub.NewUserOnly { - if time.Since(u.CreatedAt) > 24*time.Hour { - return errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNewUserOnly), "not a new user") + // check new user only restriction inside transaction to prevent race condition (tier-level only) + if orderInfo.Type == 1 { + var txNewUserOnly bool + if sub.Discount != "" { + var dis []types.SubscribeDiscount + _ = json.Unmarshal([]byte(sub.Discount), &dis) + txNewUserOnly = isNewUserOnlyForQuantity(dis, orderInfo.Quantity) } - var historyCount int64 - if e := db.Model(&order.Order{}). - Where("user_id = ? AND subscribe_id = ? AND type = 1 AND status IN ?", - u.Id, targetSubscribeID, []int{2, 5}). - Count(&historyCount).Error; e != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "check new user purchase history error: %v", e.Error()) - } - if historyCount >= 1 { - return errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNewUserOnly), "already purchased new user plan") + if txNewUserOnly { + if time.Since(u.CreatedAt) > 24*time.Hour { + return errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNewUserOnly), "not a new user") + } + var historyCount int64 + if e := db.Model(&order.Order{}). + Where("user_id = ? AND subscribe_id = ? AND type = 1 AND status IN ?", + u.Id, targetSubscribeID, []int{2, 5}). + Count(&historyCount).Error; e != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "check new user purchase history error: %v", e.Error()) + } + if historyCount >= 1 { + return errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNewUserOnly), "already purchased new user plan") + } } } diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index 1f20c96..cf363af 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -73,8 +73,9 @@ func (s *Subscribe) BeforeUpdate(tx *gorm.DB) error { } type Discount struct { - Months int64 `json:"months"` - Discount int64 `json:"discount"` + Months int64 `json:"months"` + Discount int64 `json:"discount"` + NewUserOnly bool `json:"new_user_only"` } type Group struct { diff --git a/internal/types/types.go b/internal/types/types.go index 4651fa5..07761fe 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2455,8 +2455,9 @@ type SubscribeConfig struct { } type SubscribeDiscount struct { - Quantity int64 `json:"quantity"` - Discount float64 `json:"discount"` + Quantity int64 `json:"quantity"` + Discount float64 `json:"discount"` + NewUserOnly bool `json:"new_user_only"` } type SubscribeGroup struct { diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index f94f567..9872966 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -19,6 +19,7 @@ import ( "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/logic/telegram" "github.com/perfect-panel/server/internal/model/order" + internaltypes "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/internal/model/redemption" "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/model/user" @@ -223,6 +224,29 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O return err } + // check new user only restriction at activation to prevent concurrent bypass + if orderInfo.Type == OrderTypeSubscribe && sub.Discount != "" { + var dis []internaltypes.SubscribeDiscount + if jsonErr := json.Unmarshal([]byte(sub.Discount), &dis); jsonErr == nil { + newUserOnly := isNewUserOnlyForQuantity(dis, orderInfo.Quantity) + if newUserOnly { + if time.Since(userInfo.CreatedAt) > 24*time.Hour { + return fmt.Errorf("new user only: user %d is not a new user", userInfo.Id) + } + var historyCount int64 + if e := l.svc.DB.Model(&order.Order{}). + Where("user_id = ? AND subscribe_id = ? AND type = 1 AND status = ? AND order_no != ?", + orderInfo.UserId, orderInfo.SubscribeId, OrderStatusFinished, orderInfo.OrderNo). + Count(&historyCount).Error; e != nil { + return fmt.Errorf("new user only: check history error: %w", e) + } + if historyCount >= 1 { + return fmt.Errorf("new user only: user %d already activated subscribe %d", userInfo.Id, orderInfo.SubscribeId) + } + } + } + } + var userSub *user.Subscribe // 单订阅模式下,优先兜底为“续费语义”:延长已购订阅,避免并发下重复创建 user_subscribe @@ -615,7 +639,7 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, referee *user.User, orderNo string) { giftDays := l.svc.Config.Invite.GiftDays - if giftDays <= 0 || referee == nil || referee.Id == 0 { + if giftDays <= 0 || referee == nil || referee.Id == 0 || referee.RefererId == 0 { return } _ = l.grantGiftDays(ctx, referee, int(giftDays), orderNo, "邀请赠送") @@ -1312,3 +1336,18 @@ func (l *ActivateOrderLogic) RedemptionActivate(ctx context.Context, orderInfo * return nil } + +// isNewUserOnlyForQuantity checks whether the matched discount tier has new_user_only enabled. +func isNewUserOnlyForQuantity(discounts []internaltypes.SubscribeDiscount, inputQuantity int64) bool { + var finalDiscount float64 = 100 + var newUserOnly bool + + for _, d := range discounts { + if inputQuantity >= d.Quantity && d.Discount < finalDiscount { + finalDiscount = d.Discount + newUserOnly = d.NewUserOnly + } + } + + return newUserOnly +}