diff --git a/internal/handler/app/client_asset.go b/internal/handler/app/client_asset.go index 0f340fe..6953976 100644 --- a/internal/handler/app/client_asset.go +++ b/internal/handler/app/client_asset.go @@ -63,15 +63,16 @@ func NewClientAssetHandler( } type resolvedAssetContext struct { - CustomerID uint - Identifier string - Asset *dto.AssetResolveResponse - Generation int - WalletBalance int64 - SkipPermissionCtx context.Context - IsAgentChannel bool - SellerShopID uint - HasAddonMainPackage bool + CustomerID uint + Identifier string + Asset *dto.AssetResolveResponse + Generation int + WalletBalance int64 + SkipPermissionCtx context.Context + IsAgentChannel bool + SellerShopID uint + HasAddonMainPackage bool + HasFormalMainPackage bool } // resolveAssetFromIdentifier 统一执行资产解析与归属校验 @@ -242,6 +243,16 @@ func (h *ClientAssetHandler) GetAvailablePackages(c *fiber.Ctx) error { } else if err != gorm.ErrRecordNotFound { return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败") } + if hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal( + h.db.WithContext(resolved.SkipPermissionCtx), + resolved.Asset.AssetType, + resolved.Asset.AssetID, + time.Now(), + ); err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败") + } else { + resolved.HasFormalMainPackage = hasBlockingMain + } listCtx := resolved.SkipPermissionCtx if resolved.IsAgentChannel { @@ -530,6 +541,9 @@ func buildClientPackageItem( } isAddon := pkg.PackageType == constants.PackageTypeAddon + if pkg.PackageType == constants.PackageTypeFormal && resolved.HasFormalMainPackage { + return dto.ClientPackageItem{}, false + } if isAddon && !resolved.HasAddonMainPackage { return dto.ClientPackageItem{}, false } diff --git a/internal/service/client_order/service.go b/internal/service/client_order/service.go index d53264d..831f62c 100644 --- a/internal/service/client_order/service.go +++ b/internal/service/client_order/service.go @@ -13,6 +13,7 @@ import ( "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" asset "github.com/break/junhong_cmp_fiber/internal/service/asset" + packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package" "github.com/break/junhong_cmp_fiber/internal/service/purchase_validation" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/alipay" @@ -1247,6 +1248,10 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r return &dto.ClientPayOrderResponse{PaymentMethod: model.PaymentMethodWallet}, nil case model.PaymentMethodWechat: + if err := s.ensureOrderFormalPackagePayable(skipCtx, order); err != nil { + return nil, err + } + activeConfig, appID, err := s.resolveWechatConfig(skipCtx, req.AppType) if err != nil { return nil, err @@ -1303,6 +1308,10 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r }, nil case model.PaymentMethodAlipay: + if err := s.ensureOrderFormalPackagePayable(skipCtx, order); err != nil { + return nil, err + } + // 支付宝支付:不需要 app_type / OpenID activeConfig, err := s.wechatConfigService.GetActiveConfig(skipCtx) if err != nil { @@ -1337,6 +1346,58 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r } } +func (s *Service) ensureOrderFormalPackagePayable(ctx context.Context, order *model.Order) error { + if order == nil { + return errors.New(errors.CodeInvalidParam) + } + + var items []*model.OrderItem + if err := s.db.WithContext(ctx).Where("order_id = ?", order.ID).Find(&items).Error; err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询订单明细失败") + } + + hasFormal := false + for _, item := range items { + if item == nil || item.PackageType == nil { + continue + } + if *item.PackageType == constants.PackageTypeFormal { + hasFormal = true + break + } + } + if !hasFormal { + return nil + } + + carrierType := "" + carrierID := uint(0) + if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil { + carrierType = constants.AssetWalletResourceTypeIotCard + carrierID = *order.IotCardID + } else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil { + carrierType = constants.AssetWalletResourceTypeDevice + carrierID = *order.DeviceID + } + if carrierID == 0 { + return errors.New(errors.CodeInvalidParam, "订单缺少套餐载体") + } + + hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal( + s.db.WithContext(ctx), + carrierType, + carrierID, + time.Now(), + ) + if err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败") + } + if hasBlockingMain { + return errors.New(errors.CodeMainPackageExists) + } + return nil +} + func (s *Service) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) { switch assetType { case "card": diff --git a/internal/service/order/service.go b/internal/service/order/service.go index 1f18687..ca234a9 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -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 diff --git a/internal/service/package/addon_main_package.go b/internal/service/package/addon_main_package.go index fd6a105..f99ce33 100644 --- a/internal/service/package/addon_main_package.go +++ b/internal/service/package/addon_main_package.go @@ -39,3 +39,47 @@ func FindAttachableMainPackageForAddon(tx *gorm.DB, carrierType string, carrierI } return &mainPackage, nil } + +// FindBlockingMainPackageForFormal 查询阻止购买新主套餐的现有主套餐。 +// 待生效、生效中、未过期的已用完主套餐都仍占用主套餐资格;只有过期或失效后才允许再次购买主套餐。 +func FindBlockingMainPackageForFormal(tx *gorm.DB, carrierType string, carrierID uint, now time.Time) (*model.PackageUsage, error) { + var mainPackage model.PackageUsage + query := tx.Model(&model.PackageUsage{}). + Where("master_usage_id IS NULL"). + Where( + "(status IN ? OR (status = ? AND (expires_at IS NULL OR expires_at > ?)))", + []int{ + constants.PackageUsageStatusPending, + constants.PackageUsageStatusActive, + }, + constants.PackageUsageStatusDepleted, + now, + ). + Order("priority ASC, created_at ASC") + + switch carrierType { + case constants.AssetWalletResourceTypeIotCard, "card": + query = query.Where("iot_card_id = ?", carrierID) + case constants.AssetWalletResourceTypeDevice: + query = query.Where("device_id = ?", carrierID) + default: + return nil, gorm.ErrRecordNotFound + } + + if err := query.First(&mainPackage).Error; err != nil { + return nil, err + } + return &mainPackage, nil +} + +// HasBlockingMainPackageForFormal 判断载体是否已有阻止新主套餐购买的主套餐。 +func HasBlockingMainPackageForFormal(tx *gorm.DB, carrierType string, carrierID uint, now time.Time) (bool, error) { + _, err := FindBlockingMainPackageForFormal(tx, carrierType, carrierID, now) + if err == nil { + return true, nil + } + if err == gorm.ErrRecordNotFound { + return false, nil + } + return false, err +} diff --git a/internal/service/purchase_validation/service.go b/internal/service/purchase_validation/service.go index 3003c7d..c955422 100644 --- a/internal/service/purchase_validation/service.go +++ b/internal/service/purchase_validation/service.go @@ -2,8 +2,10 @@ package purchase_validation import ( "context" + "time" "github.com/break/junhong_cmp_fiber/internal/model" + packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package" "github.com/break/junhong_cmp_fiber/internal/service/packageprice" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" @@ -82,6 +84,10 @@ func (s *Service) ValidateCardPurchase(ctx context.Context, cardID uint, package return nil, err } + if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeIotCard, card.ID, packages); err != nil { + return nil, err + } + return &PurchaseValidationResult{ Card: card, Packages: packages, @@ -113,6 +119,10 @@ func (s *Service) ValidateDevicePurchase(ctx context.Context, deviceID uint, pac return nil, err } + if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeDevice, device.ID, packages); err != nil { + return nil, err + } + return &PurchaseValidationResult{ Device: device, Packages: packages, @@ -179,6 +189,61 @@ func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, serie return packages, totalPrice, nil } +// validatePackageUsageRules 校验主套餐和加油包的购买前置条件。 +func (s *Service) validatePackageUsageRules(ctx context.Context, carrierType string, carrierID uint, packages []*model.Package) error { + formalCount := 0 + hasAddon := false + for _, pkg := range packages { + if pkg == nil { + continue + } + switch pkg.PackageType { + case constants.PackageTypeFormal: + formalCount++ + case constants.PackageTypeAddon: + hasAddon = true + } + if formalCount > 0 && hasAddon { + return errors.New(errors.CodeMixedOrderForbidden) + } + } + + if formalCount > 1 { + return errors.New(errors.CodeMainPackageExists, "一次只能购买一个主套餐") + } + + if formalCount == 1 { + hasBlockingMain, err := packagepkg.HasBlockingMainPackageForFormal( + s.db.WithContext(ctx), + carrierType, + carrierID, + time.Now(), + ) + if err != nil { + return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败") + } + if hasBlockingMain { + return errors.New(errors.CodeMainPackageExists) + } + } + + if hasAddon { + if _, err := packagepkg.FindAttachableMainPackageForAddon( + s.db.WithContext(ctx), + carrierType, + carrierID, + time.Now(), + ); err != nil { + if err == gorm.ErrRecordNotFound { + return errors.New(errors.CodeNoMainPackage) + } + return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败") + } + } + + return nil +} + // validateAgentAllocation 校验卖家代理的分配记录上架状态,并返回分配记录 func (s *Service) validateAgentAllocation(ctx context.Context, sellerShopID, packageID uint) (*model.ShopPackageAllocation, error) { allocation, err := s.packageAllocationStore.GetByShopAndPackageForSystem(ctx, sellerShopID, packageID) @@ -252,6 +317,10 @@ func (s *Service) ValidateAdminOfflineCardPurchase(ctx context.Context, cardID u return nil, err } + if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeIotCard, card.ID, packages); err != nil { + return nil, err + } + return &PurchaseValidationResult{ Card: card, Packages: packages, @@ -279,6 +348,10 @@ func (s *Service) ValidateAdminOfflineDevicePurchase(ctx context.Context, device return nil, err } + if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeDevice, device.ID, packages); err != nil { + return nil, err + } + return &PurchaseValidationResult{ Device: device, Packages: packages, diff --git a/internal/task/auto_purchase.go b/internal/task/auto_purchase.go index 0deb1db..e0c82cd 100644 --- a/internal/task/auto_purchase.go +++ b/internal/task/auto_purchase.go @@ -10,6 +10,7 @@ import ( "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" + "gorm.io/gorm/clause" "github.com/break/junhong_cmp_fiber/internal/model" packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package" @@ -496,14 +497,17 @@ func (h *AutoPurchaseHandler) activateMainPackage( now time.Time, ) error { _ = ctx - 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 := h.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 err + } + if hasBlockingMain { + return errors.New("已有主套餐,不能重复购买主套餐") + } var status int var priority int @@ -512,21 +516,11 @@ func (h *AutoPurchaseHandler) activateMainPackage( var nextResetAt *time.Time var pendingRealnameActivation bool - if hasActiveMain { - 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 { - 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: 自动购包属于 C 端充值触发,不需要等实名(C 端已做前置实名检查) // ExpiryBase 仅影响后台囤货路径,此处无需判断 @@ -580,6 +574,19 @@ func (h *AutoPurchaseHandler) activateMainPackage( }).Error } +func (h *AutoPurchaseHandler) lockPackageCarrier(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) error { + switch carrierType { + case constants.AssetWalletResourceTypeIotCard, "card": + var card model.IotCard + return tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&card, carrierID).Error + case constants.AssetWalletResourceTypeDevice: + var device model.Device + return tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&device, carrierID).Error + default: + return errors.New("无效的套餐载体类型") + } +} + func (h *AutoPurchaseHandler) activateAddonPackage( ctx context.Context, tx *gorm.DB, diff --git a/pkg/errors/codes.go b/pkg/errors/codes.go index ef570fa..9c9132d 100644 --- a/pkg/errors/codes.go +++ b/pkg/errors/codes.go @@ -134,6 +134,7 @@ const ( CodeNoMainPackage = 1162 // 必须有主套餐才能购买加油包 CodeRealnameRequired = 1163 // 设备/卡必须先完成实名认证才能购买套餐 CodeMixedOrderForbidden = 1164 // 同订单不能同时购买正式套餐和加油包 + CodeMainPackageExists = 1165 // 已有主套餐,不能重复购买主套餐 // 微信配置相关错误 (1170-1179) CodeWechatConfigNotFound = 1170 // 微信支付配置不存在 @@ -279,6 +280,7 @@ var allErrorCodes = []int{ CodeNoMainPackage, CodeRealnameRequired, CodeMixedOrderForbidden, + CodeMainPackageExists, CodeWechatConfigNotFound, CodeWechatConfigActive, CodeWechatConfigHasPendingOrders, @@ -413,6 +415,7 @@ var errorMessages = map[int]string{ CodeNoMainPackage: "必须有主套餐才能购买加油包", CodeRealnameRequired: "设备/卡必须先完成实名认证才能购买套餐", CodeMixedOrderForbidden: "同订单不能同时购买正式套餐和加油包", + CodeMainPackageExists: "已有主套餐,不能重复购买主套餐", CodeWechatConfigNotFound: "微信支付配置不存在", CodeWechatConfigActive: "不能删除当前生效的支付配置,请先停用", CodeWechatConfigHasPendingOrders: "该配置存在未完成的支付订单,暂时无法删除",