实现套餐生效条件覆盖与购买快照
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
@@ -2222,6 +2223,11 @@ func (s *Service) markOrderCreated(ctx context.Context, idempotencyKey string, o
|
||||
|
||||
// activateMainPackage 任务 8.2-8.4: 主套餐激活逻辑
|
||||
func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *model.Order, pkg *model.Package, carrierType string, carrierID uint, now time.Time) error {
|
||||
terms, err := s.resolvePackageTerms(ctx, pkg, order.SellerShopID)
|
||||
if err != nil {
|
||||
s.logger.Error("生成套餐计时快照失败", zap.Uint("package_id", pkg.ID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if err := s.lockPackageCarrier(ctx, tx, carrierType, carrierID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2252,19 +2258,17 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
priority = 1
|
||||
activatedAt = now
|
||||
// 使用工具函数计算过期时间
|
||||
expiresAt = packagepkg.CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||
expiresAt = packagepkg.CalculateExpiryTime(terms.CalendarType, activatedAt, terms.DurationMonths, terms.DurationDays)
|
||||
// 计算下次重置时间(基于套餐周期类型)
|
||||
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||||
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, terms.CalendarType, now, activatedAt)
|
||||
|
||||
// REALNAME-02: 后台囤货场景三维决策(卡类型 + 购买路径 + expiry_base + 实名状态)
|
||||
// 查询卡类型(行业卡永远直接激活,不等实名)
|
||||
// 同时查询载体当前实名状态:已实名则跳过 pending,直接激活
|
||||
var cardCategory string
|
||||
var currentlyRealnamed bool
|
||||
if carrierType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.Select("card_category", "real_name_status").First(&card, carrierID).Error; err == nil {
|
||||
cardCategory = card.CardCategory
|
||||
if err := tx.Select("real_name_status").First(&card, carrierID).Error; err == nil {
|
||||
currentlyRealnamed = card.RealNameStatus == constants.RealNameStatusVerified
|
||||
}
|
||||
// err != nil 时 currentlyRealnamed 保守默认 false,不阻断购买流程
|
||||
@@ -2285,9 +2289,9 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
// 判断是否 C 端购买(C 端购买前已做实名前置检查,直接激活)
|
||||
isCEndPurchase := order.BuyerType == model.BuyerTypePersonal
|
||||
|
||||
if cardCategory != "industry" && !isCEndPurchase {
|
||||
if !isCEndPurchase {
|
||||
// 后台囤货路径:按 expiry_base 决定是否等实名
|
||||
if pkg.ExpiryBase != "from_purchase" {
|
||||
if terms.ExpiryBase != constants.PackageExpiryBaseFromPurchase {
|
||||
if currentlyRealnamed {
|
||||
s.logger.Info("购买时载体已实名,直接激活套餐",
|
||||
zap.String("carrier_type", carrierType),
|
||||
@@ -2303,7 +2307,7 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
}
|
||||
// from_purchase:立即激活,status 已为 Active,保持不变
|
||||
}
|
||||
// 行业卡或 C 端购买:直接激活,status 已为 Active,保持不变
|
||||
// C 端购买已完成实名前置校验,直接激活。
|
||||
}
|
||||
|
||||
// 创建套餐使用记录
|
||||
@@ -2332,6 +2336,7 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PackageIsGift: pkg.IsGift,
|
||||
}
|
||||
terms.Apply(usage)
|
||||
|
||||
if carrierType == "iot_card" {
|
||||
usage.IotCardID = carrierID
|
||||
@@ -2362,6 +2367,11 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
|
||||
// activateAddonPackage 任务 8.5-8.7: 加油包激活逻辑
|
||||
func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *model.Order, pkg *model.Package, carrierType string, carrierID uint, now time.Time) error {
|
||||
terms, err := s.resolvePackageTerms(ctx, pkg, order.SellerShopID)
|
||||
if err != nil {
|
||||
s.logger.Error("生成加油包计时快照失败", zap.Uint("package_id", pkg.ID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 任务 8.5-8.6: 检查是否有可挂靠主套餐(待生效、未过期的生效中或已用完)
|
||||
mainPackage, err := packagepkg.FindAttachableMainPackageForAddon(tx, carrierType, carrierID, now)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -2421,6 +2431,7 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PackageIsGift: pkg.IsGift,
|
||||
}
|
||||
terms.Apply(usage)
|
||||
|
||||
if carrierType == "iot_card" {
|
||||
usage.IotCardID = carrierID
|
||||
@@ -2436,6 +2447,20 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) resolvePackageTerms(ctx context.Context, pkg *model.Package, sellerShopID *uint) (packagedomain.TermsSnapshot, error) {
|
||||
var allocation *model.ShopPackageAllocation
|
||||
if sellerShopID != nil && *sellerShopID > 0 {
|
||||
found, err := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, *sellerShopID, pkg.ID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return packagedomain.TermsSnapshot{}, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐分配失败")
|
||||
}
|
||||
if err == nil {
|
||||
allocation = found
|
||||
}
|
||||
}
|
||||
return packagedomain.ResolveTermsSnapshot(pkg, allocation)
|
||||
}
|
||||
|
||||
func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint) {
|
||||
if s.queueClient == nil {
|
||||
s.logger.Warn("队列客户端未初始化,跳过佣金计算任务入队", zap.Uint("order_id", orderID))
|
||||
|
||||
@@ -113,18 +113,22 @@ func (s *ActivationService) ActivateByRealname(ctx context.Context, carrierType
|
||||
}
|
||||
}
|
||||
|
||||
// REALNAME-04: 按 ExpiryBase 选择激活计时基准
|
||||
terms, termsErr := ResolveUsageTerms(usage, &pkg, s.logger)
|
||||
if termsErr != nil {
|
||||
return termsErr
|
||||
}
|
||||
// REALNAME-04: 按购买快照选择激活计时基准
|
||||
// from_purchase:购买时起算,用 PackageUsage 创建时间;否则用实名触发当前时刻
|
||||
var activatedAt time.Time
|
||||
if pkg.ExpiryBase == "from_purchase" {
|
||||
if terms.ExpiryBase == constants.PackageExpiryBaseFromPurchase {
|
||||
activatedAt = usage.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||
expiresAt := CalculateExpiryTime(terms.CalendarType, activatedAt, terms.DurationMonths, terms.DurationDays)
|
||||
|
||||
// 计算下次重置时间
|
||||
nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||||
nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, terms.CalendarType, now, activatedAt)
|
||||
|
||||
// 更新套餐使用记录
|
||||
updates := map[string]interface{}{
|
||||
@@ -552,20 +556,24 @@ func (s *ActivationService) isCarrierRealnamed(ctx context.Context, tx *gorm.DB,
|
||||
}
|
||||
|
||||
func (s *ActivationService) activatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, pkg *model.Package, carrierType string, carrierID uint, now time.Time, logMessage string) error {
|
||||
terms, err := ResolveUsageTerms(usage, pkg, s.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ExpiryBase=from_purchase 只用于"等待实名激活"场景(REALNAME-04):套餐已购买但资产未实名,
|
||||
// 计时基准按购买时间算,实名只是解锁使用权。此函数同时被"前一个主套餐到期后排队顺延"场景复用,
|
||||
// 这种情况下 usage.PendingRealnameActivation 为 false,不应该套用购买时间,否则排队等待的天数会
|
||||
// 从到期时间里被扣掉。只有当这条记录确实是因为等实名才被搁置时,才按 ExpiryBase 选基准。
|
||||
var activatedAt time.Time
|
||||
if usage.PendingRealnameActivation && pkg.ExpiryBase == "from_purchase" {
|
||||
if usage.PendingRealnameActivation && terms.ExpiryBase == constants.PackageExpiryBaseFromPurchase {
|
||||
activatedAt = usage.CreatedAt
|
||||
} else {
|
||||
activatedAt = now
|
||||
}
|
||||
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||||
expiresAt := CalculateExpiryTime(terms.CalendarType, activatedAt, terms.DurationMonths, terms.DurationDays)
|
||||
|
||||
// 计算下次重置时间
|
||||
nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||||
nextResetAt := CalculateNextResetTime(pkg.DataResetCycle, terms.CalendarType, now, activatedAt)
|
||||
|
||||
// 更新套餐使用记录
|
||||
updates := map[string]interface{}{
|
||||
|
||||
@@ -101,18 +101,18 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
|
||||
}
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: req.PackageCode,
|
||||
PackageName: req.PackageName,
|
||||
PackageType: req.PackageType,
|
||||
IsGift: req.IsGift,
|
||||
DurationMonths: req.DurationMonths,
|
||||
CostPrice: req.CostPrice,
|
||||
PriceConfigStatus: priceConfigStatus,
|
||||
PackageCode: req.PackageCode,
|
||||
PackageName: req.PackageName,
|
||||
PackageType: req.PackageType,
|
||||
IsGift: req.IsGift,
|
||||
DurationMonths: req.DurationMonths,
|
||||
CostPrice: req.CostPrice,
|
||||
PriceConfigStatus: priceConfigStatus,
|
||||
SuggestedRetailPrice: storedRetailPrice,
|
||||
EnableVirtualData: req.EnableVirtualData,
|
||||
CalendarType: calendarType,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
EnableVirtualData: req.EnableVirtualData,
|
||||
CalendarType: calendarType,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
}
|
||||
if req.SeriesID != nil {
|
||||
pkg.SeriesID = *req.SeriesID
|
||||
@@ -607,29 +607,33 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
}
|
||||
|
||||
resp := &dto.PackageResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
IsGift: pkg.IsGift,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
|
||||
PriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
|
||||
CalendarType: pkg.CalendarType,
|
||||
DurationDays: durationDays,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
ExpiryBase: pkg.ExpiryBase,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
IsGift: pkg.IsGift,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
|
||||
PriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
|
||||
CalendarType: pkg.CalendarType,
|
||||
DurationDays: durationDays,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
ExpiryBase: pkg.ExpiryBase,
|
||||
DefaultExpiryBase: pkg.ExpiryBase,
|
||||
DefaultExpiryBaseName: ExpiryBaseName(pkg.ExpiryBase),
|
||||
EffectiveExpiryBase: pkg.ExpiryBase,
|
||||
EffectiveExpiryBaseName: ExpiryBaseName(pkg.ExpiryBase),
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
@@ -646,6 +650,7 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
profitMargin := effectiveRetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
resp.ShelfStatus = allocation.ShelfStatus
|
||||
applyAllocationExpiryBase(resp, pkg, allocation)
|
||||
}
|
||||
} else {
|
||||
effectiveRetailPrice := packageprice.PackageEffectiveRetailPrice(pkg)
|
||||
@@ -684,29 +689,33 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
|
||||
}
|
||||
|
||||
resp := &dto.PackageResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
IsGift: pkg.IsGift,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
|
||||
PriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
|
||||
CalendarType: pkg.CalendarType,
|
||||
DurationDays: durationDays,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
ExpiryBase: pkg.ExpiryBase,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
IsGift: pkg.IsGift,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
|
||||
PriceConfigStatus: pkg.PriceConfigStatus,
|
||||
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
|
||||
CalendarType: pkg.CalendarType,
|
||||
DurationDays: durationDays,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
ExpiryBase: pkg.ExpiryBase,
|
||||
DefaultExpiryBase: pkg.ExpiryBase,
|
||||
DefaultExpiryBaseName: ExpiryBaseName(pkg.ExpiryBase),
|
||||
EffectiveExpiryBase: pkg.ExpiryBase,
|
||||
EffectiveExpiryBaseName: ExpiryBaseName(pkg.ExpiryBase),
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if allocationMap != nil {
|
||||
@@ -719,6 +728,7 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
|
||||
resp.EffectiveRetailPrice = &effectiveRetailPrice
|
||||
profitMargin := effectiveRetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
applyAllocationExpiryBase(resp, pkg, allocation)
|
||||
resp.ShelfStatus = allocation.ShelfStatus
|
||||
}
|
||||
} else {
|
||||
@@ -734,6 +744,13 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
|
||||
return resp
|
||||
}
|
||||
|
||||
func applyAllocationExpiryBase(resp *dto.PackageResponse, pkg *model.Package, allocation *model.ShopPackageAllocation) {
|
||||
resp.ExpiryBaseOverride = allocation.ExpiryBaseOverride
|
||||
resp.ExpiryBaseOverrideName = ExpiryBaseOverrideName(allocation.ExpiryBaseOverride)
|
||||
resp.EffectiveExpiryBase = EffectiveExpiryBase(pkg, allocation)
|
||||
resp.EffectiveExpiryBaseName = ExpiryBaseName(resp.EffectiveExpiryBase)
|
||||
}
|
||||
|
||||
// fillCommissionInfo 填充返佣信息到响应中
|
||||
func (s *Service) fillCommissionInfo(resp *dto.PackageResponse, seriesID uint, seriesAllocationMap map[uint]*model.ShopSeriesAllocation, seriesConfigMap map[uint]*model.OneTimeCommissionConfig) {
|
||||
seriesAllocation, hasAllocation := seriesAllocationMap[seriesID]
|
||||
|
||||
49
internal/service/package/terms.go
Normal file
49
internal/service/package/terms.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package packagepkg
|
||||
|
||||
import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ValidateExpiryBaseOverride 校验显式提交的分配生效条件覆盖。
|
||||
func ValidateExpiryBaseOverride(value *string, submitted bool) (*string, error) {
|
||||
if !submitted {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if *value != constants.PackageExpiryBaseFromActivation && *value != constants.PackageExpiryBaseFromPurchase {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// EffectiveExpiryBase 返回套餐分配最终采用的生效条件。
|
||||
func EffectiveExpiryBase(pkg *model.Package, allocation *model.ShopPackageAllocation) string {
|
||||
if allocation != nil && allocation.ExpiryBaseOverride != nil {
|
||||
return *allocation.ExpiryBaseOverride
|
||||
}
|
||||
return pkg.ExpiryBase
|
||||
}
|
||||
|
||||
// ExpiryBaseName 返回生效条件中文名称。
|
||||
func ExpiryBaseName(value string) string {
|
||||
switch value {
|
||||
case constants.PackageExpiryBaseFromActivation:
|
||||
return "实名激活时生效"
|
||||
case constants.PackageExpiryBaseFromPurchase:
|
||||
return "购买即生效"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// ExpiryBaseOverrideName 返回覆盖值中文名称,空值表示跟随套餐默认。
|
||||
func ExpiryBaseOverrideName(value *string) string {
|
||||
if value == nil {
|
||||
return "跟随套餐默认"
|
||||
}
|
||||
return ExpiryBaseName(*value)
|
||||
}
|
||||
57
internal/service/package/terms_test.go
Normal file
57
internal/service/package/terms_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package packagepkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// TestValidateExpiryBaseOverride 验证覆盖字段必须显式提交且仅接受既定枚举。
|
||||
func TestValidateExpiryBaseOverride(t *testing.T) {
|
||||
fromPurchase := constants.PackageExpiryBaseFromPurchase
|
||||
invalid := "from_realname"
|
||||
tests := []struct {
|
||||
name string
|
||||
value *string
|
||||
submitted bool
|
||||
wantValue *string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "字段缺失", wantErr: true},
|
||||
{name: "跟随默认", submitted: true},
|
||||
{name: "购买即生效", value: &fromPurchase, submitted: true, wantValue: &fromPurchase},
|
||||
{name: "非法枚举", value: &invalid, submitted: true, wantErr: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := ValidateExpiryBaseOverride(test.value, test.submitted)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("错误状态不符合预期:%v", err)
|
||||
}
|
||||
if test.wantValue != nil && (got == nil || *got != *test.wantValue) {
|
||||
t.Fatalf("覆盖值不符合预期:%v", got)
|
||||
}
|
||||
if test.wantValue == nil && !test.wantErr && got != nil {
|
||||
t.Fatalf("期望跟随默认,实际为:%v", *got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEffectiveExpiryBase 验证覆盖优先于套餐默认值。
|
||||
func TestEffectiveExpiryBase(t *testing.T) {
|
||||
pkg := &model.Package{ExpiryBase: constants.PackageExpiryBaseFromActivation}
|
||||
if got := EffectiveExpiryBase(pkg, nil); got != constants.PackageExpiryBaseFromActivation {
|
||||
t.Fatalf("无覆盖时应使用套餐默认值,实际为 %s", got)
|
||||
}
|
||||
override := constants.PackageExpiryBaseFromPurchase
|
||||
allocation := &model.ShopPackageAllocation{ExpiryBaseOverride: &override}
|
||||
if got := EffectiveExpiryBase(pkg, allocation); got != constants.PackageExpiryBaseFromPurchase {
|
||||
t.Fatalf("有覆盖时应使用覆盖值,实际为 %s", got)
|
||||
}
|
||||
pkg.ExpiryBase = constants.PackageExpiryBaseFromPurchase
|
||||
if got := EffectiveExpiryBase(pkg, allocation); got != constants.PackageExpiryBaseFromPurchase {
|
||||
t.Fatalf("套餐默认值变化不应改变覆盖结果,实际为 %s", got)
|
||||
}
|
||||
}
|
||||
47
internal/service/package/usage_terms.go
Normal file
47
internal/service/package/usage_terms.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package packagepkg
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var historicalTermsFallbackCount atomic.Uint64
|
||||
|
||||
// ResolveUsageTerms 优先读取使用记录快照,仅对完整空快照的历史记录显式回退。
|
||||
func ResolveUsageTerms(usage *model.PackageUsage, pkg *model.Package, logger *zap.Logger) (packagedomain.TermsSnapshot, error) {
|
||||
terms := packagedomain.TermsSnapshotFromUsage(usage)
|
||||
if terms.IsValid() {
|
||||
return terms, nil
|
||||
}
|
||||
if !isEmptyHistoricalTerms(usage) {
|
||||
if logger != nil {
|
||||
logger.Error("套餐使用记录计时快照异常", zap.Uint("package_usage_id", usage.ID), zap.Uint("package_id", usage.PackageID))
|
||||
}
|
||||
return packagedomain.TermsSnapshot{}, errors.New(errors.CodeInternalError, "套餐使用记录计时快照异常")
|
||||
}
|
||||
fallback, err := packagedomain.ResolveTermsSnapshot(pkg, nil)
|
||||
if err != nil {
|
||||
return packagedomain.TermsSnapshot{}, err
|
||||
}
|
||||
historicalTermsFallbackCount.Add(1)
|
||||
if logger != nil {
|
||||
logger.Warn("历史套餐使用记录缺少计时快照,回退套餐当前配置",
|
||||
zap.Uint("package_usage_id", usage.ID), zap.Uint("package_id", usage.PackageID),
|
||||
zap.Uint64("historical_terms_fallback_count", historicalTermsFallbackCount.Load()))
|
||||
}
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
// HistoricalTermsFallbackCount 返回历史计时条款回退累计次数。
|
||||
func HistoricalTermsFallbackCount() uint64 {
|
||||
return historicalTermsFallbackCount.Load()
|
||||
}
|
||||
|
||||
func isEmptyHistoricalTerms(usage *model.PackageUsage) bool {
|
||||
return usage.ExpiryBaseSnapshot == "" && usage.CalendarTypeSnapshot == "" &&
|
||||
usage.DurationMonthsSnapshot == 0 && usage.DurationDaysSnapshot == 0
|
||||
}
|
||||
59
internal/service/package/usage_terms_test.go
Normal file
59
internal/service/package/usage_terms_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package packagepkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestResolveUsageTerms 验证快照优先、历史完整空值回退和部分缺失拒绝。
|
||||
func TestResolveUsageTerms(t *testing.T) {
|
||||
pkg := &model.Package{
|
||||
ExpiryBase: constants.PackageExpiryBaseFromActivation,
|
||||
CalendarType: constants.PackageCalendarTypeByDay, DurationDays: 30,
|
||||
}
|
||||
usage := &model.PackageUsage{
|
||||
ExpiryBaseSnapshot: constants.PackageExpiryBaseFromPurchase,
|
||||
CalendarTypeSnapshot: constants.PackageCalendarTypeNaturalMonth,
|
||||
DurationMonthsSnapshot: 12,
|
||||
}
|
||||
terms, err := ResolveUsageTerms(usage, pkg, zap.NewNop())
|
||||
if err != nil || terms.ExpiryBase != constants.PackageExpiryBaseFromPurchase || terms.DurationMonths != 12 {
|
||||
t.Fatalf("应优先读取不可变快照:%+v, %v", terms, err)
|
||||
}
|
||||
|
||||
before := HistoricalTermsFallbackCount()
|
||||
historical := &model.PackageUsage{Model: usage.Model}
|
||||
terms, err = ResolveUsageTerms(historical, pkg, zap.NewNop())
|
||||
if err != nil || terms.DurationDays != 30 || HistoricalTermsFallbackCount() != before+1 {
|
||||
t.Fatalf("历史空快照应可观测回退:%+v, %v", terms, err)
|
||||
}
|
||||
|
||||
broken := &model.PackageUsage{ExpiryBaseSnapshot: constants.PackageExpiryBaseFromPurchase}
|
||||
if _, err = ResolveUsageTerms(broken, pkg, zap.NewNop()); err == nil {
|
||||
t.Fatal("部分缺失快照必须拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveUsageTermsKeepsPurchasedTerms 验证购买后修改套餐配置不改变已有使用记录语义。
|
||||
func TestResolveUsageTermsKeepsPurchasedTerms(t *testing.T) {
|
||||
usage := &model.PackageUsage{
|
||||
ExpiryBaseSnapshot: constants.PackageExpiryBaseFromPurchase,
|
||||
CalendarTypeSnapshot: constants.PackageCalendarTypeByDay,
|
||||
DurationDaysSnapshot: 90,
|
||||
}
|
||||
pkg := &model.Package{
|
||||
ExpiryBase: constants.PackageExpiryBaseFromActivation,
|
||||
CalendarType: constants.PackageCalendarTypeNaturalMonth,
|
||||
DurationMonths: 1,
|
||||
}
|
||||
terms, err := ResolveUsageTerms(usage, pkg, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("读取购买快照失败:%v", err)
|
||||
}
|
||||
if terms.ExpiryBase != constants.PackageExpiryBaseFromPurchase || terms.CalendarType != constants.PackageCalendarTypeByDay || terms.DurationDays != 90 {
|
||||
t.Fatalf("套餐当前配置不应覆盖购买快照:%+v", terms)
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package shop_package_batch_allocation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
"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"
|
||||
@@ -18,6 +20,12 @@ type Service struct {
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
shopStore *postgres.ShopStore
|
||||
auditService AuditService
|
||||
}
|
||||
|
||||
// AuditService 敏感配置变更审计能力。
|
||||
type AuditService interface {
|
||||
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -26,6 +34,7 @@ func New(
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
auditService AuditService,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
@@ -33,61 +42,123 @@ func New(
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
shopStore: shopStore,
|
||||
auditService: auditService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePackagesRequest) error {
|
||||
// UpdateExpiryBase 修改单条套餐分配的生效条件覆盖。
|
||||
func (s *Service) UpdateExpiryBase(ctx context.Context, id uint, req *dto.UpdateAllocationExpiryBaseRequest) (*dto.ShopPackageAllocationTermsResponse, error) {
|
||||
override, err := packagepkg.ValidateExpiryBaseOverride(req.ExpiryBaseOverride, req.ExpiryBaseOverrideSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allocation, err := s.packageAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
pkg, err := s.packageStore.GetByID(ctx, allocation.PackageID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
before := allocation.ExpiryBaseOverride
|
||||
if !sameNullableString(before, override) {
|
||||
allocation.ExpiryBaseOverride = override
|
||||
allocation.Updater = middleware.GetUserIDFromContext(ctx)
|
||||
if err := s.packageAllocationStore.Update(ctx, allocation); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新套餐分配生效条件失败")
|
||||
}
|
||||
s.logExpiryBaseAudit(ctx, allocation, before, override)
|
||||
}
|
||||
return buildTermsResponse(pkg, allocation), nil
|
||||
}
|
||||
|
||||
func buildTermsResponse(pkg *model.Package, allocation *model.ShopPackageAllocation) *dto.ShopPackageAllocationTermsResponse {
|
||||
effective := packagepkg.EffectiveExpiryBase(pkg, allocation)
|
||||
return &dto.ShopPackageAllocationTermsResponse{
|
||||
ID: allocation.ID, DefaultExpiryBase: pkg.ExpiryBase,
|
||||
DefaultExpiryBaseName: packagepkg.ExpiryBaseName(pkg.ExpiryBase),
|
||||
ExpiryBaseOverride: allocation.ExpiryBaseOverride,
|
||||
ExpiryBaseOverrideName: packagepkg.ExpiryBaseOverrideName(allocation.ExpiryBaseOverride),
|
||||
EffectiveExpiryBase: effective, EffectiveExpiryBaseName: packagepkg.ExpiryBaseName(effective),
|
||||
}
|
||||
}
|
||||
|
||||
func sameNullableString(left, right *string) bool {
|
||||
return left == nil && right == nil || left != nil && right != nil && *left == *right
|
||||
}
|
||||
|
||||
func (s *Service) logExpiryBaseAudit(ctx context.Context, allocation *model.ShopPackageAllocation, before, after *string) {
|
||||
if s.auditService == nil {
|
||||
return
|
||||
}
|
||||
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||||
OperatorID: middleware.GetUserIDFromContext(ctx), OperatorType: middleware.GetUserTypeFromContext(ctx),
|
||||
OperatorName: middleware.GetUsernameFromContext(ctx), OperationType: "update_package_expiry_base",
|
||||
OperationDesc: fmt.Sprintf("修改套餐分配生效条件覆盖: %d", allocation.ID),
|
||||
BeforeData: model.JSONB{"allocation_id": allocation.ID, "expiry_base_override": before},
|
||||
AfterData: model.JSONB{"allocation_id": allocation.ID, "expiry_base_override": after},
|
||||
RequestID: middleware.GetRequestIDFromContext(ctx), IPAddress: middleware.GetIPFromContext(ctx), UserAgent: middleware.GetUserAgentFromContext(ctx),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePackagesRequest) (*dto.BatchAllocatePackagesResponse, error) {
|
||||
expiryBaseOverride, err := packagepkg.ValidateExpiryBaseOverride(req.ExpiryBaseOverride, req.ExpiryBaseOverrideSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
allocatorShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
if userType == constants.UserTypeAgent && allocatorShopID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
return nil, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
targetShop, err := s.shopStore.GetByID(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
return nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取目标店铺失败")
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取目标店铺失败")
|
||||
}
|
||||
|
||||
if userType == constants.UserTypeAgent {
|
||||
if targetShop.ParentID == nil || *targetShop.ParentID != allocatorShopID {
|
||||
return errors.New(errors.CodeForbidden, "只能分配给直属下级店铺")
|
||||
return nil, errors.New(errors.CodeForbidden, "只能分配给直属下级店铺")
|
||||
}
|
||||
}
|
||||
|
||||
packages, err := s.getEnabledPackagesBySeries(ctx, req.SeriesID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(packages) == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "该系列下没有启用的套餐")
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该系列下没有启用的套餐")
|
||||
}
|
||||
|
||||
// 检查目标店铺是否有该系列的分配
|
||||
seriesAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, req.ShopID, req.SeriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeInvalidParam, "目标店铺没有该系列的分配权限")
|
||||
return nil, errors.New(errors.CodeInvalidParam, "目标店铺没有该系列的分配权限")
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询系列分配失败")
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询系列分配失败")
|
||||
}
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := &dto.BatchAllocatePackagesResponse{TotalPackages: len(packages), Allocations: []dto.ShopPackageAllocationTermsResponse{}}
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
txPkgAllocStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
|
||||
for _, pkg := range packages {
|
||||
// 已存在该套餐的分配记录则跳过,避免唯一约束冲突
|
||||
_, existErr := txPkgAllocStore.GetByShopAndPackageForSystem(ctx, req.ShopID, pkg.ID)
|
||||
if existErr == nil {
|
||||
result.SkippedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -97,23 +168,30 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
|
||||
}
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
BaseModel: model.BaseModel{Creator: currentUserID, Updater: currentUserID},
|
||||
ShopID: req.ShopID,
|
||||
PackageID: pkg.ID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: costPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
BaseModel: model.BaseModel{Creator: currentUserID, Updater: currentUserID},
|
||||
ShopID: req.ShopID,
|
||||
PackageID: pkg.ID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: costPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
RetailPriceConfigStatus: pkg.PriceConfigStatus,
|
||||
SeriesAllocationID: &seriesAllocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
SeriesAllocationID: &seriesAllocation.ID,
|
||||
ExpiryBaseOverride: expiryBaseOverride,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
if err := tx.Create(allocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建套餐分配失败")
|
||||
}
|
||||
result.Allocations = append(result.Allocations, *buildTermsResponse(pkg, allocation))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.AllocatedCount = len(result.Allocations)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) getEnabledPackagesBySeries(ctx context.Context, seriesID uint) ([]*model.Package, error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
"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"
|
||||
@@ -185,12 +186,18 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
|
||||
continue
|
||||
}
|
||||
packages = append(packages, dto.ShopSeriesGrantPackageItem{
|
||||
PackageID: pa.PackageID,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageCode: pkg.PackageCode,
|
||||
CostPrice: pa.CostPrice,
|
||||
ShelfStatus: pa.ShelfStatus,
|
||||
Status: pa.Status,
|
||||
PackageID: pa.PackageID,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageCode: pkg.PackageCode,
|
||||
CostPrice: pa.CostPrice,
|
||||
ShelfStatus: pa.ShelfStatus,
|
||||
Status: pa.Status,
|
||||
DefaultExpiryBase: pkg.ExpiryBase,
|
||||
DefaultExpiryBaseName: packagepkg.ExpiryBaseName(pkg.ExpiryBase),
|
||||
ExpiryBaseOverride: pa.ExpiryBaseOverride,
|
||||
ExpiryBaseOverrideName: packagepkg.ExpiryBaseOverrideName(pa.ExpiryBaseOverride),
|
||||
EffectiveExpiryBase: packagepkg.EffectiveExpiryBase(pkg, pa),
|
||||
EffectiveExpiryBaseName: packagepkg.ExpiryBaseName(packagepkg.EffectiveExpiryBase(pkg, pa)),
|
||||
})
|
||||
}
|
||||
resp.Packages = packages
|
||||
@@ -201,6 +208,10 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
|
||||
// Create 创建系列授权
|
||||
// POST /api/admin/shop-series-grants
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequest) (*dto.ShopSeriesGrantResponse, error) {
|
||||
expiryBaseOverride, err := packagepkg.ValidateExpiryBaseOverride(req.ExpiryBaseOverride, req.ExpiryBaseOverrideSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||||
operatorShopID := middleware.GetShopIDFromContext(ctx)
|
||||
operatorType := middleware.GetUserTypeFromContext(ctx)
|
||||
@@ -339,15 +350,16 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
||||
}
|
||||
}
|
||||
pkgAlloc := &model.ShopPackageAllocation{
|
||||
ShopID: req.ShopID,
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
ShopID: req.ShopID,
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
RetailPriceConfigStatus: pkg.PriceConfigStatus,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.ShelfStatusOn,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
ExpiryBaseOverride: expiryBaseOverride,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.ShelfStatusOn,
|
||||
}
|
||||
pkgAlloc.Creator = operatorID
|
||||
pkgAlloc.Updater = operatorID
|
||||
@@ -600,6 +612,10 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
|
||||
// ManagePackages 管理授权套餐(新增/更新/删除)
|
||||
// PUT /api/admin/shop-series-grants/:id/packages
|
||||
func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGrantPackagesRequest) (*dto.ShopSeriesGrantResponse, error) {
|
||||
expiryBaseOverride, err := packagepkg.ValidateExpiryBaseOverride(req.ExpiryBaseOverride, req.ExpiryBaseOverrideSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
operatorID := middleware.GetUserIDFromContext(ctx)
|
||||
operatorShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
@@ -677,15 +693,16 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
|
||||
}
|
||||
}
|
||||
pkgAlloc := &model.ShopPackageAllocation{
|
||||
ShopID: allocation.ShopID,
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocation.AllocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
ShopID: allocation.ShopID,
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocation.AllocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
RetailPriceConfigStatus: pkg.PriceConfigStatus,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.ShelfStatusOn,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
ExpiryBaseOverride: expiryBaseOverride,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.ShelfStatusOn,
|
||||
}
|
||||
pkgAlloc.Creator = operatorID
|
||||
pkgAlloc.Updater = operatorID
|
||||
|
||||
Reference in New Issue
Block a user