重构: 店铺套餐分配系统从加价模式改为返佣模式
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m18s

主要变更:
- 重构分配模型:从加价模式(pricing_mode/pricing_value)改为返佣模式(base_commission + tier_commission)
- 删除独立的 my_package 接口,统一到 /api/admin/packages(通过数据权限自动过滤)
- 新增批量分配和批量调价功能,支持事务和性能优化
- 新增配置版本管理,订单创建时锁定返佣配置
- 新增成本价历史记录,支持审计和纠纷处理
- 新增统计缓存系统(Redis + 异步任务),优化梯度返佣计算性能
- 删除冗余的梯度佣金独立 CRUD 接口(合并到分配配置中)
- 归档 3 个已完成的 OpenSpec changes 并同步 8 个新 capabilities 到 main specs

技术细节:
- 数据库迁移:000026_refactor_shop_package_allocation
- 新增 Store:AllocationConfigStore, PriceHistoryStore, CommissionStatsStore
- 新增 Service:BatchAllocationService, BatchPricingService, CommissionStatsService
- 新增异步任务:统计更新、定时同步、周期归档
- 测试覆盖:批量操作集成测试、梯度佣金 CRUD 清理验证

影响:
- API 变更:删除 4 个梯度 CRUD 接口(POST/GET/PUT/DELETE /:id/tiers)
- API 新增:批量分配、批量调价接口
- 数据模型:重构 shop_series_allocation 表结构
- 性能优化:批量操作使用 CreateInBatches,统计使用 Redis 缓存

相关文档:
- openspec/changes/archive/2026-01-28-refactor-shop-package-allocation/
- openspec/specs/agent-available-packages/
- openspec/specs/allocation-config-versioning/
- 等 8 个新 capability specs
This commit is contained in:
2026-01-28 17:11:55 +08:00
parent 23eb0307bb
commit 1da680a790
97 changed files with 6810 additions and 3622 deletions

View File

@@ -18,6 +18,7 @@ import (
type Service struct {
allocationStore *postgres.ShopSeriesAllocationStore
tierStore *postgres.ShopSeriesCommissionTierStore
configStore *postgres.ShopSeriesAllocationConfigStore
shopStore *postgres.ShopStore
packageSeriesStore *postgres.PackageSeriesStore
packageStore *postgres.PackageStore
@@ -26,6 +27,7 @@ type Service struct {
func New(
allocationStore *postgres.ShopSeriesAllocationStore,
tierStore *postgres.ShopSeriesCommissionTierStore,
configStore *postgres.ShopSeriesAllocationConfigStore,
shopStore *postgres.ShopStore,
packageSeriesStore *postgres.PackageSeriesStore,
packageStore *postgres.PackageStore,
@@ -33,6 +35,7 @@ func New(
return &Service{
allocationStore: allocationStore,
tierStore: tierStore,
configStore: configStore,
shopStore: shopStore,
packageSeriesStore: packageSeriesStore,
packageStore: packageStore,
@@ -97,15 +100,13 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
}
allocation := &model.ShopSeriesAllocation{
ShopID: req.ShopID,
SeriesID: req.SeriesID,
AllocatorShopID: allocatorShopID,
PricingMode: req.PricingMode,
PricingValue: req.PricingValue,
OneTimeCommissionTrigger: req.OneTimeCommissionTrigger,
OneTimeCommissionThreshold: req.OneTimeCommissionThreshold,
OneTimeCommissionAmount: req.OneTimeCommissionAmount,
Status: constants.StatusEnabled,
ShopID: req.ShopID,
SeriesID: req.SeriesID,
AllocatorShopID: allocatorShopID,
BaseCommissionMode: req.BaseCommission.Mode,
BaseCommissionValue: req.BaseCommission.Value,
EnableTierCommission: req.EnableTierCommission,
Status: constants.StatusEnabled,
}
allocation.Creator = currentUserID
@@ -154,23 +155,29 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
return nil, fmt.Errorf("获取分配记录失败: %w", err)
}
if req.PricingMode != nil {
allocation.PricingMode = *req.PricingMode
configChanged := false
if req.BaseCommission != nil {
if allocation.BaseCommissionMode != req.BaseCommission.Mode ||
allocation.BaseCommissionValue != req.BaseCommission.Value {
configChanged = true
}
allocation.BaseCommissionMode = req.BaseCommission.Mode
allocation.BaseCommissionValue = req.BaseCommission.Value
}
if req.PricingValue != nil {
allocation.PricingValue = *req.PricingValue
}
if req.OneTimeCommissionTrigger != nil {
allocation.OneTimeCommissionTrigger = *req.OneTimeCommissionTrigger
}
if req.OneTimeCommissionThreshold != nil {
allocation.OneTimeCommissionThreshold = *req.OneTimeCommissionThreshold
}
if req.OneTimeCommissionAmount != nil {
allocation.OneTimeCommissionAmount = *req.OneTimeCommissionAmount
if req.EnableTierCommission != nil {
if allocation.EnableTierCommission != *req.EnableTierCommission {
configChanged = true
}
allocation.EnableTierCommission = *req.EnableTierCommission
}
allocation.Updater = currentUserID
if configChanged {
if err := s.createNewConfigVersion(ctx, allocation); err != nil {
return nil, fmt.Errorf("创建配置版本失败: %w", err)
}
}
if err := s.allocationStore.Update(ctx, allocation); err != nil {
return nil, fmt.Errorf("更新分配失败: %w", err)
}
@@ -306,177 +313,7 @@ func (s *Service) GetParentCostPrice(ctx context.Context, shopID, packageID uint
return pkg.SuggestedCostPrice, nil
}
allocation, err := s.allocationStore.GetByShopAndSeries(ctx, shopID, pkg.SeriesID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return 0, errors.New(errors.CodeNotFound, "未找到分配记录")
}
return 0, fmt.Errorf("获取分配记录失败: %w", err)
}
parentCostPrice, err := s.GetParentCostPrice(ctx, allocation.AllocatorShopID, packageID)
if err != nil {
return 0, err
}
return s.CalculateCostPrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue), nil
}
func (s *Service) CalculateCostPrice(parentCostPrice int64, pricingMode string, pricingValue int64) int64 {
switch pricingMode {
case model.PricingModeFixed:
return parentCostPrice + pricingValue
case model.PricingModePercent:
return parentCostPrice + (parentCostPrice * pricingValue / 1000)
default:
return parentCostPrice
}
}
func (s *Service) AddTier(ctx context.Context, allocationID uint, req *dto.CreateCommissionTierRequest) (*dto.CommissionTierResponse, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
_, err := s.allocationStore.GetByID(ctx, allocationID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
}
return nil, fmt.Errorf("获取分配记录失败: %w", err)
}
if req.PeriodType == model.PeriodTypeCustom {
if req.PeriodStartDate == nil || req.PeriodEndDate == nil {
return nil, errors.New(errors.CodeInvalidParam, "自定义周期必须指定开始和结束日期")
}
}
tier := &model.ShopSeriesCommissionTier{
AllocationID: allocationID,
TierType: req.TierType,
PeriodType: req.PeriodType,
ThresholdValue: req.ThresholdValue,
CommissionAmount: req.CommissionAmount,
}
tier.Creator = currentUserID
if req.PeriodStartDate != nil {
t, err := time.Parse("2006-01-02", *req.PeriodStartDate)
if err != nil {
return nil, errors.New(errors.CodeInvalidParam, "开始日期格式无效")
}
tier.PeriodStartDate = &t
}
if req.PeriodEndDate != nil {
t, err := time.Parse("2006-01-02", *req.PeriodEndDate)
if err != nil {
return nil, errors.New(errors.CodeInvalidParam, "结束日期格式无效")
}
tier.PeriodEndDate = &t
}
if err := s.tierStore.Create(ctx, tier); err != nil {
return nil, fmt.Errorf("创建梯度配置失败: %w", err)
}
return s.buildTierResponse(tier), nil
}
func (s *Service) UpdateTier(ctx context.Context, allocationID, tierID uint, req *dto.UpdateCommissionTierRequest) (*dto.CommissionTierResponse, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
tier, err := s.tierStore.GetByID(ctx, tierID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "梯度配置不存在")
}
return nil, fmt.Errorf("获取梯度配置失败: %w", err)
}
if tier.AllocationID != allocationID {
return nil, errors.New(errors.CodeForbidden, "梯度配置不属于该分配")
}
if req.TierType != nil {
tier.TierType = *req.TierType
}
if req.PeriodType != nil {
tier.PeriodType = *req.PeriodType
}
if req.ThresholdValue != nil {
tier.ThresholdValue = *req.ThresholdValue
}
if req.CommissionAmount != nil {
tier.CommissionAmount = *req.CommissionAmount
}
if req.PeriodStartDate != nil {
t, err := time.Parse("2006-01-02", *req.PeriodStartDate)
if err != nil {
return nil, errors.New(errors.CodeInvalidParam, "开始日期格式无效")
}
tier.PeriodStartDate = &t
}
if req.PeriodEndDate != nil {
t, err := time.Parse("2006-01-02", *req.PeriodEndDate)
if err != nil {
return nil, errors.New(errors.CodeInvalidParam, "结束日期格式无效")
}
tier.PeriodEndDate = &t
}
tier.Updater = currentUserID
if err := s.tierStore.Update(ctx, tier); err != nil {
return nil, fmt.Errorf("更新梯度配置失败: %w", err)
}
return s.buildTierResponse(tier), nil
}
func (s *Service) DeleteTier(ctx context.Context, allocationID, tierID uint) error {
tier, err := s.tierStore.GetByID(ctx, tierID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "梯度配置不存在")
}
return fmt.Errorf("获取梯度配置失败: %w", err)
}
if tier.AllocationID != allocationID {
return errors.New(errors.CodeForbidden, "梯度配置不属于该分配")
}
if err := s.tierStore.Delete(ctx, tierID); err != nil {
return fmt.Errorf("删除梯度配置失败: %w", err)
}
return nil
}
func (s *Service) ListTiers(ctx context.Context, allocationID uint) ([]*dto.CommissionTierResponse, error) {
_, err := s.allocationStore.GetByID(ctx, allocationID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
}
return nil, fmt.Errorf("获取分配记录失败: %w", err)
}
tiers, err := s.tierStore.ListByAllocationID(ctx, allocationID)
if err != nil {
return nil, fmt.Errorf("查询梯度配置失败: %w", err)
}
responses := make([]*dto.CommissionTierResponse, len(tiers))
for i, t := range tiers {
responses[i] = s.buildTierResponse(t)
}
return responses, nil
return 0, errors.New(errors.CodeInvalidParam, "自动计算成本价功能已移除,请手动设置成本价")
}
func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocation, shopName, seriesName string) (*dto.ShopSeriesAllocationResponse, error) {
@@ -486,46 +323,78 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocati
allocatorShopName = allocatorShop.ShopName
}
var calculatedCostPrice int64 = 0
return &dto.ShopSeriesAllocationResponse{
ID: a.ID,
ShopID: a.ShopID,
ShopName: shopName,
SeriesID: a.SeriesID,
SeriesName: seriesName,
AllocatorShopID: a.AllocatorShopID,
AllocatorShopName: allocatorShopName,
PricingMode: a.PricingMode,
PricingValue: a.PricingValue,
CalculatedCostPrice: calculatedCostPrice,
OneTimeCommissionTrigger: a.OneTimeCommissionTrigger,
OneTimeCommissionThreshold: a.OneTimeCommissionThreshold,
OneTimeCommissionAmount: a.OneTimeCommissionAmount,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
ID: a.ID,
ShopID: a.ShopID,
ShopName: shopName,
SeriesID: a.SeriesID,
SeriesName: seriesName,
AllocatorShopID: a.AllocatorShopID,
AllocatorShopName: allocatorShopName,
BaseCommission: dto.BaseCommissionConfig{
Mode: a.BaseCommissionMode,
Value: a.BaseCommissionValue,
},
EnableTierCommission: a.EnableTierCommission,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
}, nil
}
func (s *Service) buildTierResponse(t *model.ShopSeriesCommissionTier) *dto.CommissionTierResponse {
resp := &dto.CommissionTierResponse{
ID: t.ID,
AllocationID: t.AllocationID,
TierType: t.TierType,
PeriodType: t.PeriodType,
ThresholdValue: t.ThresholdValue,
CommissionAmount: t.CommissionAmount,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
UpdatedAt: t.UpdatedAt.Format(time.RFC3339),
func (s *Service) createNewConfigVersion(ctx context.Context, allocation *model.ShopSeriesAllocation) error {
now := time.Now()
if err := s.configStore.InvalidateCurrent(ctx, allocation.ID, now); err != nil {
return fmt.Errorf("失效当前配置版本失败: %w", err)
}
if t.PeriodStartDate != nil {
resp.PeriodStartDate = t.PeriodStartDate.Format("2006-01-02")
}
if t.PeriodEndDate != nil {
resp.PeriodEndDate = t.PeriodEndDate.Format("2006-01-02")
latestVersion, err := s.configStore.GetLatestVersion(ctx, allocation.ID)
newVersion := 1
if err == nil && latestVersion != nil {
newVersion = latestVersion.Version + 1
}
return resp
newConfig := &model.ShopSeriesAllocationConfig{
AllocationID: allocation.ID,
Version: newVersion,
BaseCommissionMode: allocation.BaseCommissionMode,
BaseCommissionValue: allocation.BaseCommissionValue,
EnableTierCommission: allocation.EnableTierCommission,
EffectiveFrom: now,
}
if err := s.configStore.Create(ctx, newConfig); err != nil {
return fmt.Errorf("创建新配置版本失败: %w", err)
}
return nil
}
func (s *Service) GetEffectiveConfig(ctx context.Context, allocationID uint, at time.Time) (*model.ShopSeriesAllocationConfig, error) {
config, err := s.configStore.GetEffective(ctx, allocationID, at)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "未找到生效的配置版本")
}
return nil, fmt.Errorf("获取生效配置失败: %w", err)
}
return config, nil
}
func (s *Service) ListConfigVersions(ctx context.Context, allocationID uint) ([]*model.ShopSeriesAllocationConfig, error) {
_, err := s.allocationStore.GetByID(ctx, allocationID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
}
return nil, fmt.Errorf("获取分配记录失败: %w", err)
}
configs, err := s.configStore.List(ctx, allocationID)
if err != nil {
return nil, fmt.Errorf("获取配置版本列表失败: %w", err)
}
return configs, nil
}

View File

@@ -1,595 +0,0 @@
package shop_series_allocation
import (
"context"
"testing"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"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"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func createTestService(t *testing.T) (*Service, *postgres.ShopSeriesAllocationStore, *postgres.ShopStore, *postgres.PackageSeriesStore, *postgres.PackageStore, *postgres.ShopSeriesCommissionTierStore) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
allocationStore := postgres.NewShopSeriesAllocationStore(tx)
tierStore := postgres.NewShopSeriesCommissionTierStore(tx)
shopStore := postgres.NewShopStore(tx, rdb)
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
packageStore := postgres.NewPackageStore(tx)
svc := New(allocationStore, tierStore, shopStore, packageSeriesStore, packageStore)
return svc, allocationStore, shopStore, packageSeriesStore, packageStore, tierStore
}
func createContextWithUser(userID uint, userType int, shopID uint) context.Context {
ctx := context.Background()
info := &middleware.UserContextInfo{
UserID: userID,
UserType: userType,
ShopID: shopID,
}
return middleware.SetUserContext(ctx, info)
}
func createTestShop(t *testing.T, store *postgres.ShopStore, ctx context.Context, shopName string, parentID *uint) *model.Shop {
shop := &model.Shop{
ShopName: shopName,
ShopCode: shopName,
ParentID: parentID,
Status: constants.StatusEnabled,
}
shop.Creator = 1
err := store.Create(ctx, shop)
require.NoError(t, err)
return shop
}
func createTestSeries(t *testing.T, store *postgres.PackageSeriesStore, ctx context.Context, seriesName string) *model.PackageSeries {
series := &model.PackageSeries{
SeriesName: seriesName,
SeriesCode: seriesName,
Status: constants.StatusEnabled,
}
series.Creator = 1
err := store.Create(ctx, series)
require.NoError(t, err)
return series
}
func TestService_CalculateCostPrice(t *testing.T) {
svc, _, _, _, _, _ := createTestService(t)
tests := []struct {
name string
parentCostPrice int64
pricingMode string
pricingValue int64
expectedCostPrice int64
description string
}{
{
name: "固定加价模式10000 + 500 = 10500",
parentCostPrice: 10000,
pricingMode: model.PricingModeFixed,
pricingValue: 500,
expectedCostPrice: 10500,
description: "固定金额加价",
},
{
name: "百分比加价模式10000 + 10000*100/1000 = 11000",
parentCostPrice: 10000,
pricingMode: model.PricingModePercent,
pricingValue: 100,
expectedCostPrice: 11000,
description: "百分比加价100 = 10%",
},
{
name: "百分比加价模式5000 + 5000*50/1000 = 5250",
parentCostPrice: 5000,
pricingMode: model.PricingModePercent,
pricingValue: 50,
expectedCostPrice: 5250,
description: "百分比加价50 = 5%",
},
{
name: "未知加价模式:返回原价",
parentCostPrice: 10000,
pricingMode: "unknown",
pricingValue: 500,
expectedCostPrice: 10000,
description: "未知加价模式返回原价",
},
{
name: "固定加价为010000 + 0 = 10000",
parentCostPrice: 10000,
pricingMode: model.PricingModeFixed,
pricingValue: 0,
expectedCostPrice: 10000,
description: "固定加价为0",
},
{
name: "百分比加价为010000 + 0 = 10000",
parentCostPrice: 10000,
pricingMode: model.PricingModePercent,
pricingValue: 0,
expectedCostPrice: 10000,
description: "百分比加价为0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := svc.CalculateCostPrice(tt.parentCostPrice, tt.pricingMode, tt.pricingValue)
assert.Equal(t, tt.expectedCostPrice, result, tt.description)
})
}
}
func TestService_Create_Validation(t *testing.T) {
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
ctx := context.Background()
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
unrelatedShop := createTestShop(t, shopStore, ctx, "无关店铺", nil)
series := createTestSeries(t, seriesStore, ctx, "测试系列")
t.Run("未授权访问:无用户上下文", func(t *testing.T) {
emptyCtx := context.Background()
req := &dto.CreateShopSeriesAllocationRequest{
ShopID: childShop.ID,
SeriesID: series.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
}
_, err := svc.Create(emptyCtx, req)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
})
t.Run("代理账号无店铺上下文", func(t *testing.T) {
ctxWithoutShop := createContextWithUser(1, constants.UserTypeAgent, 0)
req := &dto.CreateShopSeriesAllocationRequest{
ShopID: childShop.ID,
SeriesID: series.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
}
_, err := svc.Create(ctxWithoutShop, req)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
})
t.Run("分配给非直属下级店铺", func(t *testing.T) {
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
req := &dto.CreateShopSeriesAllocationRequest{
ShopID: unrelatedShop.ID,
SeriesID: series.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
}
_, err := svc.Create(ctxParent, req)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeForbidden, appErr.Code)
})
t.Run("代理账号无该系列分配权限", func(t *testing.T) {
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
series2 := createTestSeries(t, seriesStore, ctx, "测试系列2")
req := &dto.CreateShopSeriesAllocationRequest{
ShopID: childShop.ID,
SeriesID: series2.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
}
_, err := svc.Create(ctxParent, req)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeForbidden, appErr.Code)
})
t.Run("重复分配:同一店铺和系列已分配", func(t *testing.T) {
series3 := createTestSeries(t, seriesStore, ctx, "测试系列3")
childShop2 := createTestShop(t, shopStore, ctx, "二级代理2", &parentShop.ID)
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
parentAllocation := &model.ShopSeriesAllocation{
ShopID: parentShop.ID,
SeriesID: series3.ID,
AllocatorShopID: 0,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
parentAllocation.Creator = 1
err := allocationStore.Create(ctx, parentAllocation)
require.NoError(t, err)
req := &dto.CreateShopSeriesAllocationRequest{
ShopID: childShop2.ID,
SeriesID: series3.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
}
resp1, err := svc.Create(ctxParent, req)
require.NoError(t, err)
assert.NotNil(t, resp1)
_, err = svc.Create(ctxParent, req)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeConflict, appErr.Code)
})
t.Run("成功创建分配:代理有该系列权限", func(t *testing.T) {
series4 := createTestSeries(t, seriesStore, ctx, "测试系列4")
childShop3 := createTestShop(t, shopStore, ctx, "二级代理3", &parentShop.ID)
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
parentAllocation := &model.ShopSeriesAllocation{
ShopID: parentShop.ID,
SeriesID: series4.ID,
AllocatorShopID: 0,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
parentAllocation.Creator = 1
err := allocationStore.Create(ctx, parentAllocation)
require.NoError(t, err)
req := &dto.CreateShopSeriesAllocationRequest{
ShopID: childShop3.ID,
SeriesID: series4.ID,
PricingMode: model.PricingModePercent,
PricingValue: 100,
}
resp, err := svc.Create(ctxParent, req)
require.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, childShop3.ID, resp.ShopID)
assert.Equal(t, series4.ID, resp.SeriesID)
assert.Equal(t, model.PricingModePercent, resp.PricingMode)
assert.Equal(t, int64(100), resp.PricingValue)
})
t.Run("平台用户需要有店铺上下文才能分配", func(t *testing.T) {
series5 := createTestSeries(t, seriesStore, ctx, "测试系列5")
childShop4 := createTestShop(t, shopStore, ctx, "二级代理4", &parentShop.ID)
ctxPlatform := createContextWithUser(2, constants.UserTypePlatform, 0)
req := &dto.CreateShopSeriesAllocationRequest{
ShopID: childShop4.ID,
SeriesID: series5.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 1000,
}
_, err := svc.Create(ctxPlatform, req)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeForbidden, appErr.Code)
})
}
func TestService_Delete_WithDependency(t *testing.T) {
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
ctx := context.Background()
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
_ = createTestShop(t, shopStore, ctx, "三级代理", &childShop.ID)
series := createTestSeries(t, seriesStore, ctx, "测试系列")
t.Run("删除无依赖的分配成功", func(t *testing.T) {
allocation := &model.ShopSeriesAllocation{
ShopID: childShop.ID,
SeriesID: series.ID,
AllocatorShopID: parentShop.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
allocation.Creator = 1
err := allocationStore.Create(ctx, allocation)
require.NoError(t, err)
err = svc.Delete(ctx, allocation.ID)
require.NoError(t, err)
_, err = allocationStore.GetByID(ctx, allocation.ID)
require.Error(t, err)
assert.Equal(t, gorm.ErrRecordNotFound, err)
})
t.Run("删除分配成功(无依赖关系)", func(t *testing.T) {
series2 := createTestSeries(t, seriesStore, ctx, "测试系列2")
allocation1 := &model.ShopSeriesAllocation{
ShopID: childShop.ID,
SeriesID: series2.ID,
AllocatorShopID: parentShop.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
allocation1.Creator = 1
err := allocationStore.Create(ctx, allocation1)
require.NoError(t, err)
err = svc.Delete(ctx, allocation1.ID)
require.NoError(t, err)
_, err = allocationStore.GetByID(ctx, allocation1.ID)
require.Error(t, err)
assert.Equal(t, gorm.ErrRecordNotFound, err)
})
t.Run("删除不存在的分配返回错误", func(t *testing.T) {
err := svc.Delete(ctx, 99999)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeNotFound, appErr.Code)
})
}
func TestService_Get(t *testing.T) {
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
ctx := context.Background()
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
series := createTestSeries(t, seriesStore, ctx, "测试系列")
allocation := &model.ShopSeriesAllocation{
ShopID: childShop.ID,
SeriesID: series.ID,
AllocatorShopID: parentShop.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
allocation.Creator = 1
err := allocationStore.Create(ctx, allocation)
require.NoError(t, err)
t.Run("获取存在的分配", func(t *testing.T) {
resp, err := svc.Get(ctx, allocation.ID)
require.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, allocation.ID, resp.ID)
assert.Equal(t, childShop.ID, resp.ShopID)
assert.Equal(t, series.ID, resp.SeriesID)
})
t.Run("获取不存在的分配", func(t *testing.T) {
_, err := svc.Get(ctx, 99999)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeNotFound, appErr.Code)
})
}
func TestService_Update(t *testing.T) {
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
ctx := context.Background()
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
series := createTestSeries(t, seriesStore, ctx, "测试系列")
allocation := &model.ShopSeriesAllocation{
ShopID: childShop.ID,
SeriesID: series.ID,
AllocatorShopID: parentShop.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
allocation.Creator = 1
err := allocationStore.Create(ctx, allocation)
require.NoError(t, err)
t.Run("更新加价模式和加价值", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
newMode := model.PricingModePercent
newValue := int64(100)
req := &dto.UpdateShopSeriesAllocationRequest{
PricingMode: &newMode,
PricingValue: &newValue,
}
resp, err := svc.Update(ctxWithUser, allocation.ID, req)
require.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, model.PricingModePercent, resp.PricingMode)
assert.Equal(t, int64(100), resp.PricingValue)
})
t.Run("更新不存在的分配", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
newMode := model.PricingModeFixed
req := &dto.UpdateShopSeriesAllocationRequest{
PricingMode: &newMode,
}
_, err := svc.Update(ctxWithUser, 99999, req)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeNotFound, appErr.Code)
})
}
func TestService_UpdateStatus(t *testing.T) {
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
ctx := context.Background()
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
series := createTestSeries(t, seriesStore, ctx, "测试系列")
allocation := &model.ShopSeriesAllocation{
ShopID: childShop.ID,
SeriesID: series.ID,
AllocatorShopID: parentShop.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
allocation.Creator = 1
err := allocationStore.Create(ctx, allocation)
require.NoError(t, err)
t.Run("禁用分配", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
err := svc.UpdateStatus(ctxWithUser, allocation.ID, constants.StatusDisabled)
require.NoError(t, err)
updated, err := allocationStore.GetByID(ctx, allocation.ID)
require.NoError(t, err)
assert.Equal(t, constants.StatusDisabled, updated.Status)
})
t.Run("启用分配", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
err := svc.UpdateStatus(ctxWithUser, allocation.ID, constants.StatusEnabled)
require.NoError(t, err)
updated, err := allocationStore.GetByID(ctx, allocation.ID)
require.NoError(t, err)
assert.Equal(t, constants.StatusEnabled, updated.Status)
})
t.Run("更新不存在的分配状态", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
err := svc.UpdateStatus(ctxWithUser, 99999, constants.StatusDisabled)
require.Error(t, err)
appErr := err.(*errors.AppError)
assert.Equal(t, errors.CodeNotFound, appErr.Code)
})
}
func TestService_List(t *testing.T) {
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
ctx := context.Background()
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
childShop1 := createTestShop(t, shopStore, ctx, "二级代理1", &parentShop.ID)
childShop2 := createTestShop(t, shopStore, ctx, "二级代理2", &parentShop.ID)
series1 := createTestSeries(t, seriesStore, ctx, "测试系列1")
series2 := createTestSeries(t, seriesStore, ctx, "测试系列2")
allocation1 := &model.ShopSeriesAllocation{
ShopID: childShop1.ID,
SeriesID: series1.ID,
AllocatorShopID: parentShop.ID,
PricingMode: model.PricingModeFixed,
PricingValue: 500,
Status: constants.StatusEnabled,
}
allocation1.Creator = 1
err := allocationStore.Create(ctx, allocation1)
require.NoError(t, err)
allocation2 := &model.ShopSeriesAllocation{
ShopID: childShop2.ID,
SeriesID: series2.ID,
AllocatorShopID: parentShop.ID,
PricingMode: model.PricingModePercent,
PricingValue: 100,
Status: constants.StatusEnabled,
}
allocation2.Creator = 1
err = allocationStore.Create(ctx, allocation2)
require.NoError(t, err)
t.Run("查询所有分配", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
req := &dto.ShopSeriesAllocationListRequest{
Page: 1,
PageSize: 20,
}
resp, total, err := svc.List(ctxWithUser, req)
require.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(2))
assert.GreaterOrEqual(t, len(resp), 2)
})
t.Run("按店铺ID过滤", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
req := &dto.ShopSeriesAllocationListRequest{
Page: 1,
PageSize: 20,
ShopID: &childShop1.ID,
}
resp, total, err := svc.List(ctxWithUser, req)
require.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
for _, a := range resp {
assert.Equal(t, childShop1.ID, a.ShopID)
}
})
t.Run("按系列ID过滤", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
req := &dto.ShopSeriesAllocationListRequest{
Page: 1,
PageSize: 20,
SeriesID: &series1.ID,
}
resp, total, err := svc.List(ctxWithUser, req)
require.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(1))
for _, a := range resp {
assert.Equal(t, series1.ID, a.SeriesID)
}
})
t.Run("按状态过滤", func(t *testing.T) {
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
status := constants.StatusEnabled
req := &dto.ShopSeriesAllocationListRequest{
Page: 1,
PageSize: 20,
Status: &status,
}
resp, total, err := svc.List(ctxWithUser, req)
require.NoError(t, err)
assert.GreaterOrEqual(t, total, int64(2))
for _, a := range resp {
assert.Equal(t, constants.StatusEnabled, a.Status)
}
})
}