Files
junhong_cmp_fiber/internal/service/package/service.go
huang 1cf17e8f14
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m46s
清理冗余的梯度返佣(TierCommission)配置
- 移除 Model 层:删除 ShopSeriesCommissionTier 模型及相关字段
- 更新 DTO:删除 TierCommissionConfig、TierEntry 类型及相关请求/响应字段
- 删除 Store 层:移除 ShopSeriesCommissionTierStore 及相关查询逻辑
- 简化 Service 层:删除梯度返佣处理逻辑,统计查询移除 tier_bonus 字段
- 数据库迁移:创建 000034_remove_tier_commission 移除相关表和字段
- 更新测试:移除梯度返佣相关测试用例,更新集成测试
- OpenAPI 文档:删除梯度返佣相关 schema 和枚举值
- 归档变更:归档 remove-tier-commission-redundancy 到 archive/2026-01-30-
- 同步规范:更新 4 个主 specs,标记废弃功能并添加迁移指引

原因:梯度返佣功能与一次性梯度佣金功能重复,且从未实现实际计算逻辑
迁移:使用一次性佣金的梯度模式 (OneTimeCommissionConfig.type = "tiered") 替代
2026-01-30 14:57:24 +08:00

406 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package packagepkg
import (
"context"
"fmt"
"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 {
packageStore *postgres.PackageStore
packageSeriesStore *postgres.PackageSeriesStore
packageAllocationStore *postgres.ShopPackageAllocationStore
seriesAllocationStore *postgres.ShopSeriesAllocationStore
}
func New(
packageStore *postgres.PackageStore,
packageSeriesStore *postgres.PackageSeriesStore,
packageAllocationStore *postgres.ShopPackageAllocationStore,
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
) *Service {
return &Service{
packageStore: packageStore,
packageSeriesStore: packageSeriesStore,
packageAllocationStore: packageAllocationStore,
seriesAllocationStore: seriesAllocationStore,
}
}
func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*dto.PackageResponse, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
existing, _ := s.packageStore.GetByCode(ctx, req.PackageCode)
if existing != nil {
return nil, errors.New(errors.CodeConflict, "套餐编码已存在")
}
var seriesName *string
if req.SeriesID != nil && *req.SeriesID > 0 {
series, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐系列失败")
}
seriesName = &series.SeriesName
}
pkg := &model.Package{
PackageCode: req.PackageCode,
PackageName: req.PackageName,
PackageType: req.PackageType,
DurationMonths: req.DurationMonths,
Price: req.Price,
Status: constants.StatusEnabled,
ShelfStatus: 2,
}
if req.SeriesID != nil {
pkg.SeriesID = *req.SeriesID
}
if req.DataType != nil {
pkg.DataType = *req.DataType
}
if req.RealDataMB != nil {
pkg.RealDataMB = *req.RealDataMB
}
if req.VirtualDataMB != nil {
pkg.VirtualDataMB = *req.VirtualDataMB
}
if req.DataAmountMB != nil {
pkg.DataAmountMB = *req.DataAmountMB
}
if req.SuggestedCostPrice != nil {
pkg.SuggestedCostPrice = *req.SuggestedCostPrice
}
if req.SuggestedRetailPrice != nil {
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
}
pkg.Creator = currentUserID
if err := s.packageStore.Create(ctx, pkg); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建套餐失败")
}
resp := s.toResponse(ctx, pkg)
resp.SeriesName = seriesName
return resp, nil
}
func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error) {
pkg, err := s.packageStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
resp := s.toResponse(ctx, pkg)
// 查询系列名称
if pkg.SeriesID > 0 {
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
if err == nil {
resp.SeriesName = &series.SeriesName
}
}
return resp, nil
}
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageRequest) (*dto.PackageResponse, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
pkg, err := s.packageStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
var seriesName *string
if req.SeriesID != nil && *req.SeriesID > 0 {
series, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐系列失败")
}
pkg.SeriesID = *req.SeriesID
seriesName = &series.SeriesName
} else if pkg.SeriesID > 0 {
// 如果没有更新 SeriesID但现有套餐有 SeriesID则查询当前的系列名称
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
if err == nil {
seriesName = &series.SeriesName
}
}
if req.PackageName != nil {
pkg.PackageName = *req.PackageName
}
if req.PackageType != nil {
pkg.PackageType = *req.PackageType
}
if req.DurationMonths != nil {
pkg.DurationMonths = *req.DurationMonths
}
if req.DataType != nil {
pkg.DataType = *req.DataType
}
if req.RealDataMB != nil {
pkg.RealDataMB = *req.RealDataMB
}
if req.VirtualDataMB != nil {
pkg.VirtualDataMB = *req.VirtualDataMB
}
if req.DataAmountMB != nil {
pkg.DataAmountMB = *req.DataAmountMB
}
if req.Price != nil {
pkg.Price = *req.Price
}
if req.SuggestedCostPrice != nil {
pkg.SuggestedCostPrice = *req.SuggestedCostPrice
}
if req.SuggestedRetailPrice != nil {
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
}
pkg.Updater = currentUserID
if err := s.packageStore.Update(ctx, pkg); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "更新套餐失败")
}
resp := s.toResponse(ctx, pkg)
resp.SeriesName = seriesName
return resp, nil
}
func (s *Service) Delete(ctx context.Context, id uint) error {
_, err := s.packageStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
if err := s.packageStore.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除套餐失败")
}
return nil
}
func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto.PackageResponse, 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.PackageName != nil {
filters["package_name"] = *req.PackageName
}
if req.SeriesID != nil {
filters["series_id"] = *req.SeriesID
}
if req.Status != nil {
filters["status"] = *req.Status
}
if req.ShelfStatus != nil {
filters["shelf_status"] = *req.ShelfStatus
}
if req.PackageType != nil {
filters["package_type"] = *req.PackageType
}
packages, total, err := s.packageStore.List(ctx, opts, filters)
if err != nil {
return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐列表失败")
}
// 收集所有唯一的 series_id
seriesIDMap := make(map[uint]bool)
for _, pkg := range packages {
if pkg.SeriesID > 0 {
seriesIDMap[pkg.SeriesID] = true
}
}
// 批量查询套餐系列
seriesMap := make(map[uint]string)
if len(seriesIDMap) > 0 {
seriesIDs := make([]uint, 0, len(seriesIDMap))
for id := range seriesIDMap {
seriesIDs = append(seriesIDs, id)
}
seriesList, err := s.packageSeriesStore.GetByIDs(ctx, seriesIDs)
if err != nil {
return nil, 0, errors.Wrap(errors.CodeInternalError, err, "批量查询套餐系列失败")
}
for _, series := range seriesList {
seriesMap[series.ID] = series.SeriesName
}
}
// 构建响应,填充系列名称
responses := make([]*dto.PackageResponse, len(packages))
for i, pkg := range packages {
resp := s.toResponse(ctx, pkg)
if pkg.SeriesID > 0 {
if seriesName, ok := seriesMap[pkg.SeriesID]; ok {
resp.SeriesName = &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, "未授权访问")
}
pkg, err := s.packageStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
pkg.Status = status
pkg.Updater = currentUserID
if status == constants.StatusDisabled {
pkg.ShelfStatus = 2
}
if err := s.packageStore.Update(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐状态失败")
}
return nil
}
func (s *Service) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus int) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
pkg, err := s.packageStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
if shelfStatus == 1 && pkg.Status == constants.StatusDisabled {
return errors.New(errors.CodeInvalidStatus, "禁用的套餐不能上架,请先启用")
}
pkg.ShelfStatus = shelfStatus
pkg.Updater = currentUserID
if err := s.packageStore.Update(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐上架状态失败")
}
return nil
}
func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.PackageResponse {
var seriesID *uint
if pkg.SeriesID > 0 {
seriesID = &pkg.SeriesID
}
resp := &dto.PackageResponse{
ID: pkg.ID,
PackageCode: pkg.PackageCode,
PackageName: pkg.PackageName,
SeriesID: seriesID,
PackageType: pkg.PackageType,
DurationMonths: pkg.DurationMonths,
DataType: pkg.DataType,
RealDataMB: pkg.RealDataMB,
VirtualDataMB: pkg.VirtualDataMB,
DataAmountMB: pkg.DataAmountMB,
Price: pkg.Price,
SuggestedCostPrice: pkg.SuggestedCostPrice,
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
Status: pkg.Status,
ShelfStatus: pkg.ShelfStatus,
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
}
userType := middleware.GetUserTypeFromContext(ctx)
shopID := middleware.GetShopIDFromContext(ctx)
if userType == constants.UserTypeAgent && shopID > 0 {
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, pkg.ID)
if err == nil && allocation != nil {
resp.CostPrice = &allocation.CostPrice
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
resp.ProfitMargin = &profitMargin
commissionInfo := s.getCommissionInfo(ctx, allocation.AllocationID)
if commissionInfo != nil {
resp.CurrentCommissionRate = commissionInfo.CurrentRate
resp.TierInfo = commissionInfo
}
}
}
return resp
}
func (s *Service) getCommissionInfo(ctx context.Context, allocationID uint) *dto.CommissionTierInfo {
seriesAllocation, err := s.seriesAllocationStore.GetByID(ctx, allocationID)
if err != nil {
return nil
}
info := &dto.CommissionTierInfo{}
if seriesAllocation.BaseCommissionMode == constants.CommissionModeFixed {
info.CurrentRate = fmt.Sprintf("%.2f元/单", float64(seriesAllocation.BaseCommissionValue)/100)
} else {
info.CurrentRate = fmt.Sprintf("%.1f%%", float64(seriesAllocation.BaseCommissionValue)/10)
}
return info
}