Files
junhong_cmp_fiber/internal/service/package_series/service.go
huang b18ecfeb55
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
refactor: 一次性佣金配置从套餐级别提升到系列级别
主要变更:
- 新增 tb_shop_series_allocation 表,存储系列级别的一次性佣金配置
- ShopPackageAllocation 移除 one_time_commission_amount 字段
- PackageSeries 新增 enable_one_time_commission 字段控制是否启用一次性佣金
- 新增 /api/admin/shop-series-allocations CRUD 接口
- 佣金计算逻辑改为从 ShopSeriesAllocation 获取一次性佣金金额
- 删除废弃的 ShopSeriesOneTimeCommissionTier 模型
- OpenAPI Tag '系列分配' 和 '单套餐分配' 合并为 '套餐分配'

迁移脚本:
- 000042: 重构佣金套餐模型
- 000043: 简化佣金分配
- 000044: 一次性佣金分配重构
- 000045: PackageSeries 添加 enable_one_time_commission 字段

测试:
- 新增验收测试 (shop_series_allocation, commission_calculation)
- 新增流程测试 (one_time_commission_chain)
- 删除过时的单元测试(已被验收测试覆盖)
2026-02-04 14:28:44 +08:00

267 lines
8.2 KiB
Go

package package_series
import (
"context"
"time"
"gorm.io/gorm"
"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 {
packageSeriesStore *postgres.PackageSeriesStore
}
func New(packageSeriesStore *postgres.PackageSeriesStore) *Service {
return &Service{packageSeriesStore: packageSeriesStore}
}
func (s *Service) Create(ctx context.Context, req *dto.CreatePackageSeriesRequest) (*dto.PackageSeriesResponse, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
existing, _ := s.packageSeriesStore.GetByCode(ctx, req.SeriesCode)
if existing != nil {
return nil, errors.New(errors.CodeConflict, "系列编码已存在")
}
series := &model.PackageSeries{
SeriesCode: req.SeriesCode,
SeriesName: req.SeriesName,
Description: req.Description,
Status: constants.StatusEnabled,
OneTimeCommissionConfigJSON: "{}",
}
series.Creator = currentUserID
if req.OneTimeCommissionConfig != nil {
config := s.dtoToModelConfig(req.OneTimeCommissionConfig)
if err := series.SetOneTimeCommissionConfig(config); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "设置一次性佣金配置失败")
}
}
if err := s.packageSeriesStore.Create(ctx, series); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建套餐系列失败")
}
return s.toResponse(series), nil
}
func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageSeriesResponse, error) {
series, err := s.packageSeriesStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐系列失败")
}
return s.toResponse(series), nil
}
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageSeriesRequest) (*dto.PackageSeriesResponse, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
series, err := s.packageSeriesStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐系列失败")
}
if req.SeriesName != nil {
series.SeriesName = *req.SeriesName
}
if req.Description != nil {
series.Description = *req.Description
}
if req.OneTimeCommissionConfig != nil {
config := s.dtoToModelConfig(req.OneTimeCommissionConfig)
if err := series.SetOneTimeCommissionConfig(config); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "设置一次性佣金配置失败")
}
}
series.Updater = currentUserID
if err := s.packageSeriesStore.Update(ctx, series); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "更新套餐系列失败")
}
return s.toResponse(series), nil
}
func (s *Service) Delete(ctx context.Context, id uint) error {
_, err := s.packageSeriesStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐系列不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐系列失败")
}
if err := s.packageSeriesStore.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除套餐系列失败")
}
return nil
}
func (s *Service) List(ctx context.Context, req *dto.PackageSeriesListRequest) ([]*dto.PackageSeriesResponse, 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.SeriesName != nil {
filters["series_name"] = *req.SeriesName
}
if req.Status != nil {
filters["status"] = *req.Status
}
if req.EnableOneTimeCommission != nil {
filters["enable_one_time_commission"] = *req.EnableOneTimeCommission
}
seriesList, total, err := s.packageSeriesStore.List(ctx, opts, filters)
if err != nil {
return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐系列列表失败")
}
responses := make([]*dto.PackageSeriesResponse, len(seriesList))
for i, series := range seriesList {
responses[i] = s.toResponse(series)
}
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, "未授权访问")
}
series, err := s.packageSeriesStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐系列不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐系列失败")
}
series.Status = status
series.Updater = currentUserID
if err := s.packageSeriesStore.Update(ctx, series); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐系列状态失败")
}
return nil
}
func (s *Service) toResponse(series *model.PackageSeries) *dto.PackageSeriesResponse {
resp := &dto.PackageSeriesResponse{
ID: series.ID,
SeriesCode: series.SeriesCode,
SeriesName: series.SeriesName,
Description: series.Description,
EnableOneTimeCommission: series.EnableOneTimeCommission,
Status: series.Status,
CreatedAt: series.CreatedAt.Format(time.RFC3339),
UpdatedAt: series.UpdatedAt.Format(time.RFC3339),
}
if config, err := series.GetOneTimeCommissionConfig(); err == nil && config != nil {
resp.OneTimeCommissionConfig = s.modelToDTO(config)
}
return resp
}
func (s *Service) dtoToModelConfig(dtoConfig *dto.SeriesOneTimeCommissionConfigDTO) *model.OneTimeCommissionConfig {
if dtoConfig == nil {
return nil
}
var tiers []model.OneTimeCommissionTier
if len(dtoConfig.Tiers) > 0 {
tiers = make([]model.OneTimeCommissionTier, len(dtoConfig.Tiers))
for i, tier := range dtoConfig.Tiers {
tiers[i] = model.OneTimeCommissionTier{
Dimension: tier.Dimension,
StatScope: tier.StatScope,
Threshold: tier.Threshold,
Amount: tier.Amount,
}
}
}
return &model.OneTimeCommissionConfig{
Enable: dtoConfig.Enable,
TriggerType: dtoConfig.TriggerType,
Threshold: dtoConfig.Threshold,
CommissionType: dtoConfig.CommissionType,
CommissionAmount: dtoConfig.CommissionAmount,
Tiers: tiers,
ValidityType: dtoConfig.ValidityType,
ValidityValue: dtoConfig.ValidityValue,
EnableForceRecharge: dtoConfig.EnableForceRecharge,
ForceCalcType: dtoConfig.ForceCalcType,
ForceAmount: dtoConfig.ForceAmount,
}
}
func (s *Service) modelToDTO(config *model.OneTimeCommissionConfig) *dto.SeriesOneTimeCommissionConfigDTO {
if config == nil {
return nil
}
var tiers []dto.OneTimeCommissionTierDTO
if len(config.Tiers) > 0 {
tiers = make([]dto.OneTimeCommissionTierDTO, len(config.Tiers))
for i, tier := range config.Tiers {
tiers[i] = dto.OneTimeCommissionTierDTO{
Dimension: tier.Dimension,
StatScope: tier.StatScope,
Threshold: tier.Threshold,
Amount: tier.Amount,
}
}
}
return &dto.SeriesOneTimeCommissionConfigDTO{
Enable: config.Enable,
TriggerType: config.TriggerType,
Threshold: config.Threshold,
CommissionType: config.CommissionType,
CommissionAmount: config.CommissionAmount,
Tiers: tiers,
ValidityType: config.ValidityType,
ValidityValue: config.ValidityValue,
EnableForceRecharge: config.EnableForceRecharge,
ForceCalcType: config.ForceCalcType,
ForceAmount: config.ForceAmount,
}
}