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:
722
internal/service/shop_series_grant/service.go
Normal file
722
internal/service/shop_series_grant/service.go
Normal 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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user