This commit is contained in:
@@ -11,6 +11,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"
|
||||
"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"
|
||||
@@ -526,13 +527,16 @@ func buildClientPackageItem(
|
||||
if pkg == nil || pkg.Status != constants.StatusEnabled {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
if pkg.IsGift {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
isAddon := pkg.PackageType == constants.PackageTypeAddon
|
||||
if isAddon && !resolved.MainPackageActived {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
retailPrice := pkg.SuggestedRetailPrice
|
||||
retailPrice := packageprice.PackageEffectiveRetailPrice(pkg)
|
||||
costPrice := pkg.CostPrice
|
||||
|
||||
if resolved.IsAgentChannel {
|
||||
@@ -543,7 +547,7 @@ func buildClientPackageItem(
|
||||
if allocation.ShelfStatus != constants.ShelfStatusOn || allocation.Status != constants.StatusEnabled {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
retailPrice = allocation.RetailPrice
|
||||
retailPrice = packageprice.AllocationEffectiveRetailPrice(allocation)
|
||||
costPrice = allocation.CostPrice
|
||||
} else if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||||
return dto.ClientPackageItem{}, false
|
||||
|
||||
@@ -143,7 +143,7 @@ type ClientPackageItem struct {
|
||||
PackageID uint `json:"package_id" description:"套餐ID"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
PackageType string `json:"package_type" description:"套餐类型 (formal:正式套餐, addon:加油包)"`
|
||||
RetailPrice int64 `json:"retail_price" description:"零售价(分)"`
|
||||
RetailPrice int64 `json:"retail_price" description:"生效零售价(分)"`
|
||||
CostPrice int64 `json:"cost_price" description:"成本价(分)"`
|
||||
ValidityDays int `json:"validity_days" description:"有效天数"`
|
||||
IsAddon bool `json:"is_addon" description:"是否加油包"`
|
||||
|
||||
@@ -6,11 +6,12 @@ type CreatePackageRequest struct {
|
||||
PackageName string `json:"package_name" validate:"required,min=1,max=255" required:"true" minLength:"1" maxLength:"255" description:"套餐名称"`
|
||||
SeriesID *uint `json:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
PackageType string `json:"package_type" validate:"required,oneof=formal addon" required:"true" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
IsGift bool `json:"is_gift" description:"是否赠送套餐"`
|
||||
DurationMonths int `json:"duration_months" validate:"required,min=1,max=120" required:"true" minimum:"1" maximum:"120" description:"套餐时长(月数)"`
|
||||
RealDataMB *int64 `json:"real_data_mb" validate:"omitempty,min=0" minimum:"0" description:"真流量额度(MB)"`
|
||||
VirtualDataMB *int64 `json:"virtual_data_mb" validate:"omitempty,min=0" minimum:"0" description:"虚流量额度(MB)"`
|
||||
EnableVirtualData bool `json:"enable_virtual_data" description:"是否启用虚流量"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price" validate:"omitempty,min=0" minimum:"0" description:"建议售价(分)"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price" validate:"omitempty,min=0" minimum:"0" description:"建议售价原始值(分,普通可售套餐留空表示未配置,赠送套餐必须显式为0)"`
|
||||
CostPrice int64 `json:"cost_price" validate:"required,min=0" required:"true" minimum:"0" description:"成本价(分)"`
|
||||
CalendarType *string `json:"calendar_type" validate:"omitempty,oneof=natural_month by_day" description:"套餐周期类型 (natural_month:自然月, by_day:按天)"`
|
||||
DurationDays *int `json:"duration_days" validate:"omitempty,min=1,max=3650" minimum:"1" maximum:"3650" description:"套餐天数(calendar_type=by_day时必填)"`
|
||||
@@ -23,11 +24,13 @@ type UpdatePackageRequest struct {
|
||||
PackageName *string `json:"package_name" validate:"omitempty,min=1,max=255" minLength:"1" maxLength:"255" description:"套餐名称"`
|
||||
SeriesID *uint `json:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
PackageType *string `json:"package_type" validate:"omitempty,oneof=formal addon" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
IsGift *bool `json:"is_gift" description:"是否赠送套餐"`
|
||||
DurationMonths *int `json:"duration_months" validate:"omitempty,min=1,max=120" minimum:"1" maximum:"120" description:"套餐时长(月数)"`
|
||||
RealDataMB *int64 `json:"real_data_mb" validate:"omitempty,min=0" minimum:"0" description:"真流量额度(MB)"`
|
||||
VirtualDataMB *int64 `json:"virtual_data_mb" validate:"omitempty,min=0" minimum:"0" description:"虚流量额度(MB)"`
|
||||
EnableVirtualData *bool `json:"enable_virtual_data" description:"是否启用虚流量"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price" validate:"omitempty,min=0" minimum:"0" description:"建议售价(分)"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price" validate:"omitempty,min=0" minimum:"0" description:"建议售价原始值(分,普通可售套餐留空表示未配置,赠送套餐必须显式为0)"`
|
||||
ClearSuggestedRetailPrice bool `json:"clear_suggested_retail_price" description:"是否清空建议售价并恢复为未配置状态"`
|
||||
CostPrice *int64 `json:"cost_price" validate:"omitempty,min=0" minimum:"0" description:"成本价(分)"`
|
||||
CalendarType *string `json:"calendar_type" validate:"omitempty,oneof=natural_month by_day" description:"套餐周期类型 (natural_month:自然月, by_day:按天)"`
|
||||
DurationDays *int `json:"duration_days" validate:"omitempty,min=1,max=3650" minimum:"1" maximum:"3650" description:"套餐天数(calendar_type=by_day时必填)"`
|
||||
@@ -58,7 +61,7 @@ type UpdatePackageShelfStatusRequest struct {
|
||||
|
||||
// UpdateRetailPriceRequest 更新零售价请求
|
||||
type UpdateRetailPriceRequest struct {
|
||||
RetailPrice int64 `json:"retail_price" validate:"required,min=0" required:"true" minimum:"0" description:"零售价(单位:分)"`
|
||||
RetailPrice int64 `json:"retail_price" validate:"required,min=0" required:"true" minimum:"0" description:"零售价原始值(分,普通可售套餐不允许显式0)"`
|
||||
}
|
||||
|
||||
// CommissionTierInfo 返佣梯度信息
|
||||
@@ -76,12 +79,15 @@ type PackageResponse struct {
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName *string `json:"series_name" description:"套餐系列名称"`
|
||||
PackageType string `json:"package_type" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
IsGift bool `json:"is_gift" description:"是否赠送套餐"`
|
||||
DurationMonths int `json:"duration_months" description:"套餐时长(月数)"`
|
||||
RealDataMB int64 `json:"real_data_mb" description:"真流量额度(MB)"`
|
||||
VirtualDataMB int64 `json:"virtual_data_mb" description:"虚流量额度(MB)"`
|
||||
EnableVirtualData bool `json:"enable_virtual_data" description:"是否启用虚流量"`
|
||||
VirtualRatio float64 `json:"virtual_ratio" description:"虚流量比例(real_data_mb/virtual_data_mb),启用虚流量时计算,否则为1.0"`
|
||||
SuggestedRetailPrice int64 `json:"suggested_retail_price" description:"建议售价(分)"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price,omitempty" description:"建议售价原始值(分,未配置时为空)"`
|
||||
PriceConfigStatus int `json:"price_config_status" description:"价格配置状态 (0:未配置, 1:赠送0价, 2:已配置非0)"`
|
||||
PriceConfigStatusName string `json:"price_config_status_name" description:"价格配置状态名称(中文)"`
|
||||
CostPrice int64 `json:"cost_price" description:"成本价(分)"`
|
||||
OneTimeCommissionAmount *int64 `json:"one_time_commission_amount,omitempty" description:"一次性佣金金额(分,代理视角)"`
|
||||
Status int `json:"status" description:"状态 (0:禁用, 1:启用)"`
|
||||
@@ -90,7 +96,9 @@ type PackageResponse struct {
|
||||
ShelfStatusName string `json:"shelf_status_name" description:"上架状态名称(中文)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
RetailPrice *int64 `json:"retail_price,omitempty" description:"代理零售价(分),仅代理用户可见"`
|
||||
RetailPrice *int64 `json:"retail_price,omitempty" description:"代理零售价原始值(分,仅代理用户可见,未配置时为空)"`
|
||||
RetailPriceConfigStatus *int `json:"retail_price_config_status,omitempty" description:"代理零售价配置状态 (0:未配置, 1:赠送0价, 2:已配置非0)"`
|
||||
EffectiveRetailPrice *int64 `json:"effective_retail_price,omitempty" description:"生效零售价(分,仅销售链路展示使用)"`
|
||||
ProfitMargin *int64 `json:"profit_margin,omitempty" description:"利润空间(分,仅代理用户可见)"`
|
||||
CurrentCommissionRate string `json:"current_commission_rate,omitempty" description:"当前返佣比例(仅代理用户可见)"`
|
||||
TierInfo *CommissionTierInfo `json:"tier_info,omitempty" description:"梯度返佣信息(仅代理用户可见)"`
|
||||
|
||||
@@ -130,6 +130,8 @@ type OrderItem struct {
|
||||
Quantity int `gorm:"column:quantity;type:int;default:1;not null;comment:数量" json:"quantity"`
|
||||
UnitPrice int64 `gorm:"column:unit_price;type:bigint;not null;comment:单价(分)" json:"unit_price"`
|
||||
Amount int64 `gorm:"column:amount;type:bigint;not null;comment:小计金额(分)" json:"amount"`
|
||||
PackagePriceConfigStatus int `gorm:"column:package_price_config_status;type:int;default:0;not null;comment:套餐价格配置状态快照 0-未配置 1-赠送0价 2-已配置非0" json:"package_price_config_status"`
|
||||
PackageIsGift bool `gorm:"column:package_is_gift;type:boolean;default:false;not null;comment:套餐是否赠送快照 true-赠送 false-普通可售" json:"package_is_gift"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -41,6 +41,8 @@ type Package struct {
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 0=禁用 1=启用" json:"status"`
|
||||
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"`
|
||||
SuggestedRetailPrice int64 `gorm:"column:suggested_retail_price;type:bigint;default:0;comment:建议售价(分为单位)" json:"suggested_retail_price"`
|
||||
PriceConfigStatus int `gorm:"column:price_config_status;type:int;default:0;not null;comment:价格配置状态 0-未配置 1-赠送0价 2-已配置非0" json:"price_config_status"`
|
||||
IsGift bool `gorm:"column:is_gift;type:boolean;default:false;not null;comment:是否赠送套餐 true-赠送 false-普通可售" json:"is_gift"`
|
||||
ShelfStatus int `gorm:"column:shelf_status;type:int;default:2;not null;comment:上架状态 1-上架 2-下架" json:"shelf_status"`
|
||||
CalendarType string `gorm:"column:calendar_type;type:varchar(20);default:'by_day';comment:套餐周期类型 natural_month-自然月 by_day-按天" json:"calendar_type"`
|
||||
DurationDays int `gorm:"column:duration_days;type:int;comment:套餐天数(calendar_type=by_day时必填)" json:"duration_days"`
|
||||
@@ -83,6 +85,8 @@ type PackageUsage struct {
|
||||
PendingRealnameActivation bool `gorm:"column:pending_realname_activation;type:boolean;default:false;comment:是否等待实名激活(true-待实名后激活 false-已激活或不需实名)" json:"pending_realname_activation"`
|
||||
PackageName string `gorm:"column:package_name;type:varchar(255);not null;default:'';comment:套餐名称快照(从Package复制,用于历史记录展示)" json:"package_name"`
|
||||
PaidAmount *int64 `gorm:"column:paid_amount;type:bigint;comment:购买实付金额快照(分,从订单复制,无订单或线下支付时为null)" json:"paid_amount,omitempty"`
|
||||
PackagePriceConfigStatus int `gorm:"column:package_price_config_status;type:int;default:0;not null;comment:套餐价格配置状态快照 0-未配置 1-赠送0价 2-已配置非0" json:"package_price_config_status"`
|
||||
PackageIsGift bool `gorm:"column:package_is_gift;type:boolean;default:false;not null;comment:套餐是否赠送快照 true-赠送 false-普通可售" json:"package_is_gift"`
|
||||
DataResetCycle string `gorm:"column:data_reset_cycle;type:varchar(20);comment:流量重置周期(从Package复制,用于历史记录)" json:"data_reset_cycle"`
|
||||
LastResetAt *time.Time `gorm:"column:last_reset_at;comment:最后一次流量重置时间" json:"last_reset_at"`
|
||||
NextResetAt *time.Time `gorm:"column:next_reset_at;index:idx_package_usage_next_reset_at;comment:下次流量重置时间(用于定时任务查询)" json:"next_reset_at"`
|
||||
|
||||
@@ -15,6 +15,7 @@ type ShopPackageAllocation struct {
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 0=禁用 1=启用" json:"status"`
|
||||
ShelfStatus int `gorm:"column:shelf_status;type:int;default:1;not null;comment:上架状态 1-上架 2-下架" json:"shelf_status"`
|
||||
RetailPrice int64 `gorm:"column:retail_price;type:bigint;not null;default:0;comment:代理面向终端客户的零售价(分)" json:"retail_price"`
|
||||
RetailPriceConfigStatus int `gorm:"column:retail_price_config_status;type:int;default:0;not null;comment:零售价配置状态 0-未配置 1-赠送0价 2-已配置非0" json:"retail_price_config_status"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -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 {
|
||||
// ==== 场景 1:offline(线下支付)====
|
||||
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" {
|
||||
|
||||
@@ -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
|
||||
|
||||
114
internal/service/packageprice/policy.go
Normal file
114
internal/service/packageprice/policy.go
Normal 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
|
||||
}
|
||||
@@ -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 获取代理渠道的套餐成本价
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -63,6 +63,7 @@ func (s *PackageStore) List(ctx context.Context, opts *store.QueryOptions, filte
|
||||
query = query.Joins("INNER JOIN tb_shop_package_allocation ON tb_shop_package_allocation.package_id = tb_package.id AND tb_shop_package_allocation.deleted_at IS NULL").
|
||||
Where("tb_shop_package_allocation.shop_id = ? AND tb_shop_package_allocation.status = ?",
|
||||
shopID, constants.StatusEnabled)
|
||||
query = query.Where("tb_package.is_gift = ?", false)
|
||||
}
|
||||
|
||||
if packageName, ok := filters["package_name"].(string); ok && packageName != "" {
|
||||
|
||||
@@ -139,13 +139,14 @@ func (s *ShopPackageAllocationStore) UpdateShelfStatus(ctx context.Context, id u
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationStore) UpdateRetailPrice(ctx context.Context, id uint, retailPrice int64, updater uint) error {
|
||||
func (s *ShopPackageAllocationStore) UpdateRetailPrice(ctx context.Context, id uint, retailPrice int64, priceConfigStatus int, updater uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.ShopPackageAllocation{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"retail_price": retailPrice,
|
||||
"updater": updater,
|
||||
"retail_price": retailPrice,
|
||||
"retail_price_config_status": priceConfigStatus,
|
||||
"updater": updater,
|
||||
}).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -337,7 +338,10 @@ func (h *AutoPurchaseHandler) loadPackages(ctx context.Context, packageIDs []uin
|
||||
|
||||
totalAmount := int64(0)
|
||||
for _, pkg := range packages {
|
||||
totalAmount += pkg.SuggestedRetailPrice
|
||||
if pkg.IsGift {
|
||||
return nil, 0, errors.New("赠送套餐不能进入自动购包链路")
|
||||
}
|
||||
totalAmount += packageprice.PackageEffectiveRetailPrice(pkg)
|
||||
}
|
||||
|
||||
if err := validatePackageTypeMix(packages); err != nil {
|
||||
@@ -399,16 +403,19 @@ func (h *AutoPurchaseHandler) buildOrderAndItems(
|
||||
|
||||
items := make([]*model.OrderItem, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
unitPrice := packageprice.PackageEffectiveRetailPrice(pkg)
|
||||
items = append(items, &model.OrderItem{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: rechargeOrder.UserID,
|
||||
Updater: rechargeOrder.UserID,
|
||||
},
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
Quantity: 1,
|
||||
UnitPrice: pkg.SuggestedRetailPrice,
|
||||
Amount: pkg.SuggestedRetailPrice,
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
Quantity: 1,
|
||||
UnitPrice: unitPrice,
|
||||
Amount: unitPrice,
|
||||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PackageIsGift: pkg.IsGift,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -525,6 +532,8 @@ func (h *AutoPurchaseHandler) activateMainPackage(
|
||||
PendingRealnameActivation: pendingRealnameActivation,
|
||||
Generation: order.Generation,
|
||||
PaidAmount: order.ActualPaidAmount,
|
||||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PackageIsGift: pkg.IsGift,
|
||||
}
|
||||
|
||||
if carrierType == constants.AssetWalletResourceTypeIotCard {
|
||||
@@ -604,6 +613,8 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
Generation: order.Generation,
|
||||
PaidAmount: order.ActualPaidAmount,
|
||||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PackageIsGift: pkg.IsGift,
|
||||
}
|
||||
|
||||
if carrierType == constants.AssetWalletResourceTypeIotCard {
|
||||
|
||||
Reference in New Issue
Block a user