All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m12s
1. 修正 retail_price 架构:
- 删除 batch-pricing 接口的 pricing_target 字段和 retail_price 分支
(上级只能改下级成本价,不能改零售价)
- 新增 PATCH /api/admin/packages/:id/retail-price 接口
(代理自己改自己的零售价,校验 retail_price >= cost_price)
2. 清理旧微信 YAML 配置(已全部迁移到数据库 tb_wechat_config):
- 删除 config.yaml 中 wechat.official_account 配置节
- 删除 NewOfficialAccountApp() 旧工厂函数
- 清理 personal_customer service 中的死代码(旧登录/绑定微信方法)
- 清理 docker-compose.prod.yml 中旧微信环境变量和证书挂载注释
3. 归档四个已完成提案到 openspec/changes/archive/
4. 新增前端接口变更说明文档(docs/前端接口变更说明.md)
5. 修正归档提案和 specs 中关于 pricing_target 的错误描述
724 lines
23 KiB
Go
724 lines
23 KiB
Go
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
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||
}
|
||
|
||
func New(
|
||
packageStore *postgres.PackageStore,
|
||
packageSeriesStore *postgres.PackageSeriesStore,
|
||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||
) *Service {
|
||
return &Service{
|
||
packageStore: packageStore,
|
||
packageSeriesStore: packageSeriesStore,
|
||
packageAllocationStore: packageAllocationStore,
|
||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||
}
|
||
}
|
||
|
||
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, "套餐编码已存在")
|
||
}
|
||
|
||
// 校验虚流量配置:启用时虚流量必须 > 0 且 ≤ 真流量
|
||
if req.EnableVirtualData {
|
||
if req.VirtualDataMB == nil || *req.VirtualDataMB <= 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "启用虚流量时,虚流量额度必须大于0")
|
||
}
|
||
realDataMB := int64(0)
|
||
if req.RealDataMB != nil {
|
||
realDataMB = *req.RealDataMB
|
||
}
|
||
if *req.VirtualDataMB > realDataMB {
|
||
return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度")
|
||
}
|
||
}
|
||
|
||
// 校验套餐周期类型和时长配置
|
||
calendarType := constants.PackageCalendarTypeByDay // 默认按天
|
||
if req.CalendarType != nil {
|
||
calendarType = *req.CalendarType
|
||
}
|
||
if calendarType == constants.PackageCalendarTypeNaturalMonth {
|
||
// 自然月套餐:必须提供 duration_months
|
||
if req.DurationMonths <= 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "自然月套餐必须提供有效的duration_months")
|
||
}
|
||
} else if calendarType == constants.PackageCalendarTypeByDay {
|
||
// 按天套餐:必须提供 duration_days
|
||
if req.DurationDays == nil || *req.DurationDays <= 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "按天套餐必须提供有效的duration_days")
|
||
}
|
||
}
|
||
|
||
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,
|
||
CostPrice: req.CostPrice,
|
||
EnableVirtualData: req.EnableVirtualData,
|
||
CalendarType: calendarType,
|
||
Status: constants.StatusEnabled,
|
||
ShelfStatus: 2,
|
||
}
|
||
if req.SeriesID != nil {
|
||
pkg.SeriesID = *req.SeriesID
|
||
}
|
||
if req.RealDataMB != nil {
|
||
pkg.RealDataMB = *req.RealDataMB
|
||
}
|
||
if req.VirtualDataMB != nil {
|
||
pkg.VirtualDataMB = *req.VirtualDataMB
|
||
}
|
||
if req.SuggestedRetailPrice != nil {
|
||
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
|
||
}
|
||
if req.DurationDays != nil {
|
||
pkg.DurationDays = *req.DurationDays
|
||
}
|
||
if req.DataResetCycle != nil {
|
||
pkg.DataResetCycle = *req.DataResetCycle
|
||
} else {
|
||
// 默认月重置
|
||
pkg.DataResetCycle = constants.PackageDataResetMonthly
|
||
}
|
||
if req.EnableRealnameActivation != nil {
|
||
pkg.EnableRealnameActivation = *req.EnableRealnameActivation
|
||
} else {
|
||
pkg.EnableRealnameActivation = true
|
||
}
|
||
pkg.VirtualRatio = calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB)
|
||
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 {
|
||
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.RealDataMB != nil {
|
||
pkg.RealDataMB = *req.RealDataMB
|
||
}
|
||
if req.VirtualDataMB != nil {
|
||
pkg.VirtualDataMB = *req.VirtualDataMB
|
||
}
|
||
if req.EnableVirtualData != nil {
|
||
pkg.EnableVirtualData = *req.EnableVirtualData
|
||
}
|
||
if req.CostPrice != nil {
|
||
pkg.CostPrice = *req.CostPrice
|
||
}
|
||
if req.SuggestedRetailPrice != nil {
|
||
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
|
||
}
|
||
if req.CalendarType != nil {
|
||
pkg.CalendarType = *req.CalendarType
|
||
}
|
||
if req.DurationDays != nil {
|
||
pkg.DurationDays = *req.DurationDays
|
||
}
|
||
if req.DataResetCycle != nil {
|
||
pkg.DataResetCycle = *req.DataResetCycle
|
||
}
|
||
if req.EnableRealnameActivation != nil {
|
||
pkg.EnableRealnameActivation = *req.EnableRealnameActivation
|
||
}
|
||
|
||
// 校验套餐周期类型和时长配置
|
||
if pkg.CalendarType == constants.PackageCalendarTypeNaturalMonth {
|
||
if pkg.DurationMonths <= 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "自然月套餐必须提供有效的duration_months")
|
||
}
|
||
} else if pkg.CalendarType == constants.PackageCalendarTypeByDay {
|
||
if pkg.DurationDays <= 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "按天套餐必须提供有效的duration_days")
|
||
}
|
||
}
|
||
|
||
// 校验虚流量配置
|
||
if pkg.EnableVirtualData {
|
||
if pkg.VirtualDataMB <= 0 {
|
||
return nil, errors.New(errors.CodeInvalidParam, "启用虚流量时,虚流量额度必须大于0")
|
||
}
|
||
if pkg.VirtualDataMB > pkg.RealDataMB {
|
||
return nil, errors.New(errors.CodeInvalidParam, "虚流量额度不能大于真流量额度")
|
||
}
|
||
}
|
||
|
||
pkg.VirtualRatio = calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB)
|
||
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 和 package_id
|
||
seriesIDMap := make(map[uint]bool)
|
||
packageIDs := make([]uint, len(packages))
|
||
for i, pkg := range packages {
|
||
packageIDs[i] = pkg.ID
|
||
if pkg.SeriesID > 0 {
|
||
seriesIDMap[pkg.SeriesID] = true
|
||
}
|
||
}
|
||
|
||
// 批量查询套餐系列(名称和配置)
|
||
seriesMap := make(map[uint]string)
|
||
seriesConfigMap := make(map[uint]*model.OneTimeCommissionConfig)
|
||
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
|
||
// 解析一次性佣金配置
|
||
if series.EnableOneTimeCommission {
|
||
config, _ := series.GetOneTimeCommissionConfig()
|
||
if config != nil {
|
||
seriesConfigMap[series.ID] = config
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
shopID := middleware.GetShopIDFromContext(ctx)
|
||
var allocationMap map[uint]*model.ShopPackageAllocation
|
||
var seriesAllocationMap map[uint]*model.ShopSeriesAllocation
|
||
if userType == constants.UserTypeAgent && shopID > 0 && len(packageIDs) > 0 {
|
||
allocationMap = s.batchGetAllocationsForShop(ctx, shopID, packageIDs)
|
||
// 批量获取店铺的系列分配
|
||
seriesAllocationMap = s.batchGetSeriesAllocationsForShop(ctx, shopID, seriesIDMap)
|
||
}
|
||
|
||
responses := make([]*dto.PackageResponse, len(packages))
|
||
for i, pkg := range packages {
|
||
resp := s.toResponseWithAllocation(ctx, pkg, allocationMap, seriesAllocationMap, seriesConfigMap)
|
||
if pkg.SeriesID > 0 {
|
||
if seriesName, ok := seriesMap[pkg.SeriesID]; ok {
|
||
resp.SeriesName = &seriesName
|
||
}
|
||
}
|
||
responses[i] = resp
|
||
}
|
||
|
||
return responses, total, nil
|
||
}
|
||
|
||
// batchGetSeriesAllocationsForShop 批量获取店铺的系列分配
|
||
func (s *Service) batchGetSeriesAllocationsForShop(ctx context.Context, shopID uint, seriesIDMap map[uint]bool) map[uint]*model.ShopSeriesAllocation {
|
||
result := make(map[uint]*model.ShopSeriesAllocation)
|
||
if len(seriesIDMap) == 0 {
|
||
return result
|
||
}
|
||
|
||
allocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
|
||
if err != nil || len(allocations) == 0 {
|
||
return result
|
||
}
|
||
|
||
for _, alloc := range allocations {
|
||
if seriesIDMap[alloc.SeriesID] {
|
||
result[alloc.SeriesID] = alloc
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
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, "未授权访问")
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
|
||
// 代理用户:修改自己分配记录的 shelf_status,不影响平台全局套餐状态
|
||
if userType == constants.UserTypeAgent {
|
||
return s.updateAgentShelfStatus(ctx, id, shelfStatus, currentUserID)
|
||
}
|
||
|
||
// 平台/超管:修改套餐全局 shelf_status
|
||
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 == constants.ShelfStatusOn && 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
|
||
}
|
||
|
||
// UpdateRetailPrice 代理修改自己店铺的套餐零售价
|
||
func (s *Service) UpdateRetailPrice(ctx context.Context, packageID uint, retailPrice int64) error {
|
||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||
if currentUserID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||
}
|
||
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
if userType != constants.UserTypeAgent {
|
||
return errors.New(errors.CodeForbidden, "仅代理用户可修改零售价")
|
||
}
|
||
|
||
shopID := middleware.GetShopIDFromContext(ctx)
|
||
if shopID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||
}
|
||
|
||
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeNotFound, "该套餐未分配给您")
|
||
}
|
||
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
|
||
}
|
||
|
||
if retailPrice < allocation.CostPrice {
|
||
return errors.New(errors.CodeInvalidParam, "零售价不能低于成本价")
|
||
}
|
||
|
||
if err := s.packageAllocationStore.UpdateRetailPrice(ctx, allocation.ID, retailPrice, currentUserID); err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "更新零售价失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// updateAgentShelfStatus 代理上下架路径:更新分配记录的 shelf_status
|
||
func (s *Service) updateAgentShelfStatus(ctx context.Context, packageID uint, shelfStatus int, updaterID uint) error {
|
||
shopID := middleware.GetShopIDFromContext(ctx)
|
||
if shopID == 0 {
|
||
return errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||
}
|
||
|
||
// 查找代理对该套餐的分配记录
|
||
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeNotFound, "该套餐未分配给您,无法操作上下架")
|
||
}
|
||
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
|
||
}
|
||
|
||
// 上架时检查套餐全局禁用状态
|
||
if shelfStatus == constants.ShelfStatusOn {
|
||
pkg, err := s.packageStore.GetByID(ctx, packageID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New(errors.CodeNotFound, "套餐不存在")
|
||
}
|
||
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
|
||
}
|
||
if pkg.Status == constants.StatusDisabled {
|
||
return errors.New(errors.CodeInvalidStatus, "套餐已禁用,无法上架")
|
||
}
|
||
}
|
||
|
||
if err := s.packageAllocationStore.UpdateShelfStatus(ctx, allocation.ID, shelfStatus, updaterID); 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
|
||
}
|
||
|
||
var durationDays *int
|
||
if pkg.CalendarType == constants.PackageCalendarTypeByDay && pkg.DurationDays > 0 {
|
||
durationDays = &pkg.DurationDays
|
||
}
|
||
|
||
resp := &dto.PackageResponse{
|
||
ID: pkg.ID,
|
||
PackageCode: pkg.PackageCode,
|
||
PackageName: pkg.PackageName,
|
||
SeriesID: seriesID,
|
||
PackageType: pkg.PackageType,
|
||
DurationMonths: pkg.DurationMonths,
|
||
RealDataMB: pkg.RealDataMB,
|
||
VirtualDataMB: pkg.VirtualDataMB,
|
||
EnableVirtualData: pkg.EnableVirtualData,
|
||
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
|
||
CostPrice: pkg.CostPrice,
|
||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||
CalendarType: pkg.CalendarType,
|
||
DurationDays: durationDays,
|
||
DataResetCycle: pkg.DataResetCycle,
|
||
EnableRealnameActivation: pkg.EnableRealnameActivation,
|
||
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
|
||
resp.RetailPrice = &allocation.RetailPrice
|
||
profitMargin := allocation.RetailPrice - allocation.CostPrice
|
||
resp.ProfitMargin = &profitMargin
|
||
resp.ShelfStatus = allocation.ShelfStatus
|
||
}
|
||
}
|
||
|
||
return resp
|
||
}
|
||
|
||
func (s *Service) batchGetAllocationsForShop(ctx context.Context, shopID uint, packageIDs []uint) map[uint]*model.ShopPackageAllocation {
|
||
allocationMap := make(map[uint]*model.ShopPackageAllocation)
|
||
|
||
allocations, err := s.packageAllocationStore.GetByShopAndPackages(ctx, shopID, packageIDs)
|
||
if err != nil || len(allocations) == 0 {
|
||
return allocationMap
|
||
}
|
||
|
||
for _, alloc := range allocations {
|
||
allocationMap[alloc.PackageID] = alloc
|
||
}
|
||
|
||
return allocationMap
|
||
}
|
||
|
||
func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package, allocationMap map[uint]*model.ShopPackageAllocation, seriesAllocationMap map[uint]*model.ShopSeriesAllocation, seriesConfigMap map[uint]*model.OneTimeCommissionConfig) *dto.PackageResponse {
|
||
var seriesID *uint
|
||
if pkg.SeriesID > 0 {
|
||
seriesID = &pkg.SeriesID
|
||
}
|
||
|
||
var durationDays *int
|
||
if pkg.CalendarType == constants.PackageCalendarTypeByDay && pkg.DurationDays > 0 {
|
||
durationDays = &pkg.DurationDays
|
||
}
|
||
|
||
resp := &dto.PackageResponse{
|
||
ID: pkg.ID,
|
||
PackageCode: pkg.PackageCode,
|
||
PackageName: pkg.PackageName,
|
||
SeriesID: seriesID,
|
||
PackageType: pkg.PackageType,
|
||
DurationMonths: pkg.DurationMonths,
|
||
RealDataMB: pkg.RealDataMB,
|
||
VirtualDataMB: pkg.VirtualDataMB,
|
||
EnableVirtualData: pkg.EnableVirtualData,
|
||
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
|
||
CostPrice: pkg.CostPrice,
|
||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||
CalendarType: pkg.CalendarType,
|
||
DurationDays: durationDays,
|
||
DataResetCycle: pkg.DataResetCycle,
|
||
EnableRealnameActivation: pkg.EnableRealnameActivation,
|
||
Status: pkg.Status,
|
||
ShelfStatus: pkg.ShelfStatus,
|
||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||
}
|
||
|
||
if allocationMap != nil {
|
||
if allocation, ok := allocationMap[pkg.ID]; ok {
|
||
resp.CostPrice = allocation.CostPrice
|
||
resp.RetailPrice = &allocation.RetailPrice
|
||
profitMargin := allocation.RetailPrice - allocation.CostPrice
|
||
resp.ProfitMargin = &profitMargin
|
||
resp.ShelfStatus = allocation.ShelfStatus
|
||
}
|
||
}
|
||
|
||
// 填充返佣信息(仅代理用户可见)
|
||
if pkg.SeriesID > 0 && seriesAllocationMap != nil && seriesConfigMap != nil {
|
||
s.fillCommissionInfo(resp, pkg.SeriesID, seriesAllocationMap, seriesConfigMap)
|
||
}
|
||
|
||
return resp
|
||
}
|
||
|
||
// fillCommissionInfo 填充返佣信息到响应中
|
||
func (s *Service) fillCommissionInfo(resp *dto.PackageResponse, seriesID uint, seriesAllocationMap map[uint]*model.ShopSeriesAllocation, seriesConfigMap map[uint]*model.OneTimeCommissionConfig) {
|
||
seriesAllocation, hasAllocation := seriesAllocationMap[seriesID]
|
||
config, hasConfig := seriesConfigMap[seriesID]
|
||
|
||
if !hasAllocation || !hasConfig {
|
||
return
|
||
}
|
||
|
||
// 一次性佣金是否启用由 PackageSeries.enable_one_time_commission 控制
|
||
if !config.Enable {
|
||
return
|
||
}
|
||
|
||
// 设置一次性佣金金额
|
||
oneTimeAmount := seriesAllocation.OneTimeCommissionAmount
|
||
resp.OneTimeCommissionAmount = &oneTimeAmount
|
||
|
||
// 设置当前返佣比例(格式化为可读字符串)
|
||
if config.CommissionType == "fixed" {
|
||
// 固定金额模式:显示代理能拿到的金额
|
||
resp.CurrentCommissionRate = formatAmount(seriesAllocation.OneTimeCommissionAmount)
|
||
} else if config.CommissionType == "tiered" && len(config.Tiers) > 0 {
|
||
// 梯度模式:显示基础金额,并设置梯度信息
|
||
resp.CurrentCommissionRate = formatAmount(seriesAllocation.OneTimeCommissionAmount)
|
||
|
||
// 构建梯度信息
|
||
tierInfo := s.buildTierInfo(config.Tiers, seriesAllocation.OneTimeCommissionAmount)
|
||
if tierInfo != nil {
|
||
resp.TierInfo = tierInfo
|
||
}
|
||
}
|
||
}
|
||
|
||
// buildTierInfo 构建梯度返佣信息
|
||
func (s *Service) buildTierInfo(tiers []model.OneTimeCommissionTier, currentAmount int64) *dto.CommissionTierInfo {
|
||
if len(tiers) == 0 {
|
||
return nil
|
||
}
|
||
|
||
tierInfo := &dto.CommissionTierInfo{
|
||
CurrentRate: formatAmount(currentAmount),
|
||
}
|
||
|
||
// 找到下一个可达到的梯度
|
||
// 梯度按 threshold 升序排列,找到第一个 amount > currentAmount 的梯度
|
||
for _, tier := range tiers {
|
||
if tier.Amount > currentAmount {
|
||
tierInfo.NextThreshold = &tier.Threshold
|
||
nextRate := formatAmount(tier.Amount)
|
||
tierInfo.NextRate = nextRate
|
||
break
|
||
}
|
||
}
|
||
|
||
return tierInfo
|
||
}
|
||
|
||
// formatAmount 格式化金额为可读字符串(分转元)
|
||
func formatAmount(amountFen int64) string {
|
||
yuan := float64(amountFen) / 100
|
||
if yuan == float64(int64(yuan)) {
|
||
return fmt.Sprintf("%.0f元/张", yuan)
|
||
}
|
||
return fmt.Sprintf("%.2f元/张", yuan)
|
||
}
|
||
|
||
// calculateVirtualRatio 计算虚流量比例
|
||
// enable_virtual_data=true 且 virtual_data_mb>0 时 = real_data_mb/virtual_data_mb,否则 = 1.0
|
||
func calculateVirtualRatio(enableVirtualData bool, realDataMB, virtualDataMB int64) float64 {
|
||
if enableVirtualData && virtualDataMB > 0 {
|
||
return float64(realDataMB) / float64(virtualDataMB)
|
||
}
|
||
return 1.0
|
||
}
|