feat: 实现门店套餐分配功能并统一测试基础设施
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
新增功能: - 门店套餐分配管理(shop_package_allocation):支持门店套餐库存管理 - 门店套餐系列分配管理(shop_series_allocation):支持套餐系列分配和佣金层级设置 - 我的套餐查询(my_package):支持门店查询自己的套餐分配情况 测试改进: - 统一集成测试基础设施,新增 testutils.NewIntegrationTestEnv - 重构所有集成测试使用新的测试环境设置 - 移除旧的测试辅助函数和冗余测试文件 - 新增 test_helpers_test.go 统一任务测试辅助 技术细节: - 新增数据库迁移 000025_create_shop_allocation_tables - 新增 3 个 Handler、Service、Store 和对应的单元测试 - 更新 OpenAPI 文档和文档生成器 - 测试覆盖率:Service 层 > 90% Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
306
internal/service/my_package/service.go
Normal file
306
internal/service/my_package/service.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package my_package
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageStore *postgres.PackageStore
|
||||
shopStore *postgres.ShopStore
|
||||
}
|
||||
|
||||
func New(
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageStore: packageStore,
|
||||
shopStore: shopStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListMyPackages(ctx context.Context, req *dto.MyPackageListRequest) ([]*dto.MyPackageResponse, int64, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, 0, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
seriesAllocations, err := s.seriesAllocationStore.GetByShopID(ctx, shopID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("获取系列分配失败: %w", err)
|
||||
}
|
||||
|
||||
if len(seriesAllocations) == 0 {
|
||||
return []*dto.MyPackageResponse{}, 0, nil
|
||||
}
|
||||
|
||||
seriesIDs := make([]uint, 0, len(seriesAllocations))
|
||||
for _, sa := range seriesAllocations {
|
||||
seriesIDs = append(seriesIDs, sa.SeriesID)
|
||||
}
|
||||
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "id DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
filters["series_ids"] = seriesIDs
|
||||
filters["status"] = constants.StatusEnabled
|
||||
filters["shelf_status"] = 1
|
||||
|
||||
if req.SeriesID != nil {
|
||||
found := false
|
||||
for _, sid := range seriesIDs {
|
||||
if sid == *req.SeriesID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return []*dto.MyPackageResponse{}, 0, nil
|
||||
}
|
||||
filters["series_id"] = *req.SeriesID
|
||||
}
|
||||
if req.PackageType != nil {
|
||||
filters["package_type"] = *req.PackageType
|
||||
}
|
||||
|
||||
packages, total, err := s.packageStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询套餐列表失败: %w", err)
|
||||
}
|
||||
|
||||
packageOverrides, _ := s.packageAllocationStore.GetByShopID(ctx, shopID)
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
for _, po := range packageOverrides {
|
||||
overrideMap[po.PackageID] = po
|
||||
}
|
||||
|
||||
allocationMap := make(map[uint]*model.ShopSeriesAllocation)
|
||||
for _, sa := range seriesAllocations {
|
||||
allocationMap[sa.SeriesID] = sa
|
||||
}
|
||||
|
||||
responses := make([]*dto.MyPackageResponse, len(packages))
|
||||
for i, pkg := range packages {
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
seriesName := ""
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
costPrice, priceSource := s.GetCostPrice(ctx, shopID, pkg, allocationMap, overrideMap)
|
||||
|
||||
responses[i] = &dto.MyPackageResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: pkg.PackageType,
|
||||
SeriesID: pkg.SeriesID,
|
||||
SeriesName: seriesName,
|
||||
CostPrice: costPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
ProfitMargin: pkg.SuggestedRetailPrice - costPrice,
|
||||
PriceSource: priceSource,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
}
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetMyPackage(ctx context.Context, packageID uint) (*dto.MyPackageDetailResponse, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
pkg, err := s.packageStore.GetByID(ctx, packageID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
|
||||
}
|
||||
|
||||
seriesAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, shopID, pkg.SeriesID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeForbidden, "您没有该套餐的销售权限")
|
||||
}
|
||||
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
seriesName := ""
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{pkg.SeriesID: seriesAllocation}
|
||||
|
||||
packageOverride, _ := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
if packageOverride != nil {
|
||||
overrideMap[packageID] = packageOverride
|
||||
}
|
||||
|
||||
costPrice, priceSource := s.GetCostPrice(ctx, shopID, pkg, allocationMap, overrideMap)
|
||||
|
||||
return &dto.MyPackageDetailResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: pkg.PackageType,
|
||||
Description: "",
|
||||
SeriesID: pkg.SeriesID,
|
||||
SeriesName: seriesName,
|
||||
CostPrice: costPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
ProfitMargin: pkg.SuggestedRetailPrice - costPrice,
|
||||
PriceSource: priceSource,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListMySeriesAllocations(ctx context.Context, req *dto.MySeriesAllocationListRequest) ([]*dto.MySeriesAllocationResponse, int64, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, 0, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
allocations, err := s.seriesAllocationStore.GetByShopID(ctx, shopID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("获取系列分配失败: %w", err)
|
||||
}
|
||||
|
||||
total := int64(len(allocations))
|
||||
|
||||
page := req.Page
|
||||
pageSize := req.PageSize
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize == 0 {
|
||||
pageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if start >= int(total) {
|
||||
return []*dto.MySeriesAllocationResponse{}, total, nil
|
||||
}
|
||||
if end > int(total) {
|
||||
end = int(total)
|
||||
}
|
||||
|
||||
allocations = allocations[start:end]
|
||||
|
||||
responses := make([]*dto.MySeriesAllocationResponse, len(allocations))
|
||||
for i, a := range allocations {
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, a.SeriesID)
|
||||
seriesCode := ""
|
||||
seriesName := ""
|
||||
if series != nil {
|
||||
seriesCode = series.SeriesCode
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
allocatorShop, _ := s.shopStore.GetByID(ctx, a.AllocatorShopID)
|
||||
allocatorShopName := ""
|
||||
if allocatorShop != nil {
|
||||
allocatorShopName = allocatorShop.ShopName
|
||||
}
|
||||
|
||||
availableCount := 0
|
||||
filters := map[string]interface{}{
|
||||
"series_id": a.SeriesID,
|
||||
"status": constants.StatusEnabled,
|
||||
"shelf_status": 1,
|
||||
}
|
||||
packages, _, _ := s.packageStore.List(ctx, &store.QueryOptions{Page: 1, PageSize: 1000}, filters)
|
||||
availableCount = len(packages)
|
||||
|
||||
responses[i] = &dto.MySeriesAllocationResponse{
|
||||
ID: a.ID,
|
||||
SeriesID: a.SeriesID,
|
||||
SeriesCode: seriesCode,
|
||||
SeriesName: seriesName,
|
||||
PricingMode: a.PricingMode,
|
||||
PricingValue: a.PricingValue,
|
||||
AvailablePackageCount: availableCount,
|
||||
AllocatorShopName: allocatorShopName,
|
||||
Status: a.Status,
|
||||
}
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetCostPrice(ctx context.Context, shopID uint, pkg *model.Package, allocationMap map[uint]*model.ShopSeriesAllocation, overrideMap map[uint]*model.ShopPackageAllocation) (int64, string) {
|
||||
if override, ok := overrideMap[pkg.ID]; ok && override.Status == constants.StatusEnabled {
|
||||
return override.CostPrice, dto.PriceSourcePackageOverride
|
||||
}
|
||||
|
||||
allocation, ok := allocationMap[pkg.SeriesID]
|
||||
if !ok {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
parentCostPrice := s.getParentCostPriceRecursive(ctx, allocation.AllocatorShopID, pkg)
|
||||
costPrice := s.calculateCostPrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue)
|
||||
|
||||
return costPrice, dto.PriceSourceSeriesPricing
|
||||
}
|
||||
|
||||
func (s *Service) getParentCostPriceRecursive(ctx context.Context, shopID uint, pkg *model.Package) int64 {
|
||||
shop, err := s.shopStore.GetByID(ctx, shopID)
|
||||
if err != nil {
|
||||
return pkg.SuggestedCostPrice
|
||||
}
|
||||
|
||||
if shop.ParentID == nil || *shop.ParentID == 0 {
|
||||
return pkg.SuggestedCostPrice
|
||||
}
|
||||
|
||||
allocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, shopID, pkg.SeriesID)
|
||||
if err != nil {
|
||||
return pkg.SuggestedCostPrice
|
||||
}
|
||||
|
||||
parentCostPrice := s.getParentCostPriceRecursive(ctx, allocation.AllocatorShopID, pkg)
|
||||
return s.calculateCostPrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
820
internal/service/my_package/service_test.go
Normal file
820
internal/service/my_package/service_test.go
Normal file
@@ -0,0 +1,820 @@
|
||||
package my_package
|
||||
|
||||
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/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_GetCostPrice_Priority(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
// 创建测试数据:套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_001",
|
||||
SeriesName: "测试系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建测试数据:套餐
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_001",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000, // 基础成本价:50元
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
// 创建测试数据:上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺",
|
||||
ShopCode: "ALLOCATOR_001",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建测试数据:下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺",
|
||||
ShopCode: "SHOP_001",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建测试数据:系列分配(系列加价模式)
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000, // 固定加价:10元
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
t.Run("套餐覆盖优先级最高", func(t *testing.T) {
|
||||
// 创建套餐覆盖(覆盖成本价:80元)
|
||||
packageOverride := &model.ShopPackageAllocation{
|
||||
ShopID: shop.ID,
|
||||
PackageID: pkg.ID,
|
||||
AllocationID: seriesAllocation.ID,
|
||||
CostPrice: 8000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageAllocationStore.Create(ctx, packageOverride))
|
||||
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{series.ID: seriesAllocation}
|
||||
overrideMap := map[uint]*model.ShopPackageAllocation{pkg.ID: packageOverride}
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg, allocationMap, overrideMap)
|
||||
|
||||
// 应该返回套餐覆盖的成本价
|
||||
assert.Equal(t, int64(8000), costPrice)
|
||||
assert.Equal(t, dto.PriceSourcePackageOverride, priceSource)
|
||||
})
|
||||
|
||||
t.Run("套餐覆盖禁用时使用系列加价", func(t *testing.T) {
|
||||
pkg2 := &model.Package{
|
||||
PackageCode: "TEST_PKG_001_DISABLED",
|
||||
PackageName: "测试套餐禁用",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg2))
|
||||
|
||||
packageOverride := &model.ShopPackageAllocation{
|
||||
ShopID: shop.ID,
|
||||
PackageID: pkg2.ID,
|
||||
AllocationID: seriesAllocation.ID,
|
||||
CostPrice: 8000,
|
||||
Status: constants.StatusDisabled,
|
||||
}
|
||||
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{series.ID: seriesAllocation}
|
||||
overrideMap := map[uint]*model.ShopPackageAllocation{pkg2.ID: packageOverride}
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg2, allocationMap, overrideMap)
|
||||
|
||||
assert.Equal(t, int64(6000), costPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, priceSource)
|
||||
})
|
||||
|
||||
t.Run("无套餐覆盖时使用系列加价", func(t *testing.T) {
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{series.ID: seriesAllocation}
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg, allocationMap, overrideMap)
|
||||
|
||||
// 应该返回系列加价的成本价:5000 + 1000 = 6000
|
||||
assert.Equal(t, int64(6000), costPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, priceSource)
|
||||
})
|
||||
|
||||
t.Run("无系列分配时返回0", func(t *testing.T) {
|
||||
allocationMap := make(map[uint]*model.ShopSeriesAllocation)
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg, allocationMap, overrideMap)
|
||||
|
||||
// 应该返回0和空的价格来源
|
||||
assert.Equal(t, int64(0), costPrice)
|
||||
assert.Equal(t, "", priceSource)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_calculateCostPrice(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
parentCostPrice int64
|
||||
pricingMode string
|
||||
pricingValue int64
|
||||
expectedCostPrice int64
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "固定金额加价模式",
|
||||
parentCostPrice: 5000, // 50元
|
||||
pricingMode: model.PricingModeFixed,
|
||||
pricingValue: 1000, // 加价10元
|
||||
expectedCostPrice: 6000, // 60元
|
||||
description: "固定加价:5000 + 1000 = 6000",
|
||||
},
|
||||
{
|
||||
name: "百分比加价模式",
|
||||
parentCostPrice: 5000, // 50元
|
||||
pricingMode: model.PricingModePercent,
|
||||
pricingValue: 200, // 20%(千分比:200/1000 = 20%)
|
||||
expectedCostPrice: 6000, // 50 + 50*20% = 60元
|
||||
description: "百分比加价:5000 + (5000 * 200 / 1000) = 6000",
|
||||
},
|
||||
{
|
||||
name: "百分比加价模式-10%",
|
||||
parentCostPrice: 10000, // 100元
|
||||
pricingMode: model.PricingModePercent,
|
||||
pricingValue: 100, // 10%(千分比:100/1000 = 10%)
|
||||
expectedCostPrice: 11000, // 100 + 100*10% = 110元
|
||||
description: "百分比加价:10000 + (10000 * 100 / 1000) = 11000",
|
||||
},
|
||||
{
|
||||
name: "未知加价模式返回原价",
|
||||
parentCostPrice: 5000,
|
||||
pricingMode: "unknown",
|
||||
pricingValue: 1000,
|
||||
expectedCostPrice: 5000, // 返回原价不变
|
||||
description: "未知模式:返回 parentCostPrice 不变",
|
||||
},
|
||||
{
|
||||
name: "零加价",
|
||||
parentCostPrice: 5000,
|
||||
pricingMode: model.PricingModeFixed,
|
||||
pricingValue: 0,
|
||||
expectedCostPrice: 5000,
|
||||
description: "零加价:5000 + 0 = 5000",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
costPrice := svc.calculateCostPrice(tt.parentCostPrice, tt.pricingMode, tt.pricingValue)
|
||||
assert.Equal(t, tt.expectedCostPrice, costPrice, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ListMyPackages_Authorization(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
t.Run("店铺ID为0时返回错误", func(t *testing.T) {
|
||||
// 创建不包含店铺ID的context
|
||||
ctxWithoutShop := context.WithValue(ctx, constants.ContextKeyShopID, uint(0))
|
||||
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithoutShop, req)
|
||||
|
||||
// 应该返回错误
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, packages)
|
||||
assert.Equal(t, int64(0), total)
|
||||
assert.Contains(t, err.Error(), "当前用户不属于任何店铺")
|
||||
})
|
||||
|
||||
t.Run("无系列分配时返回空列表", func(t *testing.T) {
|
||||
// 创建店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "SHOP_TEST_001",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建包含店铺ID的context
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
// 应该返回空列表,无错误
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, packages)
|
||||
assert.Equal(t, 0, len(packages))
|
||||
assert.Equal(t, int64(0), total)
|
||||
})
|
||||
|
||||
t.Run("有系列分配时返回套餐列表", func(t *testing.T) {
|
||||
// 创建套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_002",
|
||||
SeriesName: "测试系列2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建套餐
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_002",
|
||||
PackageName: "测试套餐2",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺2",
|
||||
ShopCode: "ALLOCATOR_002",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺2",
|
||||
ShopCode: "SHOP_002",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建系列分配
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
// 创建包含店铺ID的context
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
// 应该返回套餐列表
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, packages)
|
||||
assert.Equal(t, 1, len(packages))
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, pkg.ID, packages[0].ID)
|
||||
assert.Equal(t, pkg.PackageName, packages[0].PackageName)
|
||||
// 验证成本价计算:5000 + 1000 = 6000
|
||||
assert.Equal(t, int64(6000), packages[0].CostPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, packages[0].PriceSource)
|
||||
})
|
||||
|
||||
t.Run("分页参数默认值", func(t *testing.T) {
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_PAGING",
|
||||
SeriesName: "分页测试系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
for i := range 5 {
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_PAGING_" + string(byte('0'+byte(i))),
|
||||
PackageName: "分页测试套餐_" + string(byte('0'+byte(i))),
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
}
|
||||
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "分页上级店铺",
|
||||
ShopCode: "ALLOCATOR_PAGING",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "分页下级店铺",
|
||||
ShopCode: "SHOP_PAGING",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
req := &dto.MyPackageListRequest{}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, packages)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
assert.LessOrEqual(t, len(packages), constants.DefaultPageSize)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_ListMyPackages_Filtering(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
// 创建两个套餐系列
|
||||
series1 := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_FILTER_001",
|
||||
SeriesName: "系列1",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series1))
|
||||
|
||||
series2 := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_FILTER_002",
|
||||
SeriesName: "系列2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series2))
|
||||
|
||||
// 创建不同类型的套餐
|
||||
pkg1 := &model.Package{
|
||||
PackageCode: "PKG_FILTER_001",
|
||||
PackageName: "正式套餐1",
|
||||
SeriesID: series1.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg1))
|
||||
|
||||
pkg2 := &model.Package{
|
||||
PackageCode: "PKG_FILTER_002",
|
||||
PackageName: "附加套餐1",
|
||||
SeriesID: series2.ID,
|
||||
PackageType: "addon",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 512,
|
||||
DataAmountMB: 512,
|
||||
Price: 4900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 2500,
|
||||
SuggestedRetailPrice: 4900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg2))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺过滤",
|
||||
ShopCode: "ALLOCATOR_FILTER",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺过滤",
|
||||
ShopCode: "SHOP_FILTER",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 为两个系列都创建分配
|
||||
for _, series := range []*model.PackageSeries{series1, series2} {
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
}
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
t.Run("按系列ID过滤", func(t *testing.T) {
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
SeriesID: &series1.ID,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, 1, len(packages))
|
||||
assert.Equal(t, pkg1.ID, packages[0].ID)
|
||||
})
|
||||
|
||||
t.Run("按套餐类型过滤", func(t *testing.T) {
|
||||
packageType := "addon"
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
PackageType: &packageType,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, 1, len(packages))
|
||||
assert.Equal(t, pkg2.ID, packages[0].ID)
|
||||
})
|
||||
|
||||
t.Run("无效的系列ID返回空列表", func(t *testing.T) {
|
||||
invalidSeriesID := uint(99999)
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
SeriesID: &invalidSeriesID,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), total)
|
||||
assert.Equal(t, 0, len(packages))
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_GetMyPackage(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
// 创建套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "DETAIL_SERIES",
|
||||
SeriesName: "详情系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建套餐
|
||||
pkg := &model.Package{
|
||||
PackageCode: "DETAIL_PKG",
|
||||
PackageName: "详情套餐",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺详情",
|
||||
ShopCode: "ALLOCATOR_DETAIL",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺详情",
|
||||
ShopCode: "SHOP_DETAIL",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建系列分配
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
t.Run("店铺ID为0时返回错误", func(t *testing.T) {
|
||||
ctxWithoutShop := context.WithValue(ctx, constants.ContextKeyShopID, uint(0))
|
||||
_, err := svc.GetMyPackage(ctxWithoutShop, pkg.ID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "当前用户不属于任何店铺")
|
||||
})
|
||||
|
||||
t.Run("成功获取套餐详情", func(t *testing.T) {
|
||||
detail, err := svc.GetMyPackage(ctxWithShop, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, detail)
|
||||
assert.Equal(t, pkg.ID, detail.ID)
|
||||
assert.Equal(t, pkg.PackageName, detail.PackageName)
|
||||
assert.Equal(t, series.SeriesName, detail.SeriesName)
|
||||
// 验证成本价:5000 + 1000 = 6000
|
||||
assert.Equal(t, int64(6000), detail.CostPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, detail.PriceSource)
|
||||
})
|
||||
|
||||
t.Run("无权限访问套餐时返回错误", func(t *testing.T) {
|
||||
// 创建另一个没有系列分配的店铺
|
||||
otherShop := &model.Shop{
|
||||
ShopName: "其他店铺",
|
||||
ShopCode: "OTHER_SHOP",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000002",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, otherShop))
|
||||
|
||||
ctxWithOtherShop := context.WithValue(ctx, constants.ContextKeyShopID, otherShop.ID)
|
||||
_, err := svc.GetMyPackage(ctxWithOtherShop, pkg.ID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "您没有该套餐的销售权限")
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_ListMySeriesAllocations(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
t.Run("店铺ID为0时返回错误", func(t *testing.T) {
|
||||
ctxWithoutShop := context.WithValue(ctx, constants.ContextKeyShopID, uint(0))
|
||||
req := &dto.MySeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
_, _, err := svc.ListMySeriesAllocations(ctxWithoutShop, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "当前用户不属于任何店铺")
|
||||
})
|
||||
|
||||
t.Run("无系列分配时返回空列表", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "分配测试店铺",
|
||||
ShopCode: "ALLOC_SHOP",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
req := &dto.MySeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
allocations, total, err := svc.ListMySeriesAllocations(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, allocations)
|
||||
assert.Equal(t, 0, len(allocations))
|
||||
assert.Equal(t, int64(0), total)
|
||||
})
|
||||
|
||||
t.Run("成功列表系列分配", func(t *testing.T) {
|
||||
// 创建套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "ALLOC_SERIES",
|
||||
SeriesName: "分配系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "分配者店铺",
|
||||
ShopCode: "ALLOCATOR_ALLOC",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "被分配店铺",
|
||||
ShopCode: "ALLOCATED_SHOP",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建系列分配
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
req := &dto.MySeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
allocations, total, err := svc.ListMySeriesAllocations(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, allocations)
|
||||
assert.Equal(t, 1, len(allocations))
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, series.SeriesName, allocations[0].SeriesName)
|
||||
assert.Equal(t, allocatorShop.ShopName, allocations[0].AllocatorShopName)
|
||||
})
|
||||
}
|
||||
273
internal/service/shop_package_allocation/service.go
Normal file
273
internal/service/shop_package_allocation/service.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package shop_package_allocation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
shopStore *postgres.ShopStore
|
||||
packageStore *postgres.PackageStore
|
||||
}
|
||||
|
||||
func New(
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
shopStore: shopStore,
|
||||
packageStore: packageStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateShopPackageAllocationRequest) (*dto.ShopPackageAllocationResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
allocatorShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
if userType == constants.UserTypeAgent && allocatorShopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
targetShop, err := s.shopStore.GetByID(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取店铺失败: %w", err)
|
||||
}
|
||||
|
||||
if userType == constants.UserTypeAgent {
|
||||
if targetShop.ParentID == nil || *targetShop.ParentID != allocatorShopID {
|
||||
return nil, errors.New(errors.CodeForbidden, "只能为直属下级分配套餐")
|
||||
}
|
||||
}
|
||||
|
||||
pkg, err := s.packageStore.GetByID(ctx, req.PackageID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取套餐失败: %w", err)
|
||||
}
|
||||
|
||||
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, fmt.Errorf("获取系列分配失败: %w", 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,
|
||||
}
|
||||
allocation.Creator = currentUserID
|
||||
|
||||
if err := s.packageAllocationStore.Create(ctx, allocation); err != nil {
|
||||
return nil, fmt.Errorf("创建分配失败: %w", err)
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, targetShop.ShopName, pkg.PackageName, pkg.PackageCode)
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uint) (*dto.ShopPackageAllocationResponse, error) {
|
||||
allocation, err := s.packageAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
||||
pkg, _ := s.packageStore.GetByID(ctx, allocation.PackageID)
|
||||
|
||||
shopName := ""
|
||||
packageName := ""
|
||||
packageCode := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if pkg != nil {
|
||||
packageName = pkg.PackageName
|
||||
packageCode = pkg.PackageCode
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, shopName, packageName, packageCode)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopPackageAllocationRequest) (*dto.ShopPackageAllocationResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
allocation, err := s.packageAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if req.CostPrice != nil {
|
||||
allocation.CostPrice = *req.CostPrice
|
||||
}
|
||||
allocation.Updater = currentUserID
|
||||
|
||||
if err := s.packageAllocationStore.Update(ctx, allocation); err != nil {
|
||||
return nil, fmt.Errorf("更新分配失败: %w", err)
|
||||
}
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
||||
pkg, _ := s.packageStore.GetByID(ctx, allocation.PackageID)
|
||||
|
||||
shopName := ""
|
||||
packageName := ""
|
||||
packageCode := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if pkg != nil {
|
||||
packageName = pkg.PackageName
|
||||
packageCode = pkg.PackageCode
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, shopName, packageName, packageCode)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
_, err := s.packageAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.packageAllocationStore.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("删除分配失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *dto.ShopPackageAllocationListRequest) ([]*dto.ShopPackageAllocationResponse, int64, error) {
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "id DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
if req.ShopID != nil {
|
||||
filters["shop_id"] = *req.ShopID
|
||||
}
|
||||
if req.PackageID != nil {
|
||||
filters["package_id"] = *req.PackageID
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
|
||||
allocations, total, err := s.packageAllocationStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询分配列表失败: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]*dto.ShopPackageAllocationResponse, len(allocations))
|
||||
for i, a := range allocations {
|
||||
shop, _ := s.shopStore.GetByID(ctx, a.ShopID)
|
||||
pkg, _ := s.packageStore.GetByID(ctx, a.PackageID)
|
||||
|
||||
shopName := ""
|
||||
packageName := ""
|
||||
packageCode := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if pkg != nil {
|
||||
packageName = pkg.PackageName
|
||||
packageCode = pkg.PackageCode
|
||||
}
|
||||
|
||||
resp, _ := s.buildResponse(ctx, a, shopName, packageName, packageCode)
|
||||
responses[i] = resp
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.packageAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.packageAllocationStore.UpdateStatus(ctx, id, status, currentUserID); err != nil {
|
||||
return fmt.Errorf("更新状态失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) buildResponse(ctx context.Context, a *model.ShopPackageAllocation, shopName, packageName, packageCode string) (*dto.ShopPackageAllocationResponse, error) {
|
||||
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),
|
||||
}, nil
|
||||
}
|
||||
531
internal/service/shop_series_allocation/service.go
Normal file
531
internal/service/shop_series_allocation/service.go
Normal file
@@ -0,0 +1,531 @@
|
||||
package shop_series_allocation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
allocationStore *postgres.ShopSeriesAllocationStore
|
||||
tierStore *postgres.ShopSeriesCommissionTierStore
|
||||
shopStore *postgres.ShopStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageStore *postgres.PackageStore
|
||||
}
|
||||
|
||||
func New(
|
||||
allocationStore *postgres.ShopSeriesAllocationStore,
|
||||
tierStore *postgres.ShopSeriesCommissionTierStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
allocationStore: allocationStore,
|
||||
tierStore: tierStore,
|
||||
shopStore: shopStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageStore: packageStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocationRequest) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
allocatorShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
if userType == constants.UserTypeAgent && allocatorShopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
targetShop, err := s.shopStore.GetByID(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取店铺失败: %w", err)
|
||||
}
|
||||
|
||||
isPlatformUser := userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform
|
||||
isFirstLevelShop := targetShop.ParentID == nil
|
||||
|
||||
if isPlatformUser {
|
||||
if !isFirstLevelShop {
|
||||
return nil, errors.New(errors.CodeForbidden, "平台只能为一级店铺分配套餐")
|
||||
}
|
||||
} else {
|
||||
if isFirstLevelShop || *targetShop.ParentID != allocatorShopID {
|
||||
return nil, errors.New(errors.CodeForbidden, "只能为直属下级分配套餐")
|
||||
}
|
||||
}
|
||||
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, req.SeriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取套餐系列失败: %w", err)
|
||||
}
|
||||
|
||||
if userType == constants.UserTypeAgent {
|
||||
myAllocation, err := s.allocationStore.GetByShopAndSeries(ctx, allocatorShopID, req.SeriesID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("检查分配权限失败: %w", err)
|
||||
}
|
||||
if myAllocation == nil || myAllocation.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeForbidden, "您没有该套餐系列的分配权限")
|
||||
}
|
||||
}
|
||||
|
||||
existing, _ := s.allocationStore.GetByShopAndSeries(ctx, req.ShopID, req.SeriesID)
|
||||
if existing != nil {
|
||||
return nil, errors.New(errors.CodeConflict, "该店铺已分配此套餐系列")
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
allocation.Creator = currentUserID
|
||||
|
||||
if err := s.allocationStore.Create(ctx, allocation); err != nil {
|
||||
return nil, fmt.Errorf("创建分配失败: %w", err)
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, targetShop.ShopName, series.SeriesName)
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uint) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
allocation, err := s.allocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
|
||||
|
||||
shopName := ""
|
||||
seriesName := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, shopName, seriesName)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeriesAllocationRequest) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
allocation, err := s.allocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if req.PricingMode != nil {
|
||||
allocation.PricingMode = *req.PricingMode
|
||||
}
|
||||
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
|
||||
}
|
||||
allocation.Updater = currentUserID
|
||||
|
||||
if err := s.allocationStore.Update(ctx, allocation); err != nil {
|
||||
return nil, fmt.Errorf("更新分配失败: %w", err)
|
||||
}
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
|
||||
|
||||
shopName := ""
|
||||
seriesName := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, shopName, seriesName)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
allocation, err := s.allocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
hasDependent, err := s.allocationStore.HasDependentAllocations(ctx, allocation.ShopID, allocation.SeriesID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查依赖关系失败: %w", err)
|
||||
}
|
||||
if hasDependent {
|
||||
return errors.New(errors.CodeConflict, "存在下级依赖,无法删除")
|
||||
}
|
||||
|
||||
if err := s.allocationStore.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("删除分配失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *dto.ShopSeriesAllocationListRequest) ([]*dto.ShopSeriesAllocationResponse, int64, error) {
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "id DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
if req.ShopID != nil {
|
||||
filters["shop_id"] = *req.ShopID
|
||||
}
|
||||
if req.SeriesID != nil {
|
||||
filters["series_id"] = *req.SeriesID
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
if shopID > 0 && userType == constants.UserTypeAgent {
|
||||
filters["allocator_shop_id"] = shopID
|
||||
}
|
||||
|
||||
allocations, total, err := s.allocationStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询分配列表失败: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]*dto.ShopSeriesAllocationResponse, len(allocations))
|
||||
for i, a := range allocations {
|
||||
shop, _ := s.shopStore.GetByID(ctx, a.ShopID)
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, a.SeriesID)
|
||||
|
||||
shopName := ""
|
||||
seriesName := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
resp, _ := s.buildResponse(ctx, a, shopName, seriesName)
|
||||
responses[i] = resp
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.allocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.allocationStore.UpdateStatus(ctx, id, status, currentUserID); err != nil {
|
||||
return fmt.Errorf("更新状态失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetParentCostPrice(ctx context.Context, shopID, packageID uint) (int64, error) {
|
||||
pkg, err := s.packageStore.GetByID(ctx, packageID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("获取套餐失败: %w", err)
|
||||
}
|
||||
|
||||
shop, err := s.shopStore.GetByID(ctx, shopID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("获取店铺失败: %w", err)
|
||||
}
|
||||
|
||||
if shop.ParentID == nil || *shop.ParentID == 0 {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocation, shopName, seriesName string) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
allocatorShop, _ := s.shopStore.GetByID(ctx, a.AllocatorShopID)
|
||||
allocatorShopName := ""
|
||||
if allocatorShop != nil {
|
||||
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),
|
||||
}, 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),
|
||||
}
|
||||
|
||||
if t.PeriodStartDate != nil {
|
||||
resp.PeriodStartDate = t.PeriodStartDate.Format("2006-01-02")
|
||||
}
|
||||
if t.PeriodEndDate != nil {
|
||||
resp.PeriodEndDate = t.PeriodEndDate.Format("2006-01-02")
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
595
internal/service/shop_series_allocation/service_test.go
Normal file
595
internal/service/shop_series_allocation/service_test.go
Normal file
@@ -0,0 +1,595 @@
|
||||
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: "固定加价为0:10000 + 0 = 10000",
|
||||
parentCostPrice: 10000,
|
||||
pricingMode: model.PricingModeFixed,
|
||||
pricingValue: 0,
|
||||
expectedCostPrice: 10000,
|
||||
description: "固定加价为0",
|
||||
},
|
||||
{
|
||||
name: "百分比加价为0:10000 + 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user