Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d12c340743 |
+1
-1
@@ -229,6 +229,7 @@ type (
|
|||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount float64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
MapApple string `json:"map_apple"`
|
MapApple string `json:"map_apple"`
|
||||||
|
Promo *SubscribePromo `json:"promo"`
|
||||||
}
|
}
|
||||||
SubscribePromo {
|
SubscribePromo {
|
||||||
RuleName string `json:"rule_name"`
|
RuleName string `json:"rule_name"`
|
||||||
@@ -250,7 +251,6 @@ type (
|
|||||||
UnitPrice int64 `json:"unit_price"`
|
UnitPrice int64 `json:"unit_price"`
|
||||||
UnitTime string `json:"unit_time"`
|
UnitTime string `json:"unit_time"`
|
||||||
Discount []SubscribeDiscount `json:"discount"`
|
Discount []SubscribeDiscount `json:"discount"`
|
||||||
Promo *SubscribePromo `json:"promo"`
|
|
||||||
NodeCount int64 `json:"node_count"`
|
NodeCount int64 `json:"node_count"`
|
||||||
Replacement int64 `json:"replacement"`
|
Replacement int64 `json:"replacement"`
|
||||||
Inventory int64 `json:"inventory"`
|
Inventory int64 `json:"inventory"`
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PromoResult struct {
|
type PromoResult struct {
|
||||||
@@ -149,12 +148,6 @@ func evaluateInactiveUserPromo(
|
|||||||
err := db.WithContext(ctx).
|
err := db.WithContext(ctx).
|
||||||
Model(&user.Subscribe{}).
|
Model(&user.Subscribe{}).
|
||||||
Where("user_id = ?", userID).
|
Where("user_id = ?", userID).
|
||||||
Order(clause.OrderBy{
|
|
||||||
Expression: clause.Expr{
|
|
||||||
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END",
|
|
||||||
Vars: []interface{}{permanentSubscribeExpireTime()},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Order("expire_time DESC").
|
Order("expire_time DESC").
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Take(&lastSub).Error
|
Take(&lastSub).Error
|
||||||
@@ -165,19 +158,8 @@ func evaluateInactiveUserPromo(
|
|||||||
return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo inactive user subscription failed")
|
return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo inactive user subscription failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return isInactivePromoEligible(lastSub.ExpireTime, now, params.InactiveMonths), ruleExpiresAt, nil
|
threshold := now.AddDate(0, -params.InactiveMonths, 0)
|
||||||
}
|
return lastSub.ExpireTime.Before(threshold) || lastSub.ExpireTime.Equal(threshold), ruleExpiresAt, nil
|
||||||
|
|
||||||
func isInactivePromoEligible(expireTime time.Time, now time.Time, inactiveMonths int) bool {
|
|
||||||
if expireTime.Equal(permanentSubscribeExpireTime()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
threshold := now.AddDate(0, -inactiveMonths, 0)
|
|
||||||
return expireTime.Before(threshold) || expireTime.Equal(threshold)
|
|
||||||
}
|
|
||||||
|
|
||||||
func permanentSubscribeExpireTime() time.Time {
|
|
||||||
return time.UnixMilli(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time {
|
func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time {
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIsInactivePromoEligible(t *testing.T) {
|
|
||||||
now := time.Date(2026, time.May, 27, 12, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
expireAt time.Time
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "expired before inactive threshold is eligible",
|
|
||||||
expireAt: now.AddDate(0, -4, 0),
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expired exactly at inactive threshold is eligible",
|
|
||||||
expireAt: now.AddDate(0, -3, 0),
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "recently expired subscription is not eligible",
|
|
||||||
expireAt: now.AddDate(0, -2, 0),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "active future subscription is not eligible",
|
|
||||||
expireAt: now.Add(time.Hour),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "permanent subscription marker is not eligible",
|
|
||||||
expireAt: time.UnixMilli(0),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := isInactivePromoEligible(tt.expireAt, now, 3)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("isInactivePromoEligible() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,7 @@ const (
|
|||||||
|
|
||||||
type subscribePromoCandidate struct {
|
type subscribePromoCandidate struct {
|
||||||
SubscribeId int64 `gorm:"column:subscribe_id"`
|
SubscribeId int64 `gorm:"column:subscribe_id"`
|
||||||
|
Quantity int64 `gorm:"column:quantity"`
|
||||||
RuleName string `gorm:"column:rule_name"`
|
RuleName string `gorm:"column:rule_name"`
|
||||||
RuleType string `gorm:"column:rule_type"`
|
RuleType string `gorm:"column:rule_type"`
|
||||||
PromoPrice int64 `gorm:"column:promo_price"`
|
PromoPrice int64 `gorm:"column:promo_price"`
|
||||||
@@ -38,8 +39,8 @@ type promoRuleParams struct {
|
|||||||
InactiveMonths int `json:"inactive_months"`
|
InactiveMonths int `json:"inactive_months"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subscribeIDs []int64) (map[int64]*types.SubscribePromo, error) {
|
func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subscribeIDs []int64) (map[int64]map[int64]*types.SubscribePromo, error) {
|
||||||
result := make(map[int64]*types.SubscribePromo)
|
result := make(map[int64]map[int64]*types.SubscribePromo)
|
||||||
if len(subscribeIDs) == 0 || svcCtx == nil || svcCtx.DB == nil {
|
if len(subscribeIDs) == 0 || svcCtx == nil || svcCtx.DB == nil {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
|
|||||||
evaluator := promoEligibilityEvaluator{ctx: ctx, db: svcCtx.DB, userInfo: userInfo}
|
evaluator := promoEligibilityEvaluator{ctx: ctx, db: svcCtx.DB, userInfo: userInfo}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range candidates {
|
||||||
if _, exists := result[candidate.SubscribeId]; exists {
|
if _, exists := result[candidate.SubscribeId][candidate.Quantity]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !candidate.isActive(now) {
|
if !candidate.isActive(now) {
|
||||||
@@ -69,7 +70,10 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result[candidate.SubscribeId] = &types.SubscribePromo{
|
if _, exists := result[candidate.SubscribeId]; !exists {
|
||||||
|
result[candidate.SubscribeId] = make(map[int64]*types.SubscribePromo)
|
||||||
|
}
|
||||||
|
result[candidate.SubscribeId][candidate.Quantity] = &types.SubscribePromo{
|
||||||
RuleName: candidate.RuleName,
|
RuleName: candidate.RuleName,
|
||||||
RuleType: candidate.RuleType,
|
RuleType: candidate.RuleType,
|
||||||
PromoPrice: candidate.PromoPrice,
|
PromoPrice: candidate.PromoPrice,
|
||||||
@@ -84,7 +88,7 @@ func querySubscribePromoCandidates(ctx context.Context, svcCtx *svc.ServiceConte
|
|||||||
var candidates []subscribePromoCandidate
|
var candidates []subscribePromoCandidate
|
||||||
query := svcCtx.DB.WithContext(ctx).
|
query := svcCtx.DB.WithContext(ctx).
|
||||||
Table("subscribe_promo AS sp").
|
Table("subscribe_promo AS sp").
|
||||||
Select("sp.subscribe_id, sp.promo_price, pr.name AS rule_name, pr.type AS rule_type, pr.params, pr.start_time, pr.end_time").
|
Select("sp.subscribe_id, sp.quantity, sp.promo_price, pr.name AS rule_name, pr.type AS rule_type, pr.params, pr.start_time, pr.end_time").
|
||||||
Joins("JOIN promo_rule AS pr ON pr.id = sp.promo_rule_id AND pr.deleted_at IS NULL").
|
Joins("JOIN promo_rule AS pr ON pr.id = sp.promo_rule_id AND pr.deleted_at IS NULL").
|
||||||
Where("sp.subscribe_id IN ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeIDs, true)
|
Where("sp.subscribe_id IN ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeIDs, true)
|
||||||
if !loggedIn {
|
if !loggedIn {
|
||||||
@@ -92,6 +96,7 @@ func querySubscribePromoCandidates(ctx context.Context, svcCtx *svc.ServiceConte
|
|||||||
}
|
}
|
||||||
err := query.
|
err := query.
|
||||||
Order("sp.subscribe_id ASC").
|
Order("sp.subscribe_id ASC").
|
||||||
|
Order("sp.quantity ASC").
|
||||||
Order("pr.priority DESC").
|
Order("pr.priority DESC").
|
||||||
Order("pr.id ASC").
|
Order("pr.id ASC").
|
||||||
Scan(&candidates).Error
|
Scan(&candidates).Error
|
||||||
|
|||||||
@@ -56,11 +56,19 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi
|
|||||||
var discount []types.SubscribeDiscount
|
var discount []types.SubscribeDiscount
|
||||||
_ = json.Unmarshal([]byte(item.Discount), &discount)
|
_ = json.Unmarshal([]byte(item.Discount), &discount)
|
||||||
sub.Discount = discount
|
sub.Discount = discount
|
||||||
list[i] = sub
|
|
||||||
}
|
}
|
||||||
list[i] = sub
|
list[i] = sub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promos, err := loadSubscribePromoMap(l.ctx, l.svcCtx, subscribeIDs)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[QuerySubscribeListLogic] Query Promo Error", logger.Field("error", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i := range list {
|
||||||
|
applySubscribeDiscountPromos(&list[i], promos[list[i].Id])
|
||||||
|
}
|
||||||
|
|
||||||
// 老版本客户端(无 X-App-Id)去掉每个套餐 discount 的最后一个
|
// 老版本客户端(无 X-App-Id)去掉每个套餐 discount 的最后一个
|
||||||
hasAppId, _ := l.ctx.Value(constant.CtxKeyHasAppId).(bool)
|
hasAppId, _ := l.ctx.Value(constant.CtxKeyHasAppId).(bool)
|
||||||
if !hasAppId {
|
if !hasAppId {
|
||||||
@@ -71,16 +79,13 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
promos, err := loadSubscribePromoMap(l.ctx, l.svcCtx, subscribeIDs)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[QuerySubscribeListLogic] Query Promo Error", logger.Field("error", err.Error()))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for i := range list {
|
|
||||||
list[i].Promo = promos[list[i].Id]
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.List = list
|
resp.List = list
|
||||||
resp.Total = int64(len(list))
|
resp.Total = int64(len(list))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applySubscribeDiscountPromos(subscribe *types.Subscribe, promoByQuantity map[int64]*types.SubscribePromo) {
|
||||||
|
for i := range subscribe.Discount {
|
||||||
|
subscribe.Discount[i].Promo = promoByQuantity[subscribe.Discount[i].Quantity]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2789,7 +2789,6 @@ type Subscribe struct {
|
|||||||
UnitPrice int64 `json:"unit_price"`
|
UnitPrice int64 `json:"unit_price"`
|
||||||
UnitTime string `json:"unit_time"`
|
UnitTime string `json:"unit_time"`
|
||||||
Discount []SubscribeDiscount `json:"discount"`
|
Discount []SubscribeDiscount `json:"discount"`
|
||||||
Promo *SubscribePromo `json:"promo"`
|
|
||||||
NodeCount int64 `json:"node_count"`
|
NodeCount int64 `json:"node_count"`
|
||||||
Replacement int64 `json:"replacement"`
|
Replacement int64 `json:"replacement"`
|
||||||
Inventory int64 `json:"inventory"`
|
Inventory int64 `json:"inventory"`
|
||||||
@@ -2850,10 +2849,11 @@ type SubscribeConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SubscribeDiscount struct {
|
type SubscribeDiscount struct {
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount float64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
NewUserOnly bool `json:"new_user_only"`
|
NewUserOnly bool `json:"new_user_only"`
|
||||||
MapApple string `json:"map_apple"`
|
MapApple string `json:"map_apple"`
|
||||||
|
Promo *SubscribePromo `json:"promo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscribeGroup struct {
|
type SubscribeGroup struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user