feat: 新增系列授权 Service,支持固定/梯度佣金模式和代理自设强充

实现 /shop-series-grants 全套业务逻辑:
- 创建授权(固定/梯度模式):原子性创建 ShopSeriesAllocation + ShopPackageAllocation;校验分配者天花板和阶梯阈值匹配;平台创建无天花板限制
- 强充层级:首次充值类型由平台锁定;累计充值类型平台已设时代理配置被忽略,平台未设时代理可自设
- 查询(列表/详情):聚合套餐列表,梯度模式从 PackageSeries 读取 operator 合并响应
- 更新佣金和强充配置;套餐增删改(事务保证)
- 删除:有下级依赖时禁止删除

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-04 11:36:09 +08:00
parent beed9d25e0
commit ad3a7a770a

View File

@@ -0,0 +1,722 @@
// Package shop_series_grant 提供代理系列授权的业务逻辑服务
// 包含授权创建、查询、更新、套餐管理、删除等功能
package shop_series_grant
import (
"context"
"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"
"go.uber.org/zap"
"gorm.io/gorm"
)
// Service 代理系列授权业务服务
type Service struct {
db *gorm.DB
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
shopPackageAllocationPriceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore
shopStore *postgres.ShopStore
packageStore *postgres.PackageStore
packageSeriesStore *postgres.PackageSeriesStore
logger *zap.Logger
}
// New 创建代理系列授权服务实例
func New(
db *gorm.DB,
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
shopPackageAllocationPriceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore,
shopStore *postgres.ShopStore,
packageStore *postgres.PackageStore,
packageSeriesStore *postgres.PackageSeriesStore,
logger *zap.Logger,
) *Service {
return &Service{
db: db,
shopSeriesAllocationStore: shopSeriesAllocationStore,
shopPackageAllocationStore: shopPackageAllocationStore,
shopPackageAllocationPriceHistoryStore: shopPackageAllocationPriceHistoryStore,
shopStore: shopStore,
packageStore: packageStore,
packageSeriesStore: packageSeriesStore,
logger: logger,
}
}
// getParentCeilingFixed 查询固定模式佣金天花板
// allocatorShopID=0 表示平台分配,天花板为 PackageSeries.commission_amount
// allocatorShopID>0 表示代理分配,天花板为分配者自身的 ShopSeriesAllocation.one_time_commission_amount
func (s *Service) getParentCeilingFixed(ctx context.Context, allocatorShopID uint, seriesID uint, config *model.OneTimeCommissionConfig) (int64, error) {
if allocatorShopID == 0 {
// 平台分配:天花板为套餐系列全局佣金金额
return config.CommissionAmount, nil
}
// 代理分配:天花板为分配者自身拥有的授权金额
allocatorAllocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, allocatorShopID, seriesID)
if err != nil {
return 0, errors.Wrap(errors.CodeNotFound, err, "分配者无此系列授权")
}
return allocatorAllocation.OneTimeCommissionAmount, nil
}
// getParentCeilingTiered 查询梯度模式各档位天花板map: threshold→amount
// allocatorShopID=0 → 读 PackageSeries 全局 Tiers 中各 threshold 的 amount
// allocatorShopID>0 → 读分配者自身 ShopSeriesAllocation.commission_tiers_json
func (s *Service) getParentCeilingTiered(ctx context.Context, allocatorShopID uint, seriesID uint, globalTiers []model.OneTimeCommissionTier) (map[int64]int64, error) {
ceilingMap := make(map[int64]int64)
if allocatorShopID == 0 {
// 平台分配:天花板来自套餐系列全局配置
for _, t := range globalTiers {
ceilingMap[t.Threshold] = t.Amount
}
return ceilingMap, nil
}
// 代理分配:天花板来自分配者的专属阶梯
allocatorAllocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, allocatorShopID, seriesID)
if err != nil {
return nil, errors.Wrap(errors.CodeNotFound, err, "分配者无此系列授权")
}
agentTiers, err := allocatorAllocation.GetCommissionTiers()
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "解析分配者阶梯佣金失败")
}
for _, t := range agentTiers {
ceilingMap[t.Threshold] = t.Amount
}
return ceilingMap, nil
}
// buildGrantResponse 构建授权详情响应
func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.ShopSeriesAllocation, series *model.PackageSeries, config *model.OneTimeCommissionConfig) (*dto.ShopSeriesGrantResponse, error) {
resp := &dto.ShopSeriesGrantResponse{
ID: allocation.ID,
ShopID: allocation.ShopID,
SeriesID: allocation.SeriesID,
SeriesName: series.SeriesName,
SeriesCode: series.SeriesCode,
CommissionType: config.CommissionType,
AllocatorShopID: allocation.AllocatorShopID,
Status: allocation.Status,
CreatedAt: allocation.CreatedAt.Format(time.DateTime),
UpdatedAt: allocation.UpdatedAt.Format(time.DateTime),
}
// 查询店铺名称
shop, err := s.shopStore.GetByID(ctx, allocation.ShopID)
if err == nil {
resp.ShopName = shop.ShopName
}
// 查询分配者名称
if allocation.AllocatorShopID > 0 {
allocatorShop, err := s.shopStore.GetByID(ctx, allocation.AllocatorShopID)
if err == nil {
resp.AllocatorShopName = allocatorShop.ShopName
}
} else {
resp.AllocatorShopName = "平台"
}
// 强充状态first_recharge 或平台已启用 accumulated_recharge 强充时,锁定不可改
forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge
resp.ForceRechargeLocked = forceRechargeLocked
resp.ForceRechargeEnabled = allocation.EnableForceRecharge
resp.ForceRechargeAmount = allocation.ForceRechargeAmount
// 固定模式
if config.CommissionType == "fixed" {
resp.OneTimeCommissionAmount = allocation.OneTimeCommissionAmount
resp.CommissionTiers = []dto.GrantCommissionTierItem{}
} else {
// 梯度模式:将代理专属金额与全局 operator 合并
resp.OneTimeCommissionAmount = 0
agentTiers, err := allocation.GetCommissionTiers()
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "解析代理阶梯佣金失败")
}
// 按 threshold 索引代理金额
agentAmountMap := make(map[int64]int64)
for _, t := range agentTiers {
agentAmountMap[t.Threshold] = t.Amount
}
// 合并全局 operator 和代理 amount
tiers := make([]dto.GrantCommissionTierItem, 0, len(config.Tiers))
for _, globalTier := range config.Tiers {
tiers = append(tiers, dto.GrantCommissionTierItem{
Operator: globalTier.Operator,
Threshold: globalTier.Threshold,
Amount: agentAmountMap[globalTier.Threshold],
})
}
resp.CommissionTiers = tiers
}
// 查询已授权套餐列表
pkgAllocations, err := s.shopPackageAllocationStore.GetBySeriesAllocationID(ctx, allocation.ID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐分配失败")
}
packages := make([]dto.ShopSeriesGrantPackageItem, 0, len(pkgAllocations))
for _, pa := range pkgAllocations {
pkg, pkgErr := s.packageStore.GetByID(ctx, pa.PackageID)
if pkgErr != nil {
continue
}
packages = append(packages, dto.ShopSeriesGrantPackageItem{
PackageID: pa.PackageID,
PackageName: pkg.PackageName,
PackageCode: pkg.PackageCode,
CostPrice: pa.CostPrice,
ShelfStatus: pa.ShelfStatus,
Status: pa.Status,
})
}
resp.Packages = packages
return resp, nil
}
// Create 创建系列授权
// POST /api/admin/shop-series-grants
func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequest) (*dto.ShopSeriesGrantResponse, error) {
operatorID := middleware.GetUserIDFromContext(ctx)
operatorShopID := middleware.GetShopIDFromContext(ctx)
operatorType := middleware.GetUserTypeFromContext(ctx)
// 1. 查询套餐系列,确认佣金类型
series, err := s.packageSeriesStore.GetByID(ctx, req.SeriesID)
if err != nil {
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil || !config.Enable {
return nil, errors.New(errors.CodeInvalidParam, "该系列未启用一次性佣金,无法创建授权")
}
// 2. 检查重复授权
exists, err := s.shopSeriesAllocationStore.ExistsByShopAndSeries(ctx, req.ShopID, req.SeriesID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "检查授权重复失败")
}
if exists {
return nil, errors.New(errors.CodeConflict, "该代理已存在此系列授权")
}
// 3. 确定 allocatorShopID代理操作者必须自己有授权才能向下分配
var allocatorShopID uint
if operatorType == constants.UserTypeAgent {
allocatorShopID = operatorShopID
_, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, allocatorShopID, req.SeriesID)
if err != nil {
return nil, errors.New(errors.CodeForbidden, "当前账号无此系列授权,无法向下分配")
}
}
// 平台/超管 allocatorShopID = 0
// 4. 参数验证:固定模式 or 梯度模式
allocation := &model.ShopSeriesAllocation{
ShopID: req.ShopID,
SeriesID: req.SeriesID,
AllocatorShopID: allocatorShopID,
Status: constants.StatusEnabled,
CommissionTiersJSON: "[]",
}
allocation.Creator = operatorID
allocation.Updater = operatorID
if config.CommissionType == "fixed" {
if req.OneTimeCommissionAmount == nil {
return nil, errors.New(errors.CodeInvalidParam, "固定模式必须填写佣金金额")
}
ceiling, ceilErr := s.getParentCeilingFixed(ctx, allocatorShopID, req.SeriesID, config)
if ceilErr != nil {
return nil, ceilErr
}
if *req.OneTimeCommissionAmount > ceiling {
return nil, errors.New(errors.CodeInvalidParam, "佣金金额不能超过上级天花板")
}
allocation.OneTimeCommissionAmount = *req.OneTimeCommissionAmount
} else {
// 梯度模式
if len(req.CommissionTiers) == 0 {
return nil, errors.New(errors.CodeInvalidParam, "梯度模式必须填写阶梯配置")
}
// 阶梯数量和 threshold 必须与全局完全一致
if len(req.CommissionTiers) != len(config.Tiers) {
return nil, errors.New(errors.CodeInvalidParam, "梯度阶梯数量与系列配置不一致")
}
ceilingMap, ceilErr := s.getParentCeilingTiered(ctx, allocatorShopID, req.SeriesID, config.Tiers)
if ceilErr != nil {
return nil, ceilErr
}
agentTiers := make([]model.AllocationCommissionTier, 0, len(req.CommissionTiers))
for i, tier := range req.CommissionTiers {
if tier.Threshold != config.Tiers[i].Threshold {
return nil, errors.New(errors.CodeInvalidParam, "梯度阶梯 threshold 与系列配置不匹配")
}
ceiling, ok := ceilingMap[tier.Threshold]
if !ok {
ceiling = 0
}
if tier.Amount > ceiling {
return nil, errors.New(errors.CodeInvalidParam, "某档位佣金金额超过上级天花板")
}
agentTiers = append(agentTiers, model.AllocationCommissionTier{
Threshold: tier.Threshold,
Amount: tier.Amount,
})
}
if err := allocation.SetCommissionTiers(agentTiers); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "序列化阶梯佣金失败")
}
}
// 5. 强充配置first_recharge 或平台已开强充 → locked忽略代理传入
forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge
if !forceRechargeLocked && req.EnableForceRecharge != nil && *req.EnableForceRecharge {
allocation.EnableForceRecharge = true
if req.ForceRechargeAmount != nil {
allocation.ForceRechargeAmount = *req.ForceRechargeAmount
}
}
// 6. 事务中创建 ShopSeriesAllocation + N 条 ShopPackageAllocation
var result *dto.ShopSeriesGrantResponse
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txSeriesStore := postgres.NewShopSeriesAllocationStore(tx)
if createErr := txSeriesStore.Create(ctx, allocation); createErr != nil {
return errors.Wrap(errors.CodeDatabaseError, createErr, "创建系列授权失败")
}
// 创建套餐分配
if len(req.Packages) > 0 {
txPkgStore := postgres.NewShopPackageAllocationStore(tx)
txHistoryStore := postgres.NewShopPackageAllocationPriceHistoryStore(tx)
for _, item := range req.Packages {
if item.Remove != nil && *item.Remove {
continue
}
// W1: 校验套餐归属于该系列,防止跨系列套餐混入
pkg, pkgErr := s.packageStore.GetByID(ctx, item.PackageID)
if pkgErr != nil || pkg.SeriesID != req.SeriesID {
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
}
// W2: 代理操作时,校验分配者已拥有此套餐授权,防止越权分配
if allocatorShopID > 0 {
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocatorShopID, item.PackageID)
if authErr != nil {
return errors.New(errors.CodeForbidden, "无权限分配该套餐")
}
}
pkgAlloc := &model.ShopPackageAllocation{
ShopID: req.ShopID,
PackageID: item.PackageID,
AllocatorShopID: allocatorShopID,
CostPrice: item.CostPrice,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
ShelfStatus: constants.StatusEnabled,
}
pkgAlloc.Creator = operatorID
pkgAlloc.Updater = operatorID
if err := txPkgStore.Create(ctx, pkgAlloc); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建套餐分配失败")
}
// 写成本价历史
_ = txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
AllocationID: pkgAlloc.ID,
OldCostPrice: 0,
NewCostPrice: item.CostPrice,
ChangeReason: "初始授权",
ChangedBy: operatorID,
EffectiveFrom: time.Now(),
})
}
}
var buildErr error
result, buildErr = s.buildGrantResponse(ctx, allocation, series, config)
return buildErr
})
if err != nil {
return nil, err
}
return result, nil
}
// Get 查询单条系列授权详情
// GET /api/admin/shop-series-grants/:id
func (s *Service) Get(ctx context.Context, id uint) (*dto.ShopSeriesGrantResponse, error) {
allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "授权记录不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败")
}
series, err := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil {
config = &model.OneTimeCommissionConfig{}
}
return s.buildGrantResponse(ctx, allocation, series, config)
}
// List 分页查询系列授权列表
// GET /api/admin/shop-series-grants
func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest) (*dto.ShopSeriesGrantPageResult, error) {
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = constants.DefaultPageSize
}
if pageSize > constants.MaxPageSize {
pageSize = constants.MaxPageSize
}
opts := &store.QueryOptions{
Page: page,
PageSize: pageSize,
OrderBy: "id DESC",
}
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.AllocatorShopID != nil {
filters["allocator_shop_id"] = *req.AllocatorShopID
}
if req.Status != nil {
filters["status"] = *req.Status
}
allocations, total, err := s.shopSeriesAllocationStore.List(ctx, opts, filters)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询授权列表失败")
}
// 批量查询店铺名称
shopIDs := make([]uint, 0)
seriesIDs := make([]uint, 0)
for _, a := range allocations {
shopIDs = append(shopIDs, a.ShopID)
if a.AllocatorShopID > 0 {
shopIDs = append(shopIDs, a.AllocatorShopID)
}
seriesIDs = append(seriesIDs, a.SeriesID)
}
shopMap := make(map[uint]string)
if len(shopIDs) > 0 {
shops, _ := s.shopStore.GetByIDs(ctx, shopIDs)
for _, sh := range shops {
shopMap[sh.ID] = sh.ShopName
}
}
seriesMap := make(map[uint]*model.PackageSeries)
if len(seriesIDs) > 0 {
seriesList, _ := s.packageSeriesStore.GetByIDs(ctx, seriesIDs)
for _, sr := range seriesList {
seriesMap[sr.ID] = sr
}
}
items := make([]*dto.ShopSeriesGrantListItem, 0, len(allocations))
for _, a := range allocations {
item := &dto.ShopSeriesGrantListItem{
ID: a.ID,
ShopID: a.ShopID,
ShopName: shopMap[a.ShopID],
SeriesID: a.SeriesID,
AllocatorShopID: a.AllocatorShopID,
OneTimeCommissionAmount: a.OneTimeCommissionAmount,
ForceRechargeEnabled: a.EnableForceRecharge,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.DateTime),
}
if a.AllocatorShopID > 0 {
item.AllocatorShopName = shopMap[a.AllocatorShopID]
} else {
item.AllocatorShopName = "平台"
}
if sr, ok := seriesMap[a.SeriesID]; ok {
item.SeriesName = sr.SeriesName
config, _ := sr.GetOneTimeCommissionConfig()
if config != nil {
item.CommissionType = config.CommissionType
}
}
// 统计套餐数量
pkgCount, _ := s.shopPackageAllocationStore.CountBySeriesAllocationID(ctx, a.ID)
item.PackageCount = int(pkgCount)
items = append(items, item)
}
totalPages := int(total) / pageSize
if int(total)%pageSize != 0 {
totalPages++
}
return &dto.ShopSeriesGrantPageResult{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
// Update 更新系列授权
// PUT /api/admin/shop-series-grants/:id
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeriesGrantRequest) (*dto.ShopSeriesGrantResponse, error) {
operatorID := middleware.GetUserIDFromContext(ctx)
operatorShopID := middleware.GetShopIDFromContext(ctx)
allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "授权记录不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败")
}
// 代理只能修改自己分配出去的授权
if operatorShopID > 0 && allocation.AllocatorShopID != operatorShopID {
return nil, errors.New(errors.CodeForbidden, "无权限操作该授权记录")
}
series, err := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil {
return nil, errors.New(errors.CodeInternalError, "获取系列佣金配置失败")
}
// 更新固定模式佣金金额
if req.OneTimeCommissionAmount != nil && config.CommissionType == "fixed" {
ceiling, ceilErr := s.getParentCeilingFixed(ctx, allocation.AllocatorShopID, allocation.SeriesID, config)
if ceilErr != nil {
return nil, ceilErr
}
if *req.OneTimeCommissionAmount > ceiling {
return nil, errors.New(errors.CodeInvalidParam, "佣金金额不能超过上级天花板")
}
allocation.OneTimeCommissionAmount = *req.OneTimeCommissionAmount
}
// 更新梯度模式阶梯
if len(req.CommissionTiers) > 0 && config.CommissionType == "tiered" {
if len(req.CommissionTiers) != len(config.Tiers) {
return nil, errors.New(errors.CodeInvalidParam, "梯度阶梯数量与系列配置不一致")
}
ceilingMap, ceilErr := s.getParentCeilingTiered(ctx, allocation.AllocatorShopID, allocation.SeriesID, config.Tiers)
if ceilErr != nil {
return nil, ceilErr
}
agentTiers := make([]model.AllocationCommissionTier, 0, len(req.CommissionTiers))
for i, tier := range req.CommissionTiers {
if tier.Threshold != config.Tiers[i].Threshold {
return nil, errors.New(errors.CodeInvalidParam, "梯度阶梯 threshold 与系列配置不匹配")
}
if ceiling, ok := ceilingMap[tier.Threshold]; ok && tier.Amount > ceiling {
return nil, errors.New(errors.CodeInvalidParam, "某档位佣金金额超过上级天花板")
}
agentTiers = append(agentTiers, model.AllocationCommissionTier{
Threshold: tier.Threshold,
Amount: tier.Amount,
})
}
if err := allocation.SetCommissionTiers(agentTiers); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "序列化阶梯佣金失败")
}
}
// 更新强充配置(平台已锁定时忽略)
forceRechargeLocked := config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge || config.EnableForceRecharge
if !forceRechargeLocked && req.EnableForceRecharge != nil {
allocation.EnableForceRecharge = *req.EnableForceRecharge
if req.ForceRechargeAmount != nil {
allocation.ForceRechargeAmount = *req.ForceRechargeAmount
}
}
allocation.Updater = operatorID
if err := s.shopSeriesAllocationStore.Update(ctx, allocation); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新授权记录失败")
}
return s.buildGrantResponse(ctx, allocation, series, config)
}
// ManagePackages 管理授权套餐(新增/更新/删除)
// PUT /api/admin/shop-series-grants/:id/packages
func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGrantPackagesRequest) (*dto.ShopSeriesGrantResponse, error) {
operatorID := middleware.GetUserIDFromContext(ctx)
operatorShopID := middleware.GetShopIDFromContext(ctx)
allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "授权记录不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败")
}
// 代理只能操作自己分配的授权
if operatorShopID > 0 && allocation.AllocatorShopID != operatorShopID {
return nil, errors.New(errors.CodeForbidden, "无权限操作该授权记录")
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txPkgStore := postgres.NewShopPackageAllocationStore(tx)
txHistoryStore := postgres.NewShopPackageAllocationPriceHistoryStore(tx)
for _, item := range req.Packages {
if item.Remove != nil && *item.Remove {
// 软删除已有的 active 分配
existing, findErr := txPkgStore.GetByShopAndPackageForSystem(ctx, allocation.ShopID, item.PackageID)
if findErr != nil {
// 找不到则静默忽略
continue
}
_ = txPkgStore.Delete(ctx, existing.ID)
continue
}
// 新增或更新套餐分配
existing, findErr := txPkgStore.GetByShopAndPackageForSystem(ctx, allocation.ShopID, item.PackageID)
if findErr == nil {
// 已有记录:更新成本价并写历史
oldPrice := existing.CostPrice
existing.CostPrice = item.CostPrice
existing.Updater = operatorID
if updateErr := txPkgStore.Update(ctx, existing); updateErr != nil {
return errors.Wrap(errors.CodeDatabaseError, updateErr, "更新套餐分配失败")
}
if oldPrice != item.CostPrice {
_ = txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
AllocationID: existing.ID,
OldCostPrice: oldPrice,
NewCostPrice: item.CostPrice,
ChangeReason: "手动调价",
ChangedBy: operatorID,
EffectiveFrom: time.Now(),
})
}
} else {
// W1: 校验套餐归属于该系列,防止跨系列套餐混入
pkg, pkgErr := s.packageStore.GetByID(ctx, item.PackageID)
if pkgErr != nil || pkg.SeriesID != allocation.SeriesID {
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
}
// W2: 代理操作时,校验分配者已拥有此套餐授权,防止越权分配
if allocation.AllocatorShopID > 0 {
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocation.AllocatorShopID, item.PackageID)
if authErr != nil {
return errors.New(errors.CodeForbidden, "无权限分配该套餐")
}
}
// 新建分配
pkgAlloc := &model.ShopPackageAllocation{
ShopID: allocation.ShopID,
PackageID: item.PackageID,
AllocatorShopID: allocation.AllocatorShopID,
CostPrice: item.CostPrice,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
ShelfStatus: constants.StatusEnabled,
}
pkgAlloc.Creator = operatorID
pkgAlloc.Updater = operatorID
if createErr := txPkgStore.Create(ctx, pkgAlloc); createErr != nil {
return errors.Wrap(errors.CodeDatabaseError, createErr, "创建套餐分配失败")
}
_ = txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
AllocationID: pkgAlloc.ID,
OldCostPrice: 0,
NewCostPrice: item.CostPrice,
ChangeReason: "新增授权",
ChangedBy: operatorID,
EffectiveFrom: time.Now(),
})
}
}
return nil
})
if err != nil {
return nil, err
}
// 重新查询最新状态
return s.Get(ctx, id)
}
// Delete 删除系列授权(软删除)
// DELETE /api/admin/shop-series-grants/:id
func (s *Service) Delete(ctx context.Context, id uint) error {
operatorShopID := middleware.GetShopIDFromContext(ctx)
allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "授权记录不存在")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败")
}
// 代理只能删除自己分配的授权
if operatorShopID > 0 && allocation.AllocatorShopID != operatorShopID {
return errors.New(errors.CodeForbidden, "无权限操作该授权记录")
}
// 检查是否有子级授权依赖(该店铺是否作为 allocator 向下分配了)
subAllocations, err := s.shopSeriesAllocationStore.GetByAllocatorShopID(ctx, allocation.ShopID)
for _, sub := range subAllocations {
if sub.SeriesID == allocation.SeriesID {
return errors.New(errors.CodeConflict, "该授权存在下级依赖,请先删除下级授权")
}
}
_ = err
// 事务软删除 ShopSeriesAllocation + 所有关联 ShopPackageAllocation
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txSeriesStore := postgres.NewShopSeriesAllocationStore(tx)
txPkgStore := postgres.NewShopPackageAllocationStore(tx)
pkgAllocations, _ := txPkgStore.GetBySeriesAllocationID(ctx, id)
for _, pa := range pkgAllocations {
if delErr := txPkgStore.Delete(ctx, pa.ID); delErr != nil {
return errors.Wrap(errors.CodeDatabaseError, delErr, "删除套餐分配失败")
}
}
if delErr := txSeriesStore.Delete(ctx, id); delErr != nil {
return errors.Wrap(errors.CodeDatabaseError, delErr, "删除系列授权失败")
}
return nil
})
}