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.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.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, "未授权访问") } 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 } 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, 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 profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice resp.ProfitMargin = &profitMargin } } 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, 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 profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice resp.ProfitMargin = &profitMargin } } // 填充返佣信息(仅代理用户可见) 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 } // 检查是否启用一次性佣金 if !seriesAllocation.EnableOneTimeCommission || !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) }