From 02b41e7a2c47b7900cc22f5f766686c8107f24a3 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Wed, 27 May 2026 05:57:21 -0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(#85):=20=E4=BF=83=E9=94=80?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=20quantity=20=E8=AE=BE=E8=AE=A1=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E8=A1=A5=E4=B8=81=EF=BC=88=E8=B7=A8=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BF=AE=E5=A4=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - subscribe_promo 表加 quantity 字段,BIGINT NOT NULL DEFAULT 1 - EvaluatePromo 加 quantity 参数,按 subscribeID + quantity 精确匹配 - Promo 从 Subscribe 顶层移到 SubscribeDiscount - 查询加 quantity,返回 map[subscribeID][quantity] 二级映射 - recordPromoUsage 错误向上传播,不再静默吞掉 - preCreate 和 purchase 的 allowPromo 判定统一为 orderType==1 - 迁移脚本增加幂等处理(guarded DROP/ADD/MODIFY) Co-authored-by: multica-agent --- common.json | 3 + .../database/02154_promo_system.up.sql | 140 +++++++++++++++++- internal/logic/common/promoEligibility.go | 6 +- .../logic/public/order/preCreateOrderLogic.go | 44 +++++- internal/logic/public/order/promoPricing.go | 2 +- .../logic/public/order/promoPricing_test.go | 68 ++++++++- internal/logic/public/subscribe/promo.go | 9 +- internal/model/promo/model.go | 6 +- internal/model/promo/promo.go | 1 + node.json | 3 + queue/logic/order/activateOrderLogic.go | 30 ++-- user.json | 3 + 12 files changed, 281 insertions(+), 34 deletions(-) diff --git a/common.json b/common.json index edb009d..3acfcaa 100644 --- a/common.json +++ b/common.json @@ -4316,6 +4316,9 @@ "discount": { "type": "number", "format": "double" + }, + "promo": { + "$ref": "#/definitions/SubscribePromo" } }, "title": "SubscribeDiscount", diff --git a/initialize/migrate/database/02154_promo_system.up.sql b/initialize/migrate/database/02154_promo_system.up.sql index 66be4d3..6ab3153 100644 --- a/initialize/migrate/database/02154_promo_system.up.sql +++ b/initialize/migrate/database/02154_promo_system.up.sql @@ -14,19 +14,155 @@ CREATE TABLE IF NOT EXISTS `promo_rule` ( KEY `idx_enabled_priority_deleted` (`enabled`, `deleted_at`, `priority` DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销规则表'; +SET @index_exists = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'promo_rule' + AND INDEX_NAME = 'idx_enabled_priority' +); + +SET @sql = IF( + @index_exists = 1, + 'ALTER TABLE `promo_rule` DROP INDEX `idx_enabled_priority`', + 'SELECT ''Index idx_enabled_priority does not exist on promo_rule 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 = 'promo_rule' + AND INDEX_NAME = 'idx_deleted_at' +); + +SET @sql = IF( + @index_exists = 1, + 'ALTER TABLE `promo_rule` DROP INDEX `idx_deleted_at`', + 'SELECT ''Index idx_deleted_at does not exist on promo_rule 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 = 'promo_rule' + AND INDEX_NAME = 'idx_enabled_priority_deleted' +); + +SET @sql = IF( + @index_exists = 0, + 'ALTER TABLE `promo_rule` ADD KEY `idx_enabled_priority_deleted` (`enabled`, `deleted_at`, `priority` DESC)', + 'SELECT ''Index idx_enabled_priority_deleted already exists on promo_rule table''' +); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + CREATE TABLE IF NOT EXISTS `subscribe_promo` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID', - `quantity` INT NOT NULL DEFAULT 0 COMMENT '购买数量', + `quantity` BIGINT NOT NULL DEFAULT 1 COMMENT '购买数量', `promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID', `promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `uk_subscribe_qty_rule` (`subscribe_id`, `quantity`, `promo_rule_id`), + UNIQUE KEY `uk_subscribe_quantity_rule` (`subscribe_id`, `quantity`, `promo_rule_id`), KEY `idx_promo_rule_id` (`promo_rule_id`) ) 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 @sql = IF( + @column_exists = 1, + 'ALTER TABLE `subscribe_promo` MODIFY COLUMN `quantity` BIGINT NOT NULL DEFAULT 1 COMMENT ''购买数量''', + 'SELECT ''Column quantity does not exist 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_qty_rule' +); + +SET @sql = IF( + @index_exists = 1, + 'ALTER TABLE `subscribe_promo` DROP INDEX `uk_subscribe_qty_rule`', + 'SELECT ''Index uk_subscribe_qty_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` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户 ID', diff --git a/internal/logic/common/promoEligibility.go b/internal/logic/common/promoEligibility.go index f50a4bb..885c7c0 100644 --- a/internal/logic/common/promoEligibility.go +++ b/internal/logic/common/promoEligibility.go @@ -27,13 +27,13 @@ type promoRuleParams struct { 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{} - 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 } - rules, err := svcCtx.PromoModel.QueryEligibleRules(ctx, subscribeID) + rules, err := svcCtx.PromoModel.QueryEligibleRules(ctx, subscribeID, quantity) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo rules failed: %v", err.Error()) } diff --git a/internal/logic/public/order/preCreateOrderLogic.go b/internal/logic/public/order/preCreateOrderLogic.go index ac9ba92..1759d53 100644 --- a/internal/logic/public/order/preCreateOrderLogic.go +++ b/internal/logic/public/order/preCreateOrderLogic.go @@ -47,13 +47,18 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r l.Debugf("[PreCreateOrder] Quantity is less than or equal to 0, setting to 1") req.Quantity = 1 } + entitlement, entErr := commonLogic.ResolveEntitlementUser(l.ctx, l.svcCtx.DB, u.Id) + if entErr != nil { + return nil, entErr + } targetSubscribeID := req.SubscribeId + orderType := uint8(1) isSingleModeRenewal := false decision, routeErr := commonLogic.ResolvePurchaseRoute( l.ctx, l.svcCtx.Config.Subscribe.SingleModel, - u.Id, + entitlement.EffectiveUserID, req.SubscribeId, l.svcCtx.UserModel.FindSingleModeAnchorSubscribe, ) @@ -68,15 +73,44 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r targetSubscribeID = decision.ResolvedSubscribeID isSingleModeRenewal = decision.Route == commonLogic.PurchaseRoutePurchaseToRenewal if isSingleModeRenewal && decision.Anchor != nil { + orderType = 2 l.Infow("[PreCreateOrder] single mode purchase routed to renewal preview", logger.Field("mode", "single"), logger.Field("route", "purchase_to_renewal"), logger.Field("anchor_user_subscribe_id", decision.Anchor.Id), logger.Field("user_id", u.Id), + logger.Field("effective_user_id", entitlement.EffectiveUserID), ) } } + // 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:%')", entitlement.EffectiveUserID). + 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("effective_user_id", entitlement.EffectiveUserID), + 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 sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, targetSubscribeID) if err != nil { @@ -86,7 +120,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r // check subscribe plan quota limit for new purchase flow only if !isSingleModeRenewal && sub.Quota > 0 { - userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id) + userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, entitlement.EffectiveUserID) if err != nil { l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error()) @@ -102,7 +136,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r } } - newUserDiscount, err := resolveNewUserDiscountEligibility(l.ctx, l.svcCtx.DB, u.Id, targetSubscribeID, req.Quantity, sub.Discount) + newUserDiscount, err := resolveNewUserDiscountEligibility(l.ctx, l.svcCtx.DB, entitlement.EffectiveUserID, targetSubscribeID, req.Quantity, sub.Discount) if err != nil { l.Errorw("[PreCreateOrder] Database query error resolving new user eligibility", logger.Field("error", err.Error()), @@ -117,13 +151,13 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r priceResult, err := calculatePurchasePrice( l.ctx, l.svcCtx, - u.Id, + entitlement.EffectiveUserID, targetSubscribeID, sub.UnitPrice, req.Quantity, newUserDiscount.Discounts, newUserDiscount.EligibleForDiscount, - !isSingleModeRenewal, + orderType == 1, ) if err != nil { l.Errorw("[PreCreateOrder] Promo price calculation error", diff --git a/internal/logic/public/order/promoPricing.go b/internal/logic/public/order/promoPricing.go index 119328e..77b6e93 100644 --- a/internal/logic/public/order/promoPricing.go +++ b/internal/logic/public/order/promoPricing.go @@ -36,7 +36,7 @@ func calculatePurchasePrice( } if allowPromo { - promoResult, err := commonLogic.EvaluatePromo(ctx, svcCtx, userID, subscribeID) + promoResult, err := commonLogic.EvaluatePromo(ctx, svcCtx, userID, subscribeID, quantity) if err != nil { return nil, err } diff --git a/internal/logic/public/order/promoPricing_test.go b/internal/logic/public/order/promoPricing_test.go index 2c3f579..4daf226 100644 --- a/internal/logic/public/order/promoPricing_test.go +++ b/internal/logic/public/order/promoPricing_test.go @@ -11,21 +11,30 @@ import ( ) 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 } -func (m fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) error { +func (m *fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) error { return nil } func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) { svcCtx := &svc.ServiceContext{ DB: &gorm.DB{}, - PromoModel: fakePromoModel{rules: []*promo.RuleWithPrice{ + PromoModel: &fakePromoModel{rules: []*promo.RuleWithPrice{ { Rule: promo.Rule{ Id: 9, @@ -73,7 +82,7 @@ func TestCalculatePurchasePricePromoSkipsPercentDiscount(t *testing.T) { func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) { svcCtx := &svc.ServiceContext{ DB: &gorm.DB{}, - PromoModel: fakePromoModel{rules: []*promo.RuleWithPrice{ + PromoModel: &fakePromoModel{rules: []*promo.RuleWithPrice{ { Rule: promo.Rule{ Id: 10, @@ -111,3 +120,52 @@ func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) { 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) + } +} diff --git a/internal/logic/public/subscribe/promo.go b/internal/logic/public/subscribe/promo.go index 5db776c..276e005 100644 --- a/internal/logic/public/subscribe/promo.go +++ b/internal/logic/public/subscribe/promo.go @@ -57,6 +57,12 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs evaluator := promoEligibilityEvaluator{ctx: ctx, db: svcCtx.DB, userInfo: userInfo} now := time.Now() for _, candidate := range candidates { + 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 } @@ -70,9 +76,6 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs if !ok { continue } - if _, exists := result[candidate.SubscribeId]; !exists { - result[candidate.SubscribeId] = make(map[int64]*types.SubscribePromo) - } result[candidate.SubscribeId][candidate.Quantity] = &types.SubscribePromo{ RuleName: candidate.RuleName, RuleType: candidate.RuleType, diff --git a/internal/model/promo/model.go b/internal/model/promo/model.go index ff48935..4ab95bd 100644 --- a/internal/model/promo/model.go +++ b/internal/model/promo/model.go @@ -13,7 +13,7 @@ type RuleWithPrice struct { } 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 } @@ -25,13 +25,13 @@ func NewModel(db *gorm.DB, _ *redis.Client) Model { 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 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.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"). Order("pr.priority DESC"). Order("pr.id ASC"). diff --git a/internal/model/promo/promo.go b/internal/model/promo/promo.go index 1a6f26f..5218d29 100644 --- a/internal/model/promo/promo.go +++ b/internal/model/promo/promo.go @@ -33,6 +33,7 @@ func (Rule) TableName() string { type SubscribePromo struct { Id int64 `gorm:"primaryKey"` 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"` PromoPrice int64 `gorm:"type:bigint;not null;default:0;comment:Promo Price"` CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` diff --git a/node.json b/node.json index 2918ade..e03f593 100644 --- a/node.json +++ b/node.json @@ -3680,6 +3680,9 @@ "discount": { "type": "number", "format": "double" + }, + "promo": { + "$ref": "#/definitions/SubscribePromo" } }, "title": "SubscribeDiscount", diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index 955d502..6bd964e 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -149,7 +149,20 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) return err } - l.recordPromoUsage(ctx, orderInfo) + if err = l.recordPromoUsage(ctx, orderInfo); err != nil { + if releaseErr := l.releaseClaim(ctx, orderInfo.OrderNo); releaseErr != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] releaseClaim also failed, stuck recovery will handle", + logger.Field("order_no", orderInfo.OrderNo), + logger.Field("release_error", releaseErr.Error()), + ) + } + 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) commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_finished", @@ -159,9 +172,9 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) 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 == "" { - return + return nil } promoPrice := int64(0) @@ -169,10 +182,10 @@ func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *or promoPrice = (orderInfo.Price - orderInfo.PromoDiscount) / orderInfo.Quantity } 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 if e := tx.Model(&promo.Usage{}).Where("order_no = ?", orderInfo.OrderNo).Count(&count).Error; e != nil { return e @@ -188,13 +201,6 @@ func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *or PromoPrice: promoPrice, }, 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 diff --git a/user.json b/user.json index 0c076cf..a026dfb 100644 --- a/user.json +++ b/user.json @@ -5900,6 +5900,9 @@ "discount": { "type": "number", "format": "double" + }, + "promo": { + "$ref": "#/definitions/SubscribePromo" } }, "title": "SubscribeDiscount",