Compare commits
3 Commits
internal
...
1540d830e1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1540d830e1 | |||
| 25811526bd | |||
| fc8daaef56 |
@@ -29,7 +29,7 @@ func (l *DeleteSubscribeApplicationLogic) DeleteSubscribeApplication(req *types.
|
||||
err := l.svcCtx.ClientModel.Delete(l.ctx, req.Id)
|
||||
if err != nil {
|
||||
l.Errorf("Failed to delete subscribe application with ID %d: %v", req.Id, err)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error())
|
||||
return errors.Wrap(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (l *ResetSortWithNodeLogic) ResetSortWithNode(req *types.ResetSortRequest)
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error()))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error())
|
||||
return errors.Wrap(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (l *ResetSortWithServerLogic) ResetSortWithServer(req *types.ResetSortReque
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error()))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error())
|
||||
return errors.Wrap(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const (
|
||||
PromoRuleTypeNewUser = "new_user"
|
||||
PromoRuleTypeInactiveUser = "inactive_user"
|
||||
PromoRuleTypeCampaign = "campaign"
|
||||
|
||||
promoRulesEnabledCacheKey = "promo:rules:enabled"
|
||||
promoSubscribeCachePrefix = "promo:subscribe:"
|
||||
promoCacheTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
type PromoResult struct {
|
||||
Eligible bool
|
||||
RuleID int64
|
||||
RuleName string
|
||||
RuleType string
|
||||
PromoPrice int64
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type promoRule struct {
|
||||
Id int64 `gorm:"column:id" json:"id"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Type string `gorm:"column:type" json:"type"`
|
||||
Params string `gorm:"column:params" json:"params"`
|
||||
Priority int64 `gorm:"column:priority" json:"priority"`
|
||||
Enabled bool `gorm:"column:enabled" json:"enabled"`
|
||||
StartTime *time.Time `gorm:"column:start_time" json:"start_time,omitempty"`
|
||||
EndTime *time.Time `gorm:"column:end_time" json:"end_time,omitempty"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at" json:"deleted_at"`
|
||||
}
|
||||
|
||||
func (promoRule) TableName() string {
|
||||
return "promo_rule"
|
||||
}
|
||||
|
||||
type subscribePromo struct {
|
||||
Id int64 `gorm:"column:id" json:"id"`
|
||||
SubscribeId int64 `gorm:"column:subscribe_id" json:"subscribe_id"`
|
||||
Quantity int64 `gorm:"column:quantity" json:"quantity"`
|
||||
PromoRuleId int64 `gorm:"column:promo_rule_id" json:"promo_rule_id"`
|
||||
PromoPrice int64 `gorm:"column:promo_price" json:"promo_price"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (subscribePromo) TableName() string {
|
||||
return "subscribe_promo"
|
||||
}
|
||||
|
||||
type promoRuleParams struct {
|
||||
WindowHours int64 `json:"window_hours"`
|
||||
InactiveMonths int `json:"inactive_months"`
|
||||
}
|
||||
|
||||
type promoEligibilitySource interface {
|
||||
UserCreatedAt(ctx context.Context, userID int64) (time.Time, error)
|
||||
LastSubscribeExpireAt(ctx context.Context, userID int64) (time.Time, error)
|
||||
}
|
||||
|
||||
type gormPromoEligibilitySource struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64, quantity int64) (*PromoResult, error) {
|
||||
if svcCtx == nil || svcCtx.DB == nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "service context is empty")
|
||||
}
|
||||
if userID <= 0 || subscribeID <= 0 || quantity <= 0 {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "user id, subscribe id or quantity is empty")
|
||||
}
|
||||
|
||||
rules, err := loadEnabledPromoRules(ctx, svcCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return &PromoResult{}, nil
|
||||
}
|
||||
|
||||
prices, err := loadSubscribePromos(ctx, svcCtx, subscribeID, quantity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(prices) == 0 {
|
||||
return &PromoResult{}, nil
|
||||
}
|
||||
|
||||
return evaluatePromoRulesAt(ctx, rules, prices, &gormPromoEligibilitySource{db: svcCtx.DB}, userID, time.Now())
|
||||
}
|
||||
|
||||
func evaluatePromoRulesAt(
|
||||
ctx context.Context,
|
||||
rules []promoRule,
|
||||
prices []subscribePromo,
|
||||
source promoEligibilitySource,
|
||||
userID int64,
|
||||
now time.Time,
|
||||
) (*PromoResult, error) {
|
||||
priceByRuleID := make(map[int64]int64, len(prices))
|
||||
for _, item := range prices {
|
||||
if item.PromoRuleId <= 0 || item.PromoPrice <= 0 {
|
||||
continue
|
||||
}
|
||||
priceByRuleID[item.PromoRuleId] = item.PromoPrice
|
||||
}
|
||||
|
||||
var userCreatedAt time.Time
|
||||
var userCreatedAtLoaded bool
|
||||
var lastExpireAt time.Time
|
||||
var lastExpireAtLoaded bool
|
||||
|
||||
for _, rule := range rules {
|
||||
promoPrice, ok := priceByRuleID[rule.Id]
|
||||
if !ok || !rule.isAvailableAt(now) {
|
||||
continue
|
||||
}
|
||||
|
||||
params, err := rule.params()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var eligible bool
|
||||
var expiresAt time.Time
|
||||
switch rule.Type {
|
||||
case PromoRuleTypeNewUser:
|
||||
if !userCreatedAtLoaded {
|
||||
userCreatedAt, err = source.UserCreatedAt(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userCreatedAtLoaded = true
|
||||
}
|
||||
eligible, expiresAt = evaluatePromoNewUser(userCreatedAt, params, now)
|
||||
case PromoRuleTypeInactiveUser:
|
||||
if !lastExpireAtLoaded {
|
||||
lastExpireAt, err = source.LastSubscribeExpireAt(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastExpireAtLoaded = true
|
||||
}
|
||||
eligible, expiresAt = evaluatePromoInactiveUser(lastExpireAt, params, rule, now)
|
||||
case PromoRuleTypeCampaign:
|
||||
eligible, expiresAt = evaluatePromoCampaign(rule)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if !eligible {
|
||||
continue
|
||||
}
|
||||
return &PromoResult{
|
||||
Eligible: true,
|
||||
RuleID: rule.Id,
|
||||
RuleName: rule.Name,
|
||||
RuleType: rule.Type,
|
||||
PromoPrice: promoPrice,
|
||||
ExpiresAt: expiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &PromoResult{}, nil
|
||||
}
|
||||
|
||||
func evaluatePromoNewUser(userCreatedAt time.Time, params promoRuleParams, now time.Time) (bool, time.Time) {
|
||||
if userCreatedAt.IsZero() || params.WindowHours <= 0 {
|
||||
return false, time.Time{}
|
||||
}
|
||||
expiresAt := userCreatedAt.Add(time.Duration(params.WindowHours) * time.Hour)
|
||||
return now.Before(expiresAt), expiresAt
|
||||
}
|
||||
|
||||
func evaluatePromoInactiveUser(lastExpireAt time.Time, params promoRuleParams, rule promoRule, now time.Time) (bool, time.Time) {
|
||||
if params.InactiveMonths <= 0 {
|
||||
return false, time.Time{}
|
||||
}
|
||||
if lastExpireAt.Equal(time.UnixMilli(0)) || lastExpireAt.After(now) {
|
||||
return false, time.Time{}
|
||||
}
|
||||
if lastExpireAt.IsZero() {
|
||||
return true, rule.expiresAt()
|
||||
}
|
||||
|
||||
threshold := now.AddDate(0, -params.InactiveMonths, 0)
|
||||
return !lastExpireAt.After(threshold), rule.expiresAt()
|
||||
}
|
||||
|
||||
func evaluatePromoCampaign(rule promoRule) (bool, time.Time) {
|
||||
return true, rule.expiresAt()
|
||||
}
|
||||
|
||||
func (r promoRule) isAvailableAt(now time.Time) bool {
|
||||
if r.Id <= 0 || !r.Enabled || r.DeletedAt.Valid {
|
||||
return false
|
||||
}
|
||||
if r.StartTime != nil && now.Before(*r.StartTime) {
|
||||
return false
|
||||
}
|
||||
if r.EndTime != nil && now.After(*r.EndTime) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r promoRule) expiresAt() time.Time {
|
||||
if r.EndTime == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return *r.EndTime
|
||||
}
|
||||
|
||||
func (r promoRule) params() (promoRuleParams, error) {
|
||||
if r.Params == "" {
|
||||
return promoRuleParams{}, nil
|
||||
}
|
||||
var params promoRuleParams
|
||||
if err := json.Unmarshal([]byte(r.Params), ¶ms); err != nil {
|
||||
return promoRuleParams{}, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "parse promo rule params failed")
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (s *gormPromoEligibilitySource) UserCreatedAt(ctx context.Context, userID int64) (time.Time, error) {
|
||||
var item user.User
|
||||
err := s.db.WithContext(ctx).
|
||||
Model(&user.User{}).
|
||||
Where("id = ?", userID).
|
||||
Take(&item).Error
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo user failed")
|
||||
}
|
||||
return item.CreatedAt, nil
|
||||
}
|
||||
|
||||
func (s *gormPromoEligibilitySource) LastSubscribeExpireAt(ctx context.Context, userID int64) (time.Time, error) {
|
||||
var item user.Subscribe
|
||||
err := lastSubscribeExpireQuery(s.db.WithContext(ctx), userID).
|
||||
Take(&item).Error
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo user last subscription failed")
|
||||
}
|
||||
return item.ExpireTime, nil
|
||||
}
|
||||
|
||||
func lastSubscribeExpireQuery(db *gorm.DB, userID int64) *gorm.DB {
|
||||
return db.
|
||||
Model(&user.Subscribe{}).
|
||||
Where("user_id = ?", userID).
|
||||
Order(clause.OrderBy{Expression: clause.Expr{
|
||||
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END ASC, expire_time DESC",
|
||||
Vars: []interface{}{time.UnixMilli(0)},
|
||||
}}).
|
||||
Limit(1)
|
||||
}
|
||||
|
||||
func loadEnabledPromoRules(ctx context.Context, svcCtx *svc.ServiceContext) ([]promoRule, error) {
|
||||
if cached, ok := getPromoCache[[]promoRule](ctx, svcCtx, promoRulesEnabledCacheKey); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var rules []promoRule
|
||||
if err := svcCtx.DB.WithContext(ctx).
|
||||
Model(&promoRule{}).
|
||||
Where("enabled = ?", true).
|
||||
Order("priority DESC").
|
||||
Order("id ASC").
|
||||
Find(&rules).Error; err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query enabled promo rules failed")
|
||||
}
|
||||
|
||||
setPromoCache(ctx, svcCtx, promoRulesEnabledCacheKey, rules)
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func loadSubscribePromos(ctx context.Context, svcCtx *svc.ServiceContext, subscribeID int64, quantity int64) ([]subscribePromo, error) {
|
||||
cacheKey := fmt.Sprintf("%s%d:%d", promoSubscribeCachePrefix, subscribeID, quantity)
|
||||
if cached, ok := getPromoCache[[]subscribePromo](ctx, svcCtx, cacheKey); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var promos []subscribePromo
|
||||
if err := subscribePromoQuery(svcCtx.DB.WithContext(ctx), subscribeID, quantity).
|
||||
Find(&promos).Error; err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe promos failed")
|
||||
}
|
||||
|
||||
setPromoCache(ctx, svcCtx, cacheKey, promos)
|
||||
return promos, nil
|
||||
}
|
||||
|
||||
func subscribePromoQuery(db *gorm.DB, subscribeID int64, quantity int64) *gorm.DB {
|
||||
return db.
|
||||
Model(&subscribePromo{}).
|
||||
Where("subscribe_id = ? AND quantity = ?", subscribeID, quantity)
|
||||
}
|
||||
|
||||
func getPromoCache[T any](ctx context.Context, svcCtx *svc.ServiceContext, key string) (T, bool) {
|
||||
var zero T
|
||||
if svcCtx == nil || svcCtx.Redis == nil {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
value, err := svcCtx.Redis.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
var data T
|
||||
if err = json.Unmarshal([]byte(value), &data); err != nil {
|
||||
_ = svcCtx.Redis.Del(ctx, key).Err()
|
||||
return zero, false
|
||||
}
|
||||
return data, true
|
||||
}
|
||||
|
||||
func setPromoCache(ctx context.Context, svcCtx *svc.ServiceContext, key string, value any) {
|
||||
if svcCtx == nil || svcCtx.Redis == nil {
|
||||
return
|
||||
}
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = svcCtx.Redis.Set(ctx, key, string(payload), promoCacheTTL).Err()
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type fakePromoEligibilitySource struct {
|
||||
userCreatedAt time.Time
|
||||
lastExpireAt time.Time
|
||||
}
|
||||
|
||||
func (s fakePromoEligibilitySource) UserCreatedAt(context.Context, int64) (time.Time, error) {
|
||||
return s.userCreatedAt, nil
|
||||
}
|
||||
|
||||
func (s fakePromoEligibilitySource) LastSubscribeExpireAt(context.Context, int64) (time.Time, error) {
|
||||
return s.lastExpireAt, nil
|
||||
}
|
||||
|
||||
func TestEvaluatePromoRulesAtPriorityFirstMatch(t *testing.T) {
|
||||
now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)
|
||||
campaignEnd := now.Add(24 * time.Hour)
|
||||
rules := []promoRule{
|
||||
{
|
||||
Id: 2,
|
||||
Name: "campaign",
|
||||
Type: PromoRuleTypeCampaign,
|
||||
Priority: 20,
|
||||
Enabled: true,
|
||||
EndTime: &campaignEnd,
|
||||
},
|
||||
{
|
||||
Id: 1,
|
||||
Name: "new user",
|
||||
Type: PromoRuleTypeNewUser,
|
||||
Params: `{"window_hours":168}`,
|
||||
Priority: 10,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
prices := []subscribePromo{
|
||||
{SubscribeId: 100, Quantity: 1, PromoRuleId: 1, PromoPrice: 599},
|
||||
{SubscribeId: 100, Quantity: 1, PromoRuleId: 2, PromoPrice: 499},
|
||||
}
|
||||
|
||||
got, err := evaluatePromoRulesAt(context.Background(), rules, prices, fakePromoEligibilitySource{
|
||||
userCreatedAt: now.Add(-time.Hour),
|
||||
}, 10, now)
|
||||
if err != nil {
|
||||
t.Fatalf("evaluatePromoRulesAt error: %v", err)
|
||||
}
|
||||
if !got.Eligible || got.RuleID != 2 || got.PromoPrice != 499 || got.RuleType != PromoRuleTypeCampaign {
|
||||
t.Fatalf("unexpected promo result: %+v", got)
|
||||
}
|
||||
if !got.ExpiresAt.Equal(campaignEnd) {
|
||||
t.Fatalf("expires_at = %v, want %v", got.ExpiresAt, campaignEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatePromoRulesAtSkipsUnavailableRules(t *testing.T) {
|
||||
now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)
|
||||
futureStart := now.Add(time.Hour)
|
||||
expiredEnd := now.Add(-time.Hour)
|
||||
rules := []promoRule{
|
||||
{Id: 1, Type: PromoRuleTypeCampaign, Enabled: true, StartTime: &futureStart},
|
||||
{Id: 2, Type: PromoRuleTypeCampaign, Enabled: true, EndTime: &expiredEnd},
|
||||
{Id: 3, Type: PromoRuleTypeCampaign, Enabled: false},
|
||||
{Id: 4, Type: "unknown", Enabled: true},
|
||||
{Id: 5, Type: PromoRuleTypeCampaign, Enabled: true},
|
||||
}
|
||||
prices := []subscribePromo{
|
||||
{SubscribeId: 100, Quantity: 1, PromoRuleId: 1, PromoPrice: 100},
|
||||
{SubscribeId: 100, Quantity: 1, PromoRuleId: 2, PromoPrice: 100},
|
||||
{SubscribeId: 100, Quantity: 1, PromoRuleId: 3, PromoPrice: 100},
|
||||
{SubscribeId: 100, Quantity: 1, PromoRuleId: 4, PromoPrice: 100},
|
||||
{SubscribeId: 100, Quantity: 1, PromoRuleId: 5, PromoPrice: 88},
|
||||
}
|
||||
|
||||
got, err := evaluatePromoRulesAt(context.Background(), rules, prices, fakePromoEligibilitySource{}, 10, now)
|
||||
if err != nil {
|
||||
t.Fatalf("evaluatePromoRulesAt error: %v", err)
|
||||
}
|
||||
if !got.Eligible || got.RuleID != 5 || got.PromoPrice != 88 {
|
||||
t.Fatalf("unexpected promo result: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatePromoNewUser(t *testing.T) {
|
||||
now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)
|
||||
createdAt := now.Add(-23 * time.Hour)
|
||||
|
||||
eligible, expiresAt := evaluatePromoNewUser(createdAt, promoRuleParams{WindowHours: 24}, now)
|
||||
if !eligible {
|
||||
t.Fatal("new user should be eligible inside configured window")
|
||||
}
|
||||
if !expiresAt.Equal(createdAt.Add(24 * time.Hour)) {
|
||||
t.Fatalf("expires_at = %v, want %v", expiresAt, createdAt.Add(24*time.Hour))
|
||||
}
|
||||
|
||||
eligible, _ = evaluatePromoNewUser(createdAt, promoRuleParams{WindowHours: 12}, now)
|
||||
if eligible {
|
||||
t.Fatal("new user should not be eligible after configured window")
|
||||
}
|
||||
|
||||
eligible, _ = evaluatePromoNewUser(createdAt, promoRuleParams{}, now)
|
||||
if eligible {
|
||||
t.Fatal("new user should not be eligible without positive window_hours")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatePromoInactiveUser(t *testing.T) {
|
||||
now := time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)
|
||||
endTime := now.Add(48 * time.Hour)
|
||||
rule := promoRule{EndTime: &endTime}
|
||||
|
||||
eligible, expiresAt := evaluatePromoInactiveUser(time.Time{}, promoRuleParams{InactiveMonths: 3}, rule, now)
|
||||
if !eligible {
|
||||
t.Fatal("never purchased user should be eligible for inactive promo")
|
||||
}
|
||||
if !expiresAt.Equal(endTime) {
|
||||
t.Fatalf("expires_at = %v, want %v", expiresAt, endTime)
|
||||
}
|
||||
|
||||
eligible, _ = evaluatePromoInactiveUser(now.AddDate(0, -4, 0), promoRuleParams{InactiveMonths: 3}, rule, now)
|
||||
if !eligible {
|
||||
t.Fatal("expired before inactive threshold should be eligible")
|
||||
}
|
||||
|
||||
eligible, _ = evaluatePromoInactiveUser(now.AddDate(0, -1, 0), promoRuleParams{InactiveMonths: 3}, rule, now)
|
||||
if eligible {
|
||||
t.Fatal("recently expired subscription should not be eligible")
|
||||
}
|
||||
|
||||
eligible, _ = evaluatePromoInactiveUser(time.UnixMilli(0), promoRuleParams{InactiveMonths: 3}, rule, now)
|
||||
if eligible {
|
||||
t.Fatal("unlimited active subscription should not be eligible")
|
||||
}
|
||||
|
||||
eligible, _ = evaluatePromoInactiveUser(now.Add(time.Hour), promoRuleParams{InactiveMonths: 3}, rule, now)
|
||||
if eligible {
|
||||
t.Fatal("currently active subscription should not be eligible")
|
||||
}
|
||||
|
||||
eligible, _ = evaluatePromoInactiveUser(now.AddDate(0, -4, 0), promoRuleParams{}, rule, now)
|
||||
if eligible {
|
||||
t.Fatal("inactive promo should require positive inactive_months")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastSubscribeExpireAtIncludesUnlimitedSubscription(t *testing.T) {
|
||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: "gorm:password@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local",
|
||||
SkipInitializeWithVersion: true,
|
||||
}), &gorm.Config{
|
||||
DryRun: true,
|
||||
DisableAutomaticPing: true,
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open gorm db: %v", err)
|
||||
}
|
||||
|
||||
stmt := lastSubscribeExpireQuery(db, 10).Take(nil).Statement
|
||||
sql := strings.ToLower(stmt.SQL.String())
|
||||
if strings.Contains(sql, "expire_time <>") || strings.Contains(sql, "expire_time !=") {
|
||||
t.Fatalf("last subscribe query should include unlimited subscription, sql: %s", stmt.SQL.String())
|
||||
}
|
||||
if !strings.Contains(sql, "case when expire_time = ? then 0 else 1 end asc") {
|
||||
t.Fatalf("last subscribe query should prioritize unlimited subscription, sql: %s", stmt.SQL.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribePromoQueryMatchesQuantity(t *testing.T) {
|
||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: "gorm:password@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local",
|
||||
SkipInitializeWithVersion: true,
|
||||
}), &gorm.Config{
|
||||
DryRun: true,
|
||||
DisableAutomaticPing: true,
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open gorm db: %v", err)
|
||||
}
|
||||
|
||||
stmt := subscribePromoQuery(db, 100, 3).Find(&[]subscribePromo{}).Statement
|
||||
sql := strings.ToLower(stmt.SQL.String())
|
||||
if !strings.Contains(sql, "subscribe_id = ?") {
|
||||
t.Fatalf("subscribe promo query should filter subscribe_id, sql: %s", stmt.SQL.String())
|
||||
}
|
||||
if !strings.Contains(sql, "quantity = ?") {
|
||||
t.Fatalf("subscribe promo query should filter quantity, sql: %s", stmt.SQL.String())
|
||||
}
|
||||
if got, want := len(stmt.Vars), 2; got != want {
|
||||
t.Fatalf("query vars len = %d, want %d, vars: %#v", got, want, stmt.Vars)
|
||||
}
|
||||
if stmt.Vars[0] != int64(100) || stmt.Vars[1] != int64(3) {
|
||||
t.Fatalf("query vars = %#v, want subscribe_id=100 quantity=3", stmt.Vars)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (l *DeviceWsConnectLogic) DeviceWsConnect(c *gin.Context) error {
|
||||
_, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier)
|
||||
if err != nil && !sysErr.Is(err, gorm.ErrRecordNotFound) {
|
||||
l.Errorf("DeviceWsConnectLogic DeviceWsConnect FindOneDeviceByIdentifier err: %v", err)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||||
return errors.Wrap(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
||||
}
|
||||
|
||||
value = l.ctx.Value(constant.CtxKeyUser)
|
||||
@@ -67,7 +67,7 @@ func (l *DeviceWsConnectLogic) DeviceWsConnect(c *gin.Context) error {
|
||||
err := l.svcCtx.UserModel.InsertDevice(l.ctx, &device)
|
||||
if err != nil {
|
||||
l.Errorf("DeviceWsConnectLogic DeviceWsConnect InsertDevice err: %v", err)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error())
|
||||
return errors.Wrap(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error())
|
||||
}
|
||||
}
|
||||
//默认在线设备1
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build tools
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build tools
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user