refactor: 一次性佣金配置从套餐级别提升到系列级别
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
主要变更: - 新增 tb_shop_series_allocation 表,存储系列级别的一次性佣金配置 - ShopPackageAllocation 移除 one_time_commission_amount 字段 - PackageSeries 新增 enable_one_time_commission 字段控制是否启用一次性佣金 - 新增 /api/admin/shop-series-allocations CRUD 接口 - 佣金计算逻辑改为从 ShopSeriesAllocation 获取一次性佣金金额 - 删除废弃的 ShopSeriesOneTimeCommissionTier 模型 - OpenAPI Tag '系列分配' 和 '单套餐分配' 合并为 '套餐分配' 迁移脚本: - 000042: 重构佣金套餐模型 - 000043: 简化佣金分配 - 000044: 一次性佣金分配重构 - 000045: PackageSeries 添加 enable_one_time_commission 字段 测试: - 新增验收测试 (shop_series_allocation, commission_calculation) - 新增流程测试 (one_time_commission_chain) - 删除过时的单元测试(已被验收测试覆盖)
This commit is contained in:
@@ -2,7 +2,6 @@ package packagepkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -20,20 +19,17 @@ type Service struct {
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
}
|
||||
|
||||
func New(
|
||||
packageStore *postgres.PackageStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +44,20 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
|
||||
return nil, errors.New(errors.CodeConflict, "套餐编码已存在")
|
||||
}
|
||||
|
||||
// 校验虚流量配置:启用时虚流量必须 > 0 且 ≤ 真流量
|
||||
if req.EnableVirtualData {
|
||||
if req.VirtualDataMB == nil || *req.VirtualDataMB <= 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "启用虚流量时,虚流量额度必须大于0")
|
||||
}
|
||||
realDataMB := int64(0)
|
||||
if req.RealDataMB != nil {
|
||||
realDataMB = *req.RealDataMB
|
||||
}
|
||||
if *req.VirtualDataMB > realDataMB {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度")
|
||||
}
|
||||
}
|
||||
|
||||
var seriesName *string
|
||||
if req.SeriesID != nil && *req.SeriesID > 0 {
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
|
||||
@@ -61,32 +71,24 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
|
||||
}
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: req.PackageCode,
|
||||
PackageName: req.PackageName,
|
||||
PackageType: req.PackageType,
|
||||
DurationMonths: req.DurationMonths,
|
||||
Price: req.Price,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
PackageCode: req.PackageCode,
|
||||
PackageName: req.PackageName,
|
||||
PackageType: req.PackageType,
|
||||
DurationMonths: req.DurationMonths,
|
||||
CostPrice: req.CostPrice,
|
||||
EnableVirtualData: req.EnableVirtualData,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
}
|
||||
if req.SeriesID != nil {
|
||||
pkg.SeriesID = *req.SeriesID
|
||||
}
|
||||
if req.DataType != nil {
|
||||
pkg.DataType = *req.DataType
|
||||
}
|
||||
if req.RealDataMB != nil {
|
||||
pkg.RealDataMB = *req.RealDataMB
|
||||
}
|
||||
if req.VirtualDataMB != nil {
|
||||
pkg.VirtualDataMB = *req.VirtualDataMB
|
||||
}
|
||||
if req.DataAmountMB != nil {
|
||||
pkg.DataAmountMB = *req.DataAmountMB
|
||||
}
|
||||
if req.SuggestedCostPrice != nil {
|
||||
pkg.SuggestedCostPrice = *req.SuggestedCostPrice
|
||||
}
|
||||
if req.SuggestedRetailPrice != nil {
|
||||
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
|
||||
}
|
||||
@@ -147,7 +149,6 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
|
||||
pkg.SeriesID = *req.SeriesID
|
||||
seriesName = &series.SeriesName
|
||||
} else if pkg.SeriesID > 0 {
|
||||
// 如果没有更新 SeriesID,但现有套餐有 SeriesID,则查询当前的系列名称
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
if err == nil {
|
||||
seriesName = &series.SeriesName
|
||||
@@ -163,27 +164,32 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
|
||||
if req.DurationMonths != nil {
|
||||
pkg.DurationMonths = *req.DurationMonths
|
||||
}
|
||||
if req.DataType != nil {
|
||||
pkg.DataType = *req.DataType
|
||||
}
|
||||
if req.RealDataMB != nil {
|
||||
pkg.RealDataMB = *req.RealDataMB
|
||||
}
|
||||
if req.VirtualDataMB != nil {
|
||||
pkg.VirtualDataMB = *req.VirtualDataMB
|
||||
}
|
||||
if req.DataAmountMB != nil {
|
||||
pkg.DataAmountMB = *req.DataAmountMB
|
||||
if req.EnableVirtualData != nil {
|
||||
pkg.EnableVirtualData = *req.EnableVirtualData
|
||||
}
|
||||
if req.Price != nil {
|
||||
pkg.Price = *req.Price
|
||||
}
|
||||
if req.SuggestedCostPrice != nil {
|
||||
pkg.SuggestedCostPrice = *req.SuggestedCostPrice
|
||||
if req.CostPrice != nil {
|
||||
pkg.CostPrice = *req.CostPrice
|
||||
}
|
||||
if req.SuggestedRetailPrice != nil {
|
||||
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
// 校验虚流量配置
|
||||
if pkg.EnableVirtualData {
|
||||
if pkg.VirtualDataMB <= 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "启用虚流量时,虚流量额度必须大于0")
|
||||
}
|
||||
if pkg.VirtualDataMB > pkg.RealDataMB {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度")
|
||||
}
|
||||
}
|
||||
|
||||
pkg.Updater = currentUserID
|
||||
|
||||
if err := s.packageStore.Update(ctx, pkg); err != nil {
|
||||
@@ -246,9 +252,11 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐列表失败")
|
||||
}
|
||||
|
||||
// 收集所有唯一的 series_id
|
||||
// 收集所有唯一的 series_id 和 package_id
|
||||
seriesIDMap := make(map[uint]bool)
|
||||
for _, pkg := range packages {
|
||||
packageIDs := make([]uint, len(packages))
|
||||
for i, pkg := range packages {
|
||||
packageIDs[i] = pkg.ID
|
||||
if pkg.SeriesID > 0 {
|
||||
seriesIDMap[pkg.SeriesID] = true
|
||||
}
|
||||
@@ -270,10 +278,16 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
}
|
||||
}
|
||||
|
||||
// 构建响应,填充系列名称
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
var allocationMap map[uint]*model.ShopPackageAllocation
|
||||
if userType == constants.UserTypeAgent && shopID > 0 && len(packageIDs) > 0 {
|
||||
allocationMap = s.batchGetAllocationsForShop(ctx, shopID, packageIDs)
|
||||
}
|
||||
|
||||
responses := make([]*dto.PackageResponse, len(packages))
|
||||
for i, pkg := range packages {
|
||||
resp := s.toResponse(ctx, pkg)
|
||||
resp := s.toResponseWithAllocation(pkg, allocationMap)
|
||||
if pkg.SeriesID > 0 {
|
||||
if seriesName, ok := seriesMap[pkg.SeriesID]; ok {
|
||||
resp.SeriesName = &seriesName
|
||||
@@ -354,12 +368,10 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
DataType: pkg.DataType,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
DataAmountMB: pkg.DataAmountMB,
|
||||
Price: pkg.Price,
|
||||
SuggestedCostPrice: pkg.SuggestedCostPrice,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
@@ -372,34 +384,61 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
if userType == constants.UserTypeAgent && shopID > 0 {
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, pkg.ID)
|
||||
if err == nil && allocation != nil {
|
||||
resp.CostPrice = &allocation.CostPrice
|
||||
resp.CostPrice = allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
|
||||
commissionInfo := s.getCommissionInfo(ctx, allocation.AllocationID)
|
||||
if commissionInfo != nil {
|
||||
resp.CurrentCommissionRate = commissionInfo.CurrentRate
|
||||
resp.TierInfo = commissionInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Service) getCommissionInfo(ctx context.Context, allocationID uint) *dto.CommissionTierInfo {
|
||||
seriesAllocation, err := s.seriesAllocationStore.GetByID(ctx, allocationID)
|
||||
if err != nil {
|
||||
return nil
|
||||
func (s *Service) batchGetAllocationsForShop(ctx context.Context, shopID uint, packageIDs []uint) map[uint]*model.ShopPackageAllocation {
|
||||
allocationMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
|
||||
allocations, err := s.packageAllocationStore.GetByShopAndPackages(ctx, shopID, packageIDs)
|
||||
if err != nil || len(allocations) == 0 {
|
||||
return allocationMap
|
||||
}
|
||||
|
||||
info := &dto.CommissionTierInfo{}
|
||||
|
||||
if seriesAllocation.BaseCommissionMode == constants.CommissionModeFixed {
|
||||
info.CurrentRate = fmt.Sprintf("%.2f元/单", float64(seriesAllocation.BaseCommissionValue)/100)
|
||||
} else {
|
||||
info.CurrentRate = fmt.Sprintf("%.1f%%", float64(seriesAllocation.BaseCommissionValue)/10)
|
||||
for _, alloc := range allocations {
|
||||
allocationMap[alloc.PackageID] = alloc
|
||||
}
|
||||
|
||||
return info
|
||||
return allocationMap
|
||||
}
|
||||
|
||||
func (s *Service) toResponseWithAllocation(pkg *model.Package, allocationMap map[uint]*model.ShopPackageAllocation) *dto.PackageResponse {
|
||||
var seriesID *uint
|
||||
if pkg.SeriesID > 0 {
|
||||
seriesID = &pkg.SeriesID
|
||||
}
|
||||
|
||||
resp := &dto.PackageResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if allocationMap != nil {
|
||||
if allocation, ok := allocationMap[pkg.ID]; ok {
|
||||
resp.CostPrice = allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user