赠送套餐逻辑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s

This commit is contained in:
2026-04-28 17:40:06 +08:00
parent 516a7f5bdf
commit 37b4483183
34 changed files with 1172 additions and 60 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
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/service/purchase_validation"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
@@ -120,11 +121,6 @@ func (s *Service) SetResumeCallback(callback packagepkg.ResumeCallback) {
// 与 CreateH5Order 的核心区别后台订单不创建待支付状态wallet 立即扣款offline 立即激活
// POST /api/admin/orders
func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
// 线下支付必须上传支付凭证
if req.PaymentMethod == model.PaymentMethodOffline && strings.TrimSpace(req.PaymentVoucherKey) == "" {
return nil, errors.New(errors.CodeInvalidParam, "线下支付必须上传支付凭证")
}
resolvedCard, resolvedDevice, resolveErr := s.resolveAssetByIdentifier(ctx, req.Identifier)
if resolveErr != nil {
return nil, resolveErr
@@ -146,18 +142,23 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
var validationResult *purchase_validation.PurchaseValidationResult
var err error
operatorUserType := middleware.GetUserTypeFromContext(ctx)
validationCtx := ctx
if req.PaymentMethod == model.PaymentMethodOffline && (operatorUserType == constants.UserTypeSuperAdmin || operatorUserType == constants.UserTypePlatform) {
validationCtx = purchase_validation.WithAllowGiftPackages(ctx)
}
if req.PaymentMethod == model.PaymentMethodOffline {
if resolvedCard != nil {
validationResult, err = s.purchaseValidationService.ValidateAdminOfflineCardPurchase(ctx, resolvedCard.ID, req.PackageIDs)
validationResult, err = s.purchaseValidationService.ValidateAdminOfflineCardPurchase(validationCtx, resolvedCard.ID, req.PackageIDs)
} else {
validationResult, err = s.purchaseValidationService.ValidateAdminOfflineDevicePurchase(ctx, resolvedDevice.ID, req.PackageIDs)
validationResult, err = s.purchaseValidationService.ValidateAdminOfflineDevicePurchase(validationCtx, resolvedDevice.ID, req.PackageIDs)
}
} else {
if resolvedCard != nil {
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, resolvedCard.ID, req.PackageIDs)
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(validationCtx, resolvedCard.ID, req.PackageIDs)
} else {
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, resolvedDevice.ID, req.PackageIDs)
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(validationCtx, resolvedDevice.ID, req.PackageIDs)
}
}
@@ -168,6 +169,21 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
if err := validatePackageTypeMixFromPackages(validationResult.Packages); err != nil {
return nil, err
}
containsGift := packageprice.ContainsGiftPackage(validationResult.Packages)
if containsGift {
if operatorUserType != constants.UserTypeSuperAdmin && operatorUserType != constants.UserTypePlatform {
return nil, errors.New(errors.CodeForbidden, "仅平台侧可发放赠送套餐")
}
if req.PaymentMethod != model.PaymentMethodOffline {
return nil, errors.New(errors.CodeInvalidParam, "赠送套餐仅支持后台线下发放")
}
if err := packageprice.ValidateGiftAdminOrder(validationResult.Packages); err != nil {
return nil, err
}
}
if req.PaymentMethod == model.PaymentMethodOffline && strings.TrimSpace(req.PaymentVoucherKey) == "" {
return nil, errors.New(errors.CodeInvalidParam, "线下支付必须上传支付凭证")
}
carrierType, carrierID := resolveAdminCarrierInfoFromVars(orderType, iotCardID, deviceID)
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, orderType, carrierType, carrierID, req.PackageIDs)
@@ -229,6 +245,22 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
// 根据支付方式分别处理
if req.PaymentMethod == model.PaymentMethodOffline {
// ==== 场景 1offline线下支付====
if containsGift {
orderBuyerType = model.BuyerTypePersonal
orderBuyerID = 0
totalAmount = 0
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = false
sellerShopID = nil
sellerCostPrice = 0
operatorID = nil
operatorType = constants.OwnerTypePlatform
actualPaidAmountVal := int64(0)
actualPaidAmount = &actualPaidAmountVal
purchaseRole = model.PurchaseRoleSelfPurchase
} else {
isPlatformOwned := resourceShopID == nil || *resourceShopID == 0
if isPlatformOwned {
@@ -267,6 +299,7 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
purchaseRole = model.PurchaseRolePurchasedByPlatform
actualPaidAmount = nil
}
}
} else if req.PaymentMethod == model.PaymentMethodWallet {
// ==== 场景 2代理钱包支付wallet====
@@ -438,7 +471,9 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
if !containsGift {
s.enqueueCommissionCalculation(ctx, order.ID)
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
@@ -786,7 +821,7 @@ func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package, un
continue
}
unitPrice := pkg.SuggestedRetailPrice
unitPrice := packageprice.PackageEffectiveRetailPrice(pkg)
if price, ok := unitPriceMap[pkg.ID]; ok {
unitPrice = price
}
@@ -796,12 +831,14 @@ func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package, un
Creator: operatorID,
Updater: operatorID,
},
PackageID: pkg.ID,
PackageName: pkg.PackageName,
PackageType: &pkg.PackageType,
Quantity: 1,
UnitPrice: unitPrice,
Amount: unitPrice,
PackageID: pkg.ID,
PackageName: pkg.PackageName,
PackageType: &pkg.PackageType,
Quantity: 1,
UnitPrice: unitPrice,
Amount: unitPrice,
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
}
items = append(items, item)
}
@@ -2055,6 +2092,8 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
DataResetCycle: pkg.DataResetCycle,
PendingRealnameActivation: pendingRealnameActivation,
PaidAmount: order.ActualPaidAmount,
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
}
if carrierType == "iot_card" {
@@ -2146,6 +2185,8 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
ExpiresAt: &expiresAt,
DataResetCycle: pkg.DataResetCycle,
PaidAmount: order.ActualPaidAmount,
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
}
if carrierType == "iot_card" {

View File

@@ -9,6 +9,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
@@ -42,6 +43,9 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
if err := validateGiftPackagePermission(ctx, req.IsGift); err != nil {
return nil, err
}
existing, _ := s.packageStore.GetByCode(ctx, req.PackageCode)
if existing != nil {
@@ -91,12 +95,20 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
seriesName = &series.SeriesName
}
priceConfigStatus, storedRetailPrice, err := packageprice.ResolvePackagePriceState(req.IsGift, req.SuggestedRetailPrice)
if err != nil {
return nil, err
}
pkg := &model.Package{
PackageCode: req.PackageCode,
PackageName: req.PackageName,
PackageType: req.PackageType,
IsGift: req.IsGift,
DurationMonths: req.DurationMonths,
CostPrice: req.CostPrice,
PriceConfigStatus: priceConfigStatus,
SuggestedRetailPrice: storedRetailPrice,
EnableVirtualData: req.EnableVirtualData,
CalendarType: calendarType,
Status: constants.StatusEnabled,
@@ -111,9 +123,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
if req.VirtualDataMB != nil {
pkg.VirtualDataMB = *req.VirtualDataMB
}
if req.SuggestedRetailPrice != nil {
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
}
if req.DurationDays != nil {
pkg.DurationDays = *req.DurationDays
}
@@ -220,6 +229,13 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
if req.PackageType != nil {
pkg.PackageType = *req.PackageType
}
nextIsGift := pkg.IsGift
if req.IsGift != nil {
nextIsGift = *req.IsGift
}
if err := validateGiftPackagePermission(ctx, nextIsGift); err != nil {
return nil, err
}
if req.DurationMonths != nil {
pkg.DurationMonths = *req.DurationMonths
}
@@ -235,9 +251,6 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
if req.CostPrice != nil {
pkg.CostPrice = *req.CostPrice
}
if req.SuggestedRetailPrice != nil {
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
}
if req.CalendarType != nil {
pkg.CalendarType = *req.CalendarType
}
@@ -251,6 +264,23 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
pkg.ExpiryBase = *req.ExpiryBase
}
var rawSuggestedRetailPrice *int64
switch {
case req.ClearSuggestedRetailPrice:
rawSuggestedRetailPrice = nil
case req.SuggestedRetailPrice != nil:
rawSuggestedRetailPrice = req.SuggestedRetailPrice
default:
rawSuggestedRetailPrice = packageprice.PackageRawSuggestedRetailPrice(pkg)
}
priceConfigStatus, storedRetailPrice, err := packageprice.ResolvePackagePriceState(nextIsGift, rawSuggestedRetailPrice)
if err != nil {
return nil, err
}
pkg.IsGift = nextIsGift
pkg.PriceConfigStatus = priceConfigStatus
pkg.SuggestedRetailPrice = storedRetailPrice
// 校验套餐周期类型和时长配置
if pkg.CalendarType == constants.PackageCalendarTypeNaturalMonth {
if pkg.DurationMonths <= 0 {
@@ -503,11 +533,25 @@ func (s *Service) UpdateRetailPrice(ctx context.Context, packageID uint, retailP
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
}
pkg, err := s.packageStore.GetByID(ctx, packageID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
if pkg.IsGift {
return errors.New(errors.CodeForbidden, "赠送套餐不允许代理修改零售价")
}
priceConfigStatus, storedRetailPrice, err := packageprice.ResolveAllocationPriceState(&retailPrice)
if err != nil {
return err
}
if retailPrice < allocation.CostPrice {
return errors.New(errors.CodeInvalidParam, "零售价不能低于成本价")
}
if err := s.packageAllocationStore.UpdateRetailPrice(ctx, allocation.ID, retailPrice, currentUserID); err != nil {
if err := s.packageAllocationStore.UpdateRetailPrice(ctx, allocation.ID, storedRetailPrice, priceConfigStatus, currentUserID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新零售价失败")
}
@@ -568,13 +612,16 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
PackageName: pkg.PackageName,
SeriesID: seriesID,
PackageType: pkg.PackageType,
IsGift: pkg.IsGift,
DurationMonths: pkg.DurationMonths,
RealDataMB: pkg.RealDataMB,
VirtualDataMB: pkg.VirtualDataMB,
EnableVirtualData: pkg.EnableVirtualData,
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
CostPrice: pkg.CostPrice,
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
PriceConfigStatus: pkg.PriceConfigStatus,
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
CalendarType: pkg.CalendarType,
DurationDays: durationDays,
DataResetCycle: pkg.DataResetCycle,
@@ -591,11 +638,18 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, pkg.ID)
if err == nil && allocation != nil {
resp.CostPrice = allocation.CostPrice
resp.RetailPrice = &allocation.RetailPrice
profitMargin := allocation.RetailPrice - allocation.CostPrice
resp.RetailPrice = packageprice.AllocationRawRetailPrice(allocation)
retailPriceConfigStatus := allocation.RetailPriceConfigStatus
resp.RetailPriceConfigStatus = &retailPriceConfigStatus
effectiveRetailPrice := packageprice.AllocationEffectiveRetailPrice(allocation)
resp.EffectiveRetailPrice = &effectiveRetailPrice
profitMargin := effectiveRetailPrice - allocation.CostPrice
resp.ProfitMargin = &profitMargin
resp.ShelfStatus = allocation.ShelfStatus
}
} else {
effectiveRetailPrice := packageprice.PackageEffectiveRetailPrice(pkg)
resp.EffectiveRetailPrice = &effectiveRetailPrice
}
resp.StatusName = constants.GetStatusName(resp.Status)
@@ -635,13 +689,16 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
PackageName: pkg.PackageName,
SeriesID: seriesID,
PackageType: pkg.PackageType,
IsGift: pkg.IsGift,
DurationMonths: pkg.DurationMonths,
RealDataMB: pkg.RealDataMB,
VirtualDataMB: pkg.VirtualDataMB,
EnableVirtualData: pkg.EnableVirtualData,
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
CostPrice: pkg.CostPrice,
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
PriceConfigStatus: pkg.PriceConfigStatus,
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
CalendarType: pkg.CalendarType,
DurationDays: durationDays,
DataResetCycle: pkg.DataResetCycle,
@@ -655,11 +712,18 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
if allocationMap != nil {
if allocation, ok := allocationMap[pkg.ID]; ok {
resp.CostPrice = allocation.CostPrice
resp.RetailPrice = &allocation.RetailPrice
profitMargin := allocation.RetailPrice - allocation.CostPrice
resp.RetailPrice = packageprice.AllocationRawRetailPrice(allocation)
retailPriceConfigStatus := allocation.RetailPriceConfigStatus
resp.RetailPriceConfigStatus = &retailPriceConfigStatus
effectiveRetailPrice := packageprice.AllocationEffectiveRetailPrice(allocation)
resp.EffectiveRetailPrice = &effectiveRetailPrice
profitMargin := effectiveRetailPrice - allocation.CostPrice
resp.ProfitMargin = &profitMargin
resp.ShelfStatus = allocation.ShelfStatus
}
} else {
effectiveRetailPrice := packageprice.PackageEffectiveRetailPrice(pkg)
resp.EffectiveRetailPrice = &effectiveRetailPrice
}
// 填充返佣信息(仅代理用户可见)
@@ -728,6 +792,28 @@ func (s *Service) buildTierInfo(tiers []model.OneTimeCommissionTier, currentAmou
return tierInfo
}
func validateGiftPackagePermission(ctx context.Context, isGift bool) error {
if !isGift {
return nil
}
userType := middleware.GetUserTypeFromContext(ctx)
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
return errors.New(errors.CodeForbidden, "仅平台侧可创建或维护赠送套餐")
}
return nil
}
func packagePriceConfigStatusName(status int) string {
switch status {
case constants.PackagePriceConfigStatusGiftZero:
return "赠送0价"
case constants.PackagePriceConfigStatusConfigured:
return "已配置非0"
default:
return "未配置"
}
}
// formatAmount 格式化金额为可读字符串(分转元)
func formatAmount(amountFen int64) string {
yuan := float64(amountFen) / 100

View File

@@ -0,0 +1,114 @@
// Package packageprice 提供套餐价格与赠送语义的统一判定逻辑
// 负责原始价格、价格状态、生效价和赠送限制的集中计算
package packageprice
import (
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// ResolvePackagePriceState 解析套餐价格状态和落库存储值。
func ResolvePackagePriceState(isGift bool, rawPrice *int64) (int, int64, error) {
if isGift {
if rawPrice == nil || *rawPrice != 0 {
return 0, 0, errors.New(errors.CodeInvalidParam, "赠送套餐必须显式配置 0 元售价")
}
return constants.PackagePriceConfigStatusGiftZero, 0, nil
}
if rawPrice == nil {
return constants.PackagePriceConfigStatusUnconfigured, 0, nil
}
if *rawPrice == 0 {
return 0, 0, errors.New(errors.CodeInvalidParam, "普通可售套餐不允许显式配置 0 元售价")
}
return constants.PackagePriceConfigStatusConfigured, *rawPrice, nil
}
// ResolveAllocationPriceState 解析代理分配零售价状态和落库存储值。
func ResolveAllocationPriceState(rawPrice *int64) (int, int64, error) {
if rawPrice == nil {
return constants.PackagePriceConfigStatusUnconfigured, 0, nil
}
if *rawPrice == 0 {
return 0, 0, errors.New(errors.CodeInvalidParam, "普通可售套餐不允许显式配置 0 元零售价")
}
return constants.PackagePriceConfigStatusConfigured, *rawPrice, nil
}
// PackageEffectiveRetailPrice 返回套餐生效售价。
func PackageEffectiveRetailPrice(pkg *model.Package) int64 {
if pkg == nil {
return 0
}
switch pkg.PriceConfigStatus {
case constants.PackagePriceConfigStatusGiftZero:
return 0
case constants.PackagePriceConfigStatusConfigured:
return pkg.SuggestedRetailPrice
default:
return pkg.CostPrice
}
}
// PackageRawSuggestedRetailPrice 返回套餐原始建议售价。
func PackageRawSuggestedRetailPrice(pkg *model.Package) *int64 {
if pkg == nil || pkg.PriceConfigStatus == constants.PackagePriceConfigStatusUnconfigured {
return nil
}
price := pkg.SuggestedRetailPrice
return &price
}
// AllocationEffectiveRetailPrice 返回代理分配记录生效零售价。
func AllocationEffectiveRetailPrice(allocation *model.ShopPackageAllocation) int64 {
if allocation == nil {
return 0
}
if allocation.RetailPriceConfigStatus == constants.PackagePriceConfigStatusUnconfigured {
return allocation.CostPrice
}
return allocation.RetailPrice
}
// AllocationRawRetailPrice 返回代理分配记录原始零售价。
func AllocationRawRetailPrice(allocation *model.ShopPackageAllocation) *int64 {
if allocation == nil || allocation.RetailPriceConfigStatus == constants.PackagePriceConfigStatusUnconfigured {
return nil
}
price := allocation.RetailPrice
return &price
}
// ContainsGiftPackage 判断套餐列表中是否包含赠送套餐。
func ContainsGiftPackage(packages []*model.Package) bool {
for _, pkg := range packages {
if pkg != nil && pkg.IsGift {
return true
}
}
return false
}
// ValidateGiftAdminOrder 校验后台赠送订单约束。
func ValidateGiftAdminOrder(packages []*model.Package) error {
if !ContainsGiftPackage(packages) {
return nil
}
if len(packages) != 1 {
return errors.New(errors.CodeInvalidParam, "赠送套餐必须单独创建订单")
}
for _, pkg := range packages {
if pkg == nil {
continue
}
if !pkg.IsGift {
return errors.New(errors.CodeInvalidParam, "赠送订单不能混入普通可售套餐")
}
}
return nil
}

View File

@@ -4,12 +4,25 @@ import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"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
@@ -130,6 +143,11 @@ func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, serie
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 {
@@ -142,16 +160,17 @@ func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, serie
if allocErr != nil {
return nil, 0, allocErr
}
// 零售价低于成本价时视为不可购买,防止亏损售卖
if allocation.RetailPrice < allocation.CostPrice {
effectiveRetailPrice := packageprice.AllocationEffectiveRetailPrice(allocation)
// 生效零售价低于成本价时视为不可购买,防止亏损售卖
if effectiveRetailPrice < allocation.CostPrice {
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐价格配置异常,暂不可购买")
}
totalPrice += allocation.RetailPrice
totalPrice += effectiveRetailPrice
} else {
if pkg.ShelfStatus != constants.ShelfStatusOn {
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已下架")
}
totalPrice += pkg.SuggestedRetailPrice
totalPrice += packageprice.PackageEffectiveRetailPrice(pkg)
}
packages = append(packages, pkg)
@@ -188,9 +207,9 @@ func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, sell
}
return 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败")
}
return allocation.RetailPrice, nil
return packageprice.AllocationEffectiveRetailPrice(allocation), nil
}
return pkg.SuggestedRetailPrice, nil
return packageprice.PackageEffectiveRetailPrice(pkg), nil
}
// GetCostPrice 获取代理渠道的套餐成本价

View File

@@ -103,6 +103,7 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
AllocatorShopID: allocatorShopID,
CostPrice: costPrice,
RetailPrice: pkg.SuggestedRetailPrice,
RetailPriceConfigStatus: pkg.PriceConfigStatus,
SeriesAllocationID: &seriesAllocation.ID,
Status: constants.StatusEnabled,
}
@@ -127,7 +128,15 @@ func (s *Service) getEnabledPackagesBySeries(ctx context.Context, seriesID uint)
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐列表失败")
}
return packages, nil
filtered := make([]*model.Package, 0, len(packages))
for _, pkg := range packages {
if pkg == nil || pkg.IsGift {
continue
}
filtered = append(filtered, pkg)
}
return filtered, nil
}
func (s *Service) calculateAdjustedPrice(basePrice int64, adjustment *dto.PriceAdjustment) int64 {

View File

@@ -181,6 +181,9 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
if pkgErr != nil {
continue
}
if pkg.IsGift {
continue
}
packages = append(packages, dto.ShopSeriesGrantPackageItem{
PackageID: pa.PackageID,
PackageName: pkg.PackageName,
@@ -325,6 +328,9 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
if pkgErr != nil || pkg.SeriesID != req.SeriesID {
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
}
if pkg.IsGift {
return errors.New(errors.CodeForbidden, "赠送套餐不能分配给代理")
}
// W2: 代理操作时,校验分配者已拥有此套餐授权,防止越权分配
if allocatorShopID > 0 {
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocatorShopID, item.PackageID)
@@ -338,6 +344,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
AllocatorShopID: allocatorShopID,
CostPrice: item.CostPrice,
RetailPrice: pkg.SuggestedRetailPrice,
RetailPriceConfigStatus: pkg.PriceConfigStatus,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,
@@ -660,6 +667,9 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
if pkgErr != nil || pkg.SeriesID != allocation.SeriesID {
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
}
if pkg.IsGift {
return errors.New(errors.CodeForbidden, "赠送套餐不能分配给代理")
}
if allocation.AllocatorShopID > 0 {
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocation.AllocatorShopID, item.PackageID)
if authErr != nil {
@@ -672,6 +682,7 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
AllocatorShopID: allocation.AllocatorShopID,
CostPrice: item.CostPrice,
RetailPrice: pkg.SuggestedRetailPrice,
RetailPriceConfigStatus: pkg.PriceConfigStatus,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,