不允许购买第二个主套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m50s

This commit is contained in:
Break
2026-06-10 15:24:26 +08:00
parent 32c9859b38
commit 5c779cb6e0
7 changed files with 314 additions and 108 deletions

View File

@@ -25,6 +25,7 @@ import (
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// WechatConfigServiceInterface 支付配置服务接口
@@ -602,7 +603,7 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
purchaseRole := ""
var sellerShopID *uint = resourceShopID
var sellerCostPrice int64
var expiresAt *time.Time // 待支付订单设置过期时间,立即支付的订单为 nil
var expiresAt *time.Time // 待支付订单设置过期时间,立即支付的订单为 nil
var itemCostPriceMap map[uint]int64 // 各套餐成本单价映射,用于 OrderItem.CostPrice 字段
// 场景判断offline平台代购、wallet代理钱包支付、其他待支付
@@ -2029,6 +2030,24 @@ func (s *Service) activatePackage(ctx context.Context, tx *gorm.DB, order *model
return nil
}
func (s *Service) lockPackageCarrier(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) error {
switch carrierType {
case constants.AssetWalletResourceTypeIotCard, "card":
var card model.IotCard
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&card, carrierID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定卡失败")
}
case constants.AssetWalletResourceTypeDevice:
var device model.Device
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&device, carrierID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "锁定设备失败")
}
default:
return errors.New(errors.CodeInvalidParam, "无效的套餐载体类型")
}
return nil
}
// validatePackageTypeMix 任务 8.1: 检查混买限制
func (s *Service) validatePackageTypeMix(tx *gorm.DB, items []*model.OrderItem) error {
hasFormal := false
@@ -2204,15 +2223,17 @@ func (s *Service) markOrderCreated(ctx context.Context, idempotencyKey string, o
// activateMainPackage 任务 8.2-8.4: 主套餐激活逻辑
func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *model.Order, pkg *model.Package, carrierType string, carrierID uint, now time.Time) error {
// 检查是否有生效中主套餐
var activeMainPackage model.PackageUsage
err := tx.Where("status = ?", constants.PackageUsageStatusActive).
Where("master_usage_id IS NULL").
Where(carrierType+"_id = ?", carrierID).
Order("priority ASC").
First(&activeMainPackage).Error
if err := s.lockPackageCarrier(ctx, tx, carrierType, carrierID); err != nil {
return err
}
hasActiveMain := err == nil
hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal(tx.WithContext(ctx), carrierType, carrierID, now)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
}
if hasBlockingMain {
return errors.New(errors.CodeMainPackageExists)
}
var status int
var priority int
@@ -2221,80 +2242,63 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
var nextResetAt *time.Time
var pendingRealnameActivation bool
if hasActiveMain {
// 任务 8.3: 有生效中主套餐,新套餐排队
status = constants.PackageUsageStatusPending
// 查询当前最大优先级
var maxPriority int
tx.Model(&model.PackageUsage{}).
Where(carrierType+"_id = ?", carrierID).
Select("COALESCE(MAX(priority), 0)").
Scan(&maxPriority)
priority = maxPriority + 1
// 排队套餐暂不设置激活时间和过期时间(由激活任务处理)
} else {
// 任务 8.4: 无生效中主套餐,立即激活
status = constants.PackageUsageStatusActive
priority = 1
activatedAt = now
// 使用工具函数计算过期时间
expiresAt = packagepkg.CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
// 计算下次重置时间(基于套餐周期类型)
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
}
status = constants.PackageUsageStatusActive
priority = 1
activatedAt = now
// 使用工具函数计算过期时间
expiresAt = packagepkg.CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
// 计算下次重置时间(基于套餐周期类型)
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
// REALNAME-02: 后台囤货场景三维决策(卡类型 + 购买路径 + expiry_base + 实名状态)
// 注意:仅在 else 分支(立即激活)时才需要判断是否切换为等实名
if !hasActiveMain {
// 查询卡类型(行业卡永远直接激活,不等实名)
// 同时查询载体当前实名状态:已实名则跳过 pending直接激活
var cardCategory string
var currentlyRealnamed bool
if carrierType == "iot_card" {
var card model.IotCard
if err := tx.Select("card_category", "real_name_status").First(&card, carrierID).Error; err == nil {
cardCategory = card.CardCategory
currentlyRealnamed = card.RealNameStatus == constants.RealNameStatusVerified
}
// err != nil 时 currentlyRealnamed 保守默认 false不阻断购买流程
} else if carrierType == "device" {
// 子查询统计该设备绑定的已实名卡数量GORM 自动注入 deleted_at IS NULL
var count int64
subQuery := tx.Model(&model.DeviceSimBinding{}).
Select("iot_card_id").
Where("device_id = ? AND bind_status = 1", carrierID)
if err := tx.Model(&model.IotCard{}).
Where("id IN (?) AND real_name_status = 1", subQuery).
Count(&count).Error; err == nil {
currentlyRealnamed = count > 0
}
// err != nil 时保守默认 false不阻断购买流程
// 查询卡类型(行业卡永远直接激活,不等实名
// 同时查询载体当前实名状态:已实名则跳过 pending直接激活
var cardCategory string
var currentlyRealnamed bool
if carrierType == "iot_card" {
var card model.IotCard
if err := tx.Select("card_category", "real_name_status").First(&card, carrierID).Error; err == nil {
cardCategory = card.CardCategory
currentlyRealnamed = card.RealNameStatus == constants.RealNameStatusVerified
}
// 判断是否 C 端购买C 端购买前已做实名前置检查,直接激活)
isCEndPurchase := order.BuyerType == model.BuyerTypePersonal
if cardCategory != "industry" && !isCEndPurchase {
// 后台囤货路径:按 expiry_base 决定是否等实名
if pkg.ExpiryBase != "from_purchase" {
if currentlyRealnamed {
s.logger.Info("购买时载体已实名,直接激活套餐",
zap.String("carrier_type", carrierType),
zap.Uint("carrier_id", carrierID))
} else {
// from_activation默认未实名等实名后激活
status = constants.PackageUsageStatusPending
pendingRealnameActivation = true
activatedAt = time.Time{}
expiresAt = time.Time{}
nextResetAt = nil
}
}
// from_purchase立即激活status 已为 Active保持不变
// err != nil 时 currentlyRealnamed 保守默认 false不阻断购买流程
} else if carrierType == "device" {
// 子查询统计该设备绑定的已实名卡数量GORM 自动注入 deleted_at IS NULL
var count int64
subQuery := tx.Model(&model.DeviceSimBinding{}).
Select("iot_card_id").
Where("device_id = ? AND bind_status = 1", carrierID)
if err := tx.Model(&model.IotCard{}).
Where("id IN (?) AND real_name_status = 1", subQuery).
Count(&count).Error; err == nil {
currentlyRealnamed = count > 0
}
// 行业卡或 C 端购买直接激活status 已为 Active保持不变
// err != nil 时保守默认 false不阻断购买流程
}
// 判断是否 C 端购买C 端购买前已做实名前置检查,直接激活)
isCEndPurchase := order.BuyerType == model.BuyerTypePersonal
if cardCategory != "industry" && !isCEndPurchase {
// 后台囤货路径:按 expiry_base 决定是否等实名
if pkg.ExpiryBase != "from_purchase" {
if currentlyRealnamed {
s.logger.Info("购买时载体已实名,直接激活套餐",
zap.String("carrier_type", carrierType),
zap.Uint("carrier_id", carrierID))
} else {
// from_activation默认未实名等实名后激活
status = constants.PackageUsageStatusPending
pendingRealnameActivation = true
activatedAt = time.Time{}
expiresAt = time.Time{}
nextResetAt = nil
}
}
// from_purchase立即激活status 已为 Active保持不变
}
// 行业卡或 C 端购买直接激活status 已为 Active保持不变
// 创建套餐使用记录
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
retailAmount := order.TotalAmount