Files
junhong_cmp_fiber/internal/service/purchase_validation/service.go
Break c437593a8c
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m48s
修复错误购买报错的问题,修复接口返回虚流量启动错误的问题
2026-06-11 14:51:26 +08:00

346 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}