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>
307 lines
9.2 KiB
Go
307 lines
9.2 KiB
Go
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
|
|
}
|
|
}
|