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" "github.com/break/junhong_cmp_fiber/pkg/errors" "gorm.io/gorm" ) type allowGiftPackagesKey struct{} // WithAllowGiftPackages 标记当前校验上下文允许后台赠送订单进入验证流程。 func WithAllowGiftPackages(ctx context.Context) context.Context { return context.WithValue(ctx, allowGiftPackagesKey{}, true) } func canAllowGiftPackages(ctx context.Context) bool { allowed, _ := ctx.Value(allowGiftPackagesKey{}).(bool) return allowed } type Service struct { db *gorm.DB iotCardStore *postgres.IotCardStore deviceStore *postgres.DeviceStore packageStore *postgres.PackageStore packageAllocationStore *postgres.ShopPackageAllocationStore } func New( db *gorm.DB, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, packageStore *postgres.PackageStore, packageAllocationStore *postgres.ShopPackageAllocationStore, ) *Service { return &Service{ db: db, iotCardStore: iotCardStore, deviceStore: deviceStore, packageStore: packageStore, packageAllocationStore: packageAllocationStore, } } type PurchaseValidationResult struct { Card *model.IotCard Device *model.Device Packages []*model.Package TotalPrice int64 } func (s *Service) ValidateCardPurchase(ctx context.Context, cardID uint, packageIDs []uint) (*PurchaseValidationResult, error) { card, err := s.iotCardStore.GetByID(ctx, cardID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeIotCardNotFound, "IoT卡不存在") } return nil, err } if !card.IsStandalone { return nil, errors.New(errors.CodeInvalidParam, "该卡已绑定设备,请前往设备页面购买套餐") } if card.SeriesID == nil || *card.SeriesID == 0 { return nil, errors.New(errors.CodeInvalidParam, "该卡未关联套餐系列,无法购买套餐") } // 确定卖家店铺ID:卡所属店铺即为卖家(代理渠道),平台自营时为0 var sellerShopID uint if card.ShopID != nil { sellerShopID = *card.ShopID } packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *card.SeriesID, sellerShopID) if err != nil { return nil, err } if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeIotCard, card.ID, packages); err != nil { return nil, err } return &PurchaseValidationResult{ Card: card, Packages: packages, TotalPrice: totalPrice, }, nil } func (s *Service) ValidateDevicePurchase(ctx context.Context, deviceID uint, packageIDs []uint) (*PurchaseValidationResult, error) { device, err := s.deviceStore.GetByID(ctx, deviceID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, err } if device.SeriesID == nil || *device.SeriesID == 0 { return nil, errors.New(errors.CodeInvalidParam, "该设备未关联套餐系列,无法购买套餐") } // 确定卖家店铺ID:设备所属店铺即为卖家(代理渠道),平台自营时为0 var sellerShopID uint if device.ShopID != nil { sellerShopID = *device.ShopID } packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *device.SeriesID, sellerShopID) if err != nil { return nil, err } if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeDevice, device.ID, packages); err != nil { return nil, err } return &PurchaseValidationResult{ Device: device, Packages: packages, TotalPrice: totalPrice, }, nil } // validatePackages 验证套餐列表是否可购买 // sellerShopID > 0 表示代理渠道,校验该代理的 allocation.shelf_status; // sellerShopID == 0 表示平台自营渠道,校验 package.shelf_status func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, seriesID uint, sellerShopID uint) ([]*model.Package, int64, error) { if len(packageIDs) == 0 { return nil, 0, errors.New(errors.CodeInvalidParam, "请选择至少一个套餐") } var packages []*model.Package var totalPrice int64 for _, pkgID := range packageIDs { pkg, err := s.packageStore.GetByID(ctx, pkgID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, 0, errors.New(errors.CodeInvalidParam, "套餐不存在") } return nil, 0, err } if pkg.SeriesID != seriesID { return nil, 0, errors.New(errors.CodeInvalidParam, "套餐不在可购买范围内") } if pkg.IsGift { if sellerShopID > 0 || !canAllowGiftPackages(ctx) { return nil, 0, errors.New(errors.CodeInvalidParam, "赠送套餐仅支持后台发放") } } // Package.status 为全局开关,任何渠道都必须检查 if pkg.Status != constants.StatusEnabled { return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已禁用") } if sellerShopID > 0 { // 代理渠道:检查上架状态并获取分配记录,使用零售价 allocation, allocErr := s.validateAgentAllocation(ctx, sellerShopID, pkgID) if allocErr != nil { return nil, 0, allocErr } effectiveRetailPrice := packageprice.AllocationEffectiveRetailPrice(allocation) // 生效零售价低于成本价时视为不可购买,防止亏损售卖 if effectiveRetailPrice < allocation.CostPrice { return nil, 0, errors.New(errors.CodeInvalidParam, "套餐价格配置异常,暂不可购买") } totalPrice += effectiveRetailPrice } else { if pkg.ShelfStatus != constants.ShelfStatusOn { return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已下架") } totalPrice += packageprice.PackageEffectiveRetailPrice(pkg) } packages = append(packages, pkg) } 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 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) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeInvalidParam, "套餐已下架") } return nil, errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败") } if allocation.ShelfStatus != constants.ShelfStatusOn { return nil, errors.New(errors.CodeInvalidParam, "套餐已下架") } return allocation, nil } // GetPurchasePrice 获取购买价格 // 代理渠道(sellerShopID > 0)返回 allocation.RetailPrice,平台渠道返回 Package.SuggestedRetailPrice func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, sellerShopID uint) (int64, error) { if sellerShopID > 0 { allocation, err := s.packageAllocationStore.GetByShopAndPackageForSystem(ctx, sellerShopID, pkg.ID) if err != nil { if err == gorm.ErrRecordNotFound { return 0, errors.New(errors.CodeInvalidParam, "套餐已下架") } return 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败") } return packageprice.AllocationEffectiveRetailPrice(allocation), nil } return packageprice.PackageEffectiveRetailPrice(pkg), nil } // GetCostPrice 获取代理渠道的套餐成本价 // sellerShopID == 0 表示平台自营,返回 0(平台直销无成本价概念) func (s *Service) GetCostPrice(ctx context.Context, pkg *model.Package, sellerShopID uint) (int64, error) { if sellerShopID == 0 { return 0, nil } allocation, err := s.packageAllocationStore.GetByShopAndPackageForSystem(ctx, sellerShopID, pkg.ID) if err != nil { if err == gorm.ErrRecordNotFound { return 0, errors.New(errors.CodeInvalidParam, "店铺没有该套餐的分配配置") } return 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败") } return allocation.CostPrice, nil } // ValidateAdminOfflineCardPurchase 后台 offline 订单专用卡验证 // 绕过代理 Allocation 上架检查,仅验证套餐全局状态 func (s *Service) ValidateAdminOfflineCardPurchase(ctx context.Context, cardID uint, packageIDs []uint) (*PurchaseValidationResult, error) { card, err := s.iotCardStore.GetByID(ctx, cardID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeIotCardNotFound, "IoT卡不存在") } return nil, err } if !card.IsStandalone { return nil, errors.New(errors.CodeInvalidParam, "该卡已绑定设备,请前往设备页面购买套餐") } if card.SeriesID == nil || *card.SeriesID == 0 { return nil, errors.New(errors.CodeInvalidParam, "该卡未关联套餐系列,无法购买套餐") } packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *card.SeriesID, 0) if err != nil { return nil, err } if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeIotCard, card.ID, packages); err != nil { return nil, err } return &PurchaseValidationResult{ Card: card, Packages: packages, TotalPrice: totalPrice, }, nil } // ValidateAdminOfflineDevicePurchase 后台 offline 订单专用设备验证 // 绕过代理 Allocation 上架检查,仅验证套餐全局状态 func (s *Service) ValidateAdminOfflineDevicePurchase(ctx context.Context, deviceID uint, packageIDs []uint) (*PurchaseValidationResult, error) { device, err := s.deviceStore.GetByID(ctx, deviceID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "设备不存在") } return nil, err } if device.SeriesID == nil || *device.SeriesID == 0 { return nil, errors.New(errors.CodeInvalidParam, "该设备未关联套餐系列,无法购买套餐") } packages, totalPrice, err := s.validatePackages(ctx, packageIDs, *device.SeriesID, 0) if err != nil { return nil, err } if err := s.validatePackageUsageRules(ctx, constants.AssetWalletResourceTypeDevice, device.ID, packages); err != nil { return nil, err } return &PurchaseValidationResult{ Device: device, Packages: packages, TotalPrice: totalPrice, }, nil }