修复(#85): 按数量匹配订阅促销价

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-27 00:36:24 -07:00
parent 48e507783e
commit ae64cc635d
15 changed files with 209 additions and 41 deletions
+1 -1
View File
@@ -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"`
+5 -1
View File
@@ -4316,12 +4316,16 @@
"discount": { "discount": {
"type": "number", "type": "number",
"format": "double" "format": "double"
},
"promo": {
"$ref": "#/definitions/SubscribePromo"
} }
}, },
"title": "SubscribeDiscount", "title": "SubscribeDiscount",
"required": [ "required": [
"quantity", "quantity",
"discount" "discount",
"promo"
] ]
}, },
"SubscribeGroup": { "SubscribeGroup": {
@@ -18,15 +18,70 @@ CREATE TABLE IF NOT EXISTS `promo_rule` (
CREATE TABLE IF NOT EXISTS `subscribe_promo` ( CREATE TABLE IF NOT EXISTS `subscribe_promo` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID', `subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID',
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT '购买数量',
`promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID', `promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID',
`promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)', `promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_subscribe_rule` (`subscribe_id`, `promo_rule_id`), UNIQUE KEY `uk_subscribe_quantity_rule` (`subscribe_id`, `quantity`, `promo_rule_id`),
KEY `idx_promo_rule_id` (`promo_rule_id`) KEY `idx_promo_rule_id` (`promo_rule_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格促销价表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格促销价表';
SET @column_exists = (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'subscribe_promo'
AND COLUMN_NAME = 'quantity'
);
SET @sql = IF(
@column_exists = 0,
'ALTER TABLE `subscribe_promo` ADD COLUMN `quantity` BIGINT NOT NULL DEFAULT 1 COMMENT ''购买数量'' AFTER `subscribe_id`',
'SELECT ''Column quantity already exists in subscribe_promo table'''
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @index_exists = (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'subscribe_promo'
AND INDEX_NAME = 'uk_subscribe_rule'
);
SET @sql = IF(
@index_exists = 1,
'ALTER TABLE `subscribe_promo` DROP INDEX `uk_subscribe_rule`',
'SELECT ''Index uk_subscribe_rule does not exist on subscribe_promo table'''
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @index_exists = (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'subscribe_promo'
AND INDEX_NAME = 'uk_subscribe_quantity_rule'
);
SET @sql = IF(
@index_exists = 0,
'ALTER TABLE `subscribe_promo` ADD UNIQUE KEY `uk_subscribe_quantity_rule` (`subscribe_id`, `quantity`, `promo_rule_id`)',
'SELECT ''Index uk_subscribe_quantity_rule already exists on subscribe_promo table'''
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS `promo_usage` ( CREATE TABLE IF NOT EXISTS `promo_usage` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户 ID', `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
+3 -3
View File
@@ -27,13 +27,13 @@ type promoRuleParams struct {
InactiveMonths int `json:"inactive_months"` InactiveMonths int `json:"inactive_months"`
} }
func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64) (*PromoResult, error) { func EvaluatePromo(ctx context.Context, svcCtx *svc.ServiceContext, userID int64, subscribeID int64, quantity int64) (*PromoResult, error) {
result := &PromoResult{} result := &PromoResult{}
if svcCtx == nil || svcCtx.PromoModel == nil || svcCtx.DB == nil || userID <= 0 || subscribeID <= 0 { if svcCtx == nil || svcCtx.PromoModel == nil || svcCtx.DB == nil || userID <= 0 || subscribeID <= 0 || quantity <= 0 {
return result, nil return result, nil
} }
rules, err := svcCtx.PromoModel.QueryEligibleRules(ctx, subscribeID) rules, err := svcCtx.PromoModel.QueryEligibleRules(ctx, subscribeID, quantity)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo rules failed: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo rules failed: %v", err.Error())
} }
@@ -49,6 +49,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
} }
targetSubscribeID := req.SubscribeId targetSubscribeID := req.SubscribeId
orderType := uint8(1)
isSingleModeRenewal := false isSingleModeRenewal := false
decision, routeErr := commonLogic.ResolvePurchaseRoute( decision, routeErr := commonLogic.ResolvePurchaseRoute(
l.ctx, l.ctx,
@@ -68,6 +69,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
targetSubscribeID = decision.ResolvedSubscribeID targetSubscribeID = decision.ResolvedSubscribeID
isSingleModeRenewal = decision.Route == commonLogic.PurchaseRoutePurchaseToRenewal isSingleModeRenewal = decision.Route == commonLogic.PurchaseRoutePurchaseToRenewal
if isSingleModeRenewal && decision.Anchor != nil { if isSingleModeRenewal && decision.Anchor != nil {
orderType = 2
l.Infow("[PreCreateOrder] single mode purchase routed to renewal preview", l.Infow("[PreCreateOrder] single mode purchase routed to renewal preview",
logger.Field("mode", "single"), logger.Field("mode", "single"),
logger.Field("route", "purchase_to_renewal"), logger.Field("route", "purchase_to_renewal"),
@@ -77,6 +79,32 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
} }
} }
// Keep promo eligibility preview aligned with Purchase: an existing paid subscription
// routes the request to renewal semantics, where first-purchase promos are disabled.
if !l.svcCtx.Config.Subscribe.SingleModel && orderType == 1 {
var existSub user.Subscribe
if e := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Subscribe{}).
Where("user_id = ? AND token != '' AND (order_id > 0 OR token LIKE 'iap:%')", u.Id).
Order("expire_time DESC").
Order("updated_at DESC").
Order("id DESC").
First(&existSub).Error; e == nil && existSub.Id > 0 && existSub.Token != "" {
orderType = 2
l.Infow("[PreCreateOrder] purchase preview routed to renewal because an existing subscription was found",
logger.Field("route_mode", "global_single_subscription"),
logger.Field("route", "purchase_to_existing_subscription"),
logger.Field("existing_subscribe_id", existSub.Id),
logger.Field("existing_status", existSub.Status),
logger.Field("user_id", u.Id),
logger.Field("resolved_subscribe_id", targetSubscribeID),
)
} else if e != nil && !errors.Is(e, gorm.ErrRecordNotFound) {
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", e.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find existing subscription error: %v", e.Error())
}
}
// find subscribe plan // find subscribe plan
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, targetSubscribeID) sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, targetSubscribeID)
if err != nil { if err != nil {
@@ -123,7 +151,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
req.Quantity, req.Quantity,
newUserDiscount.Discounts, newUserDiscount.Discounts,
newUserDiscount.EligibleForDiscount, newUserDiscount.EligibleForDiscount,
!isSingleModeRenewal, orderType == 1,
) )
if err != nil { if err != nil {
l.Errorw("[PreCreateOrder] Promo price calculation error", l.Errorw("[PreCreateOrder] Promo price calculation error",
+1 -1
View File
@@ -36,7 +36,7 @@ func calculatePurchasePrice(
} }
if allowPromo { if allowPromo {
promoResult, err := commonLogic.EvaluatePromo(ctx, svcCtx, userID, subscribeID) promoResult, err := commonLogic.EvaluatePromo(ctx, svcCtx, userID, subscribeID, quantity)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -12,20 +12,29 @@ import (
type fakePromoModel struct { type fakePromoModel struct {
rules []*promo.RuleWithPrice rules []*promo.RuleWithPrice
lastSubscribeID int64
lastQuantity int64
requireQuantity int64
quantityMismatch []*promo.RuleWithPrice
} }
func (m fakePromoModel) QueryEligibleRules(context.Context, int64) ([]*promo.RuleWithPrice, error) { func (m *fakePromoModel) QueryEligibleRules(_ context.Context, subscribeID int64, quantity int64) ([]*promo.RuleWithPrice, error) {
m.lastSubscribeID = subscribeID
m.lastQuantity = quantity
if m.requireQuantity > 0 && quantity != m.requireQuantity {
return m.quantityMismatch, nil
}
return m.rules, nil return m.rules, nil
} }
func (m fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) error { func (m *fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) error {
return nil return nil
} }
func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) { func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) {
svcCtx := &svc.ServiceContext{ svcCtx := &svc.ServiceContext{
DB: &gorm.DB{}, DB: &gorm.DB{},
PromoModel: fakePromoModel{rules: []*promo.RuleWithPrice{ PromoModel: &fakePromoModel{rules: []*promo.RuleWithPrice{
{ {
Rule: promo.Rule{ Rule: promo.Rule{
Id: 9, Id: 9,
@@ -73,7 +82,7 @@ func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) {
func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) { func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) {
svcCtx := &svc.ServiceContext{ svcCtx := &svc.ServiceContext{
DB: &gorm.DB{}, DB: &gorm.DB{},
PromoModel: fakePromoModel{rules: []*promo.RuleWithPrice{ PromoModel: &fakePromoModel{rules: []*promo.RuleWithPrice{
{ {
Rule: promo.Rule{ Rule: promo.Rule{
Id: 10, Id: 10,
@@ -111,3 +120,52 @@ func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) {
t.Fatalf("promo fields = (%d, %d), want (0, 0)", result.PromoRuleId, result.PromoDiscount) t.Fatalf("promo fields = (%d, %d), want (0, 0)", result.PromoRuleId, result.PromoDiscount)
} }
} }
func TestCalculatePurchasePricePassesQuantityToPromoEvaluation(t *testing.T) {
promoModel := &fakePromoModel{
requireQuantity: 6,
rules: []*promo.RuleWithPrice{
{
Rule: promo.Rule{
Id: 11,
Name: "quantity campaign",
Type: promo.RuleTypeCampaign,
Enabled: true,
},
PromoPrice: 500,
},
},
}
svcCtx := &svc.ServiceContext{
DB: &gorm.DB{},
PromoModel: promoModel,
}
result, err := calculatePurchasePrice(
context.Background(),
svcCtx,
1,
2,
1000,
6,
[]types.SubscribeDiscount{{Quantity: 6, Discount: 80}},
true,
true,
)
if err != nil {
t.Fatalf("calculatePurchasePrice returned error: %v", err)
}
if promoModel.lastSubscribeID != 2 {
t.Fatalf("lastSubscribeID = %d, want 2", promoModel.lastSubscribeID)
}
if promoModel.lastQuantity != 6 {
t.Fatalf("lastQuantity = %d, want 6", promoModel.lastQuantity)
}
if result.PayableBase != 3000 {
t.Fatalf("PayableBase = %d, want 3000", result.PayableBase)
}
if result.PromoRuleId != 11 {
t.Fatalf("PromoRuleId = %d, want 11", result.PromoRuleId)
}
}
+13 -5
View File
@@ -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,13 @@ 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 candidate.Quantity <= 0 {
continue
}
if result[candidate.SubscribeId] == nil {
result[candidate.SubscribeId] = make(map[int64]*types.SubscribePromo)
}
if _, exists := result[candidate.SubscribeId][candidate.Quantity]; exists {
continue continue
} }
if !candidate.isActive(now) { if !candidate.isActive(now) {
@@ -69,7 +76,7 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
if !ok { if !ok {
continue continue
} }
result[candidate.SubscribeId] = &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 +91,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 +99,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
@@ -77,7 +77,13 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi
return nil, err return nil, err
} }
for i := range list { for i := range list {
list[i].Promo = promos[list[i].Id] subscribePromos := promos[list[i].Id]
for j := range list[i].Discount {
if subscribePromos == nil {
continue
}
list[i].Discount[j].Promo = subscribePromos[list[i].Discount[j].Quantity]
}
} }
resp.List = list resp.List = list
+3 -3
View File
@@ -13,7 +13,7 @@ type RuleWithPrice struct {
} }
type Model interface { type Model interface {
QueryEligibleRules(ctx context.Context, subscribeId int64) ([]*RuleWithPrice, error) QueryEligibleRules(ctx context.Context, subscribeId int64, quantity int64) ([]*RuleWithPrice, error)
InsertUsage(ctx context.Context, data *Usage, tx ...*gorm.DB) error InsertUsage(ctx context.Context, data *Usage, tx ...*gorm.DB) error
} }
@@ -25,13 +25,13 @@ func NewModel(db *gorm.DB, _ *redis.Client) Model {
return &defaultPromoModel{db: db} return &defaultPromoModel{db: db}
} }
func (m *defaultPromoModel) QueryEligibleRules(ctx context.Context, subscribeId int64) ([]*RuleWithPrice, error) { func (m *defaultPromoModel) QueryEligibleRules(ctx context.Context, subscribeId int64, quantity int64) ([]*RuleWithPrice, error) {
var list []*RuleWithPrice var list []*RuleWithPrice
err := m.db.WithContext(ctx). err := m.db.WithContext(ctx).
Table("promo_rule AS pr"). Table("promo_rule AS pr").
Select("pr.*, sp.promo_price"). Select("pr.*, sp.promo_price").
Joins("JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id"). Joins("JOIN subscribe_promo AS sp ON sp.promo_rule_id = pr.id").
Where("sp.subscribe_id = ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeId, true). Where("sp.subscribe_id = ? AND sp.quantity = ? AND sp.promo_price > 0 AND pr.enabled = ?", subscribeId, quantity, true).
Where("pr.deleted_at IS NULL"). Where("pr.deleted_at IS NULL").
Order("pr.priority DESC"). Order("pr.priority DESC").
Order("pr.id ASC"). Order("pr.id ASC").
+1
View File
@@ -33,6 +33,7 @@ func (Rule) TableName() string {
type SubscribePromo struct { type SubscribePromo struct {
Id int64 `gorm:"primaryKey"` Id int64 `gorm:"primaryKey"`
SubscribeId int64 `gorm:"type:bigint unsigned;not null;comment:Subscribe ID"` 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"` PromoRuleId int64 `gorm:"type:bigint unsigned;not null;comment:Promo Rule ID"`
PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"` PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
+1 -1
View File
@@ -2800,7 +2800,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"`
@@ -2865,6 +2864,7 @@ type SubscribeDiscount struct {
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 {
+5 -1
View File
@@ -3680,12 +3680,16 @@
"discount": { "discount": {
"type": "number", "type": "number",
"format": "double" "format": "double"
},
"promo": {
"$ref": "#/definitions/SubscribePromo"
} }
}, },
"title": "SubscribeDiscount", "title": "SubscribeDiscount",
"required": [ "required": [
"quantity", "quantity",
"discount" "discount",
"promo"
] ]
}, },
"SubscribeGroup": { "SubscribeGroup": {
+12 -12
View File
@@ -149,7 +149,14 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
return err return err
} }
l.recordPromoUsage(ctx, orderInfo) if err = l.recordPromoUsage(ctx, orderInfo); err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] 促销使用记录写入失败,将重试",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("promo_rule_id", orderInfo.PromoRuleId),
logger.Field("error", err.Error()),
)
return err
}
l.finalizeCouponAndOrder(ctx, orderInfo) l.finalizeCouponAndOrder(ctx, orderInfo)
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_finished", commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_finished",
@@ -159,9 +166,9 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
return nil return nil
} }
func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *order.Order) { func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *order.Order) error {
if orderInfo == nil || orderInfo.PromoRuleId <= 0 || orderInfo.Quantity <= 0 || orderInfo.SubscribeId <= 0 || orderInfo.OrderNo == "" { if orderInfo == nil || orderInfo.PromoRuleId <= 0 || orderInfo.Quantity <= 0 || orderInfo.SubscribeId <= 0 || orderInfo.OrderNo == "" {
return return nil
} }
promoPrice := int64(0) promoPrice := int64(0)
@@ -169,10 +176,10 @@ func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *or
promoPrice = (orderInfo.Price - orderInfo.PromoDiscount) / orderInfo.Quantity promoPrice = (orderInfo.Price - orderInfo.PromoDiscount) / orderInfo.Quantity
} }
if promoPrice <= 0 { if promoPrice <= 0 {
return return nil
} }
err := l.svc.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return l.svc.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64 var count int64
if e := tx.Model(&promo.Usage{}).Where("order_no = ?", orderInfo.OrderNo).Count(&count).Error; e != nil { if e := tx.Model(&promo.Usage{}).Where("order_no = ?", orderInfo.OrderNo).Count(&count).Error; e != nil {
return e return e
@@ -188,13 +195,6 @@ func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *or
PromoPrice: promoPrice, PromoPrice: promoPrice,
}, tx) }, tx)
}) })
if err != nil {
logger.WithContext(ctx).Error("Insert promo usage failed",
logger.Field("error", err.Error()),
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("promo_rule_id", orderInfo.PromoRuleId),
)
}
} }
// parsePayload unMarshals the task payload into a structured format // parsePayload unMarshals the task payload into a structured format
+5 -1
View File
@@ -5900,12 +5900,16 @@
"discount": { "discount": {
"type": "number", "type": "number",
"format": "double" "format": "double"
},
"promo": {
"$ref": "#/definitions/SubscribePromo"
} }
}, },
"title": "SubscribeDiscount", "title": "SubscribeDiscount",
"required": [ "required": [
"quantity", "quantity",
"discount" "discount",
"promo"
] ]
}, },
"SubscribeGroup": { "SubscribeGroup": {