修复(#75): 适配促销数量与永久订阅判定
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type PromoResult struct {
|
||||
@@ -148,7 +149,12 @@ func evaluateInactiveUserPromo(
|
||||
err := db.WithContext(ctx).
|
||||
Model(&user.Subscribe{}).
|
||||
Where("user_id = ?", userID).
|
||||
Order("expire_time DESC").
|
||||
Order(clause.OrderBy{
|
||||
Expression: clause.Expr{
|
||||
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END, expire_time DESC",
|
||||
Vars: []interface{}{time.UnixMilli(0)},
|
||||
},
|
||||
}).
|
||||
Limit(1).
|
||||
Take(&lastSub).Error
|
||||
if err != nil {
|
||||
|
||||
@@ -32,19 +32,20 @@ func (m *fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB)
|
||||
}
|
||||
|
||||
func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) {
|
||||
svcCtx := &svc.ServiceContext{
|
||||
DB: &gorm.DB{},
|
||||
PromoModel: &fakePromoModel{rules: []*promo.RuleWithPrice{
|
||||
{
|
||||
Rule: promo.Rule{
|
||||
Id: 9,
|
||||
Name: "campaign",
|
||||
Type: promo.RuleTypeCampaign,
|
||||
Enabled: true,
|
||||
},
|
||||
PromoPrice: 600,
|
||||
model := &fakePromoModel{rules: []*promo.RuleWithPrice{
|
||||
{
|
||||
Rule: promo.Rule{
|
||||
Id: 9,
|
||||
Name: "campaign",
|
||||
Type: promo.RuleTypeCampaign,
|
||||
Enabled: true,
|
||||
},
|
||||
}},
|
||||
PromoPrice: 600,
|
||||
},
|
||||
}}
|
||||
svcCtx := &svc.ServiceContext{
|
||||
DB: &gorm.DB{},
|
||||
PromoModel: model,
|
||||
}
|
||||
|
||||
result, err := calculatePurchasePrice(
|
||||
@@ -77,22 +78,26 @@ func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) {
|
||||
if result.PromoDiscount != 1200 {
|
||||
t.Fatalf("PromoDiscount = %d, want 1200", result.PromoDiscount)
|
||||
}
|
||||
if model.lastQuantity != 3 {
|
||||
t.Fatalf("promo query quantity = %d, want 3", model.lastQuantity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) {
|
||||
svcCtx := &svc.ServiceContext{
|
||||
DB: &gorm.DB{},
|
||||
PromoModel: &fakePromoModel{rules: []*promo.RuleWithPrice{
|
||||
{
|
||||
Rule: promo.Rule{
|
||||
Id: 10,
|
||||
Name: "invalid campaign",
|
||||
Type: promo.RuleTypeCampaign,
|
||||
Enabled: true,
|
||||
},
|
||||
PromoPrice: 1000,
|
||||
model := &fakePromoModel{rules: []*promo.RuleWithPrice{
|
||||
{
|
||||
Rule: promo.Rule{
|
||||
Id: 10,
|
||||
Name: "invalid campaign",
|
||||
Type: promo.RuleTypeCampaign,
|
||||
Enabled: true,
|
||||
},
|
||||
}},
|
||||
PromoPrice: 1000,
|
||||
},
|
||||
}}
|
||||
svcCtx := &svc.ServiceContext{
|
||||
DB: &gorm.DB{},
|
||||
PromoModel: model,
|
||||
}
|
||||
|
||||
result, err := calculatePurchasePrice(
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -179,11 +180,7 @@ func (e *promoEligibilityEvaluator) lastSubscribeExpireAt() (time.Time, error) {
|
||||
return *e.lastExpire, nil
|
||||
}
|
||||
var item user.Subscribe
|
||||
err := e.db.WithContext(e.ctx).
|
||||
Model(&user.Subscribe{}).
|
||||
Where("user_id = ?", e.userInfo.Id).
|
||||
Where("expire_time != ?", time.UnixMilli(0)).
|
||||
Order("expire_time DESC").
|
||||
err := e.lastSubscribeExpireQuery().
|
||||
Limit(1).
|
||||
Take(&item).Error
|
||||
if err != nil {
|
||||
@@ -198,6 +195,18 @@ func (e *promoEligibilityEvaluator) lastSubscribeExpireAt() (time.Time, error) {
|
||||
return item.ExpireTime, nil
|
||||
}
|
||||
|
||||
func (e *promoEligibilityEvaluator) lastSubscribeExpireQuery() *gorm.DB {
|
||||
return e.db.WithContext(e.ctx).
|
||||
Model(&user.Subscribe{}).
|
||||
Where("user_id = ?", e.userInfo.Id).
|
||||
Order(clause.OrderBy{
|
||||
Expression: clause.Expr{
|
||||
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END, expire_time DESC",
|
||||
Vars: []interface{}{time.UnixMilli(0)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (c subscribePromoCandidate) expiresAt() time.Time {
|
||||
if c.EndTime == nil {
|
||||
return time.Time{}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package subscribe
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestPromoEligibilityEvaluatorMatch(t *testing.T) {
|
||||
@@ -73,3 +76,34 @@ func TestSubscribePromoCandidateActiveWindow(t *testing.T) {
|
||||
t.Fatal("candidate after end time should not be active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastSubscribeExpireAtPrioritizesPermanentSubscription(t *testing.T) {
|
||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local",
|
||||
SkipInitializeWithVersion: true,
|
||||
}), &gorm.Config{DryRun: true, DisableAutomaticPing: true})
|
||||
if err != nil {
|
||||
t.Fatalf("open dry-run db: %v", err)
|
||||
}
|
||||
|
||||
evaluator := &promoEligibilityEvaluator{
|
||||
db: db,
|
||||
userInfo: &user.User{Id: 7},
|
||||
}
|
||||
var item user.Subscribe
|
||||
tx := evaluator.lastSubscribeExpireQuery().Limit(1).Take(&item)
|
||||
|
||||
sql := tx.Statement.SQL.String()
|
||||
if !strings.Contains(sql, "CASE WHEN expire_time = ? THEN 0 ELSE 1 END") {
|
||||
t.Fatalf("SQL missing permanent subscription priority order: %s", sql)
|
||||
}
|
||||
if strings.Contains(sql, "expire_time !=") {
|
||||
t.Fatalf("SQL should not filter out permanent subscriptions: %s", sql)
|
||||
}
|
||||
if len(tx.Statement.Vars) < 2 {
|
||||
t.Fatalf("SQL vars length = %d, want at least 2; vars=%v", len(tx.Statement.Vars), tx.Statement.Vars)
|
||||
}
|
||||
if got, want := tx.Statement.Vars[1], time.UnixMilli(0); got != want {
|
||||
t.Fatalf("permanent subscription order var = %v, want %v; vars=%v", got, want, tx.Statement.Vars)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,18 +27,22 @@ func NewModel(db *gorm.DB, _ *redis.Client) Model {
|
||||
|
||||
func (m *defaultPromoModel) QueryEligibleRules(ctx context.Context, subscribeId int64, quantity int64) ([]*RuleWithPrice, error) {
|
||||
var list []*RuleWithPrice
|
||||
err := m.db.WithContext(ctx).
|
||||
Table("promo_rule AS pr").
|
||||
Select("pr.*, sp.promo_price").
|
||||
Joins("JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id").
|
||||
Where("sp.subscribe_id = ? AND sp.quantity = ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeId, quantity, true).
|
||||
Where("pr.deleted_at IS NULL").
|
||||
Order("pr.priority DESC").
|
||||
Order("pr.id ASC").
|
||||
err := m.eligibleRulesQuery(ctx, subscribeId, quantity).
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) eligibleRulesQuery(ctx context.Context, subscribeId int64, quantity int64) *gorm.DB {
|
||||
return m.db.WithContext(ctx).
|
||||
Table("promo_rule AS pr").
|
||||
Select("pr.*, sp.promo_price").
|
||||
Joins("JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id AND sp.quantity = ?", quantity).
|
||||
Where("sp.subscribe_id = ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeId, true).
|
||||
Where("pr.deleted_at IS NULL").
|
||||
Order("pr.priority DESC").
|
||||
Order("pr.id ASC")
|
||||
}
|
||||
|
||||
func (m *defaultPromoModel) InsertUsage(ctx context.Context, data *Usage, tx ...*gorm.DB) error {
|
||||
db := m.db.WithContext(ctx)
|
||||
if len(tx) > 0 {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package promo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestQueryEligibleRulesFiltersByQuantity(t *testing.T) {
|
||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local",
|
||||
SkipInitializeWithVersion: true,
|
||||
}), &gorm.Config{DryRun: true, DisableAutomaticPing: true})
|
||||
if err != nil {
|
||||
t.Fatalf("open dry-run db: %v", err)
|
||||
}
|
||||
|
||||
model := &defaultPromoModel{db: db}
|
||||
var list []*RuleWithPrice
|
||||
tx := model.eligibleRulesQuery(context.Background(), 11, 3).Find(&list)
|
||||
stmt := tx.Statement
|
||||
sql := stmt.SQL.String()
|
||||
if !strings.Contains(sql, "JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id AND sp.quantity = ?") {
|
||||
t.Fatalf("SQL missing quantity join condition: %s", sql)
|
||||
}
|
||||
|
||||
if len(stmt.Vars) < 2 {
|
||||
t.Fatalf("SQL vars length = %d, want at least 2; vars=%v", len(stmt.Vars), stmt.Vars)
|
||||
}
|
||||
if got, want := stmt.Vars[0], int64(3); got != want {
|
||||
t.Fatalf("first SQL var = %v, want quantity %d; vars=%v", got, want, stmt.Vars)
|
||||
}
|
||||
if got, want := stmt.Vars[1], int64(11); got != want {
|
||||
t.Fatalf("second SQL var = %v, want subscribe_id %d; vars=%v", got, want, stmt.Vars)
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ type SubscribePromo struct {
|
||||
SubscribeId int64 `gorm:"type:bigint unsigned;not null;comment:Subscribe ID"`
|
||||
Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"`
|
||||
PromoRuleId int64 `gorm:"type:bigint unsigned;not null;comment:Promo Rule ID"`
|
||||
Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Subscribe Quantity"`
|
||||
PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
|
||||
Reference in New Issue
Block a user