refactor: 一次性佣金配置从套餐级别提升到系列级别
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:
2026-02-04 14:28:44 +08:00
parent fba8e9e76b
commit b18ecfeb55
106 changed files with 9899 additions and 6608 deletions

View File

@@ -20,6 +20,7 @@ type Service struct {
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore
shopStore *postgres.ShopStore
packageStore *postgres.PackageStore
packageSeriesStore *postgres.PackageSeriesStore
}
func New(
@@ -28,6 +29,7 @@ func New(
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore,
shopStore *postgres.ShopStore,
packageStore *postgres.PackageStore,
packageSeriesStore *postgres.PackageSeriesStore,
) *Service {
return &Service{
packageAllocationStore: packageAllocationStore,
@@ -35,6 +37,7 @@ func New(
priceHistoryStore: priceHistoryStore,
shopStore: shopStore,
packageStore: packageStore,
packageSeriesStore: packageSeriesStore,
}
}
@@ -73,25 +76,26 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopPackageAllocati
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
existing, _ := s.packageAllocationStore.GetByShopAndPackage(ctx, req.ShopID, req.PackageID)
if existing != nil {
return nil, errors.New(errors.CodeConflict, "该店铺已有此套餐的分配配置")
}
seriesAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, req.ShopID, pkg.SeriesID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeForbidden, "该套餐的系列未分配给此店铺")
return nil, errors.New(errors.CodeInvalidParam, "请先分配该套餐所属的系列")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取系列分配失败")
}
existing, _ := s.packageAllocationStore.GetByShopAndPackage(ctx, req.ShopID, req.PackageID)
if existing != nil {
return nil, errors.New(errors.CodeConflict, "该店铺已有此套餐的覆盖配置")
}
allocation := &model.ShopPackageAllocation{
ShopID: req.ShopID,
PackageID: req.PackageID,
AllocationID: seriesAllocation.ID,
CostPrice: req.CostPrice,
Status: constants.StatusEnabled,
ShopID: req.ShopID,
PackageID: req.PackageID,
AllocatorShopID: allocatorShopID,
CostPrice: req.CostPrice,
SeriesAllocationID: &seriesAllocation.ID,
Status: constants.StatusEnabled,
}
allocation.Creator = currentUserID
@@ -204,6 +208,12 @@ func (s *Service) List(ctx context.Context, req *dto.ShopPackageAllocationListRe
if req.PackageID != nil {
filters["package_id"] = *req.PackageID
}
if req.SeriesAllocationID != nil {
filters["series_allocation_id"] = *req.SeriesAllocationID
}
if req.AllocatorShopID != nil {
filters["allocator_shop_id"] = *req.AllocatorShopID
}
if req.Status != nil {
filters["status"] = *req.Status
}
@@ -258,19 +268,44 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
}
func (s *Service) buildResponse(ctx context.Context, a *model.ShopPackageAllocation, shopName, packageName, packageCode string) (*dto.ShopPackageAllocationResponse, error) {
var seriesID uint
seriesName := ""
pkg, _ := s.packageStore.GetByID(ctx, a.PackageID)
if pkg != nil {
seriesID = pkg.SeriesID
series, _ := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
if series != nil {
seriesName = series.SeriesName
}
}
allocatorShopName := ""
if a.AllocatorShopID > 0 {
allocatorShop, _ := s.shopStore.GetByID(ctx, a.AllocatorShopID)
if allocatorShop != nil {
allocatorShopName = allocatorShop.ShopName
}
} else {
allocatorShopName = "平台"
}
return &dto.ShopPackageAllocationResponse{
ID: a.ID,
ShopID: a.ShopID,
ShopName: shopName,
PackageID: a.PackageID,
PackageName: packageName,
PackageCode: packageCode,
AllocationID: a.AllocationID,
CostPrice: a.CostPrice,
CalculatedCostPrice: 0,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
ID: a.ID,
ShopID: a.ShopID,
ShopName: shopName,
PackageID: a.PackageID,
PackageName: packageName,
PackageCode: packageCode,
SeriesID: seriesID,
SeriesName: seriesName,
SeriesAllocationID: a.SeriesAllocationID,
AllocatorShopID: a.AllocatorShopID,
AllocatorShopName: allocatorShopName,
CostPrice: a.CostPrice,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
}, nil
}