feat: 客户端接口数据模型基础准备
- 新增资产状态、订单来源、操作人类型、实名链接类型常量 - 8个模型新增字段(asset_status/generation/source/retail_price等) - 数据库迁移000082:7张表15+字段,含存量retail_price回填 - BUG-1修复:代理零售价渠道隔离,cost_price分配锁定 - BUG-2修复:一次性佣金仅客户端订单触发 - BUG-4修复:充值回调Store操作纳入事务 - 新增资产手动停用接口(PATCH /iot-cards/:id/deactivate、/devices/:id/deactivate) - Carrier管理新增实名链接配置 - 后台订单generation写时快照 - BatchUpdatePricing支持retail_price调价目标 - 清理全部H5旧接口和个人客户旧登录方法
This commit is contained in:
88
internal/service/asset/lifecycle_service.go
Normal file
88
internal/service/asset/lifecycle_service.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var deactivatableAssetStatuses = []int{constants.AssetStatusInStock, constants.AssetStatusSold}
|
||||
|
||||
// LifecycleService 资产生命周期服务
|
||||
type LifecycleService struct {
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
}
|
||||
|
||||
// NewLifecycleService 创建资产生命周期服务
|
||||
func NewLifecycleService(db *gorm.DB, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore) *LifecycleService {
|
||||
return &LifecycleService{
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
}
|
||||
}
|
||||
|
||||
// DeactivateIotCard 手动停用 IoT 卡
|
||||
func (s *LifecycleService) DeactivateIotCard(ctx context.Context, id uint) error {
|
||||
card, err := s.iotCardStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(errors.CodeIotCardNotFound)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
|
||||
if !canDeactivateAsset(card.AssetStatus) {
|
||||
return errors.New(errors.CodeForbidden, "当前状态不允许停用")
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
|
||||
Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeactivateDevice 手动停用设备
|
||||
func (s *LifecycleService) DeactivateDevice(ctx context.Context, id uint) error {
|
||||
device, err := s.deviceStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
|
||||
if !canDeactivateAsset(device.AssetStatus) {
|
||||
return errors.New(errors.CodeForbidden, "当前状态不允许停用")
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).Model(&model.Device{}).
|
||||
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
|
||||
Update("asset_status", constants.AssetStatusDeactivated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func canDeactivateAsset(assetStatus int) bool {
|
||||
return assetStatus == constants.AssetStatusInStock || assetStatus == constants.AssetStatusSold
|
||||
}
|
||||
@@ -41,6 +41,12 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateCarrierRequest) (*d
|
||||
Description: req.Description,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
if req.RealnameLinkType != nil {
|
||||
carrier.RealnameLinkType = *req.RealnameLinkType
|
||||
}
|
||||
if req.RealnameLinkTemplate != nil {
|
||||
carrier.RealnameLinkTemplate = *req.RealnameLinkTemplate
|
||||
}
|
||||
carrier.Creator = currentUserID
|
||||
|
||||
if err := s.carrierStore.Create(ctx, carrier); err != nil {
|
||||
@@ -81,6 +87,15 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateCarrierReq
|
||||
if req.Description != nil {
|
||||
carrier.Description = *req.Description
|
||||
}
|
||||
if req.RealnameLinkType != nil {
|
||||
carrier.RealnameLinkType = *req.RealnameLinkType
|
||||
}
|
||||
if req.RealnameLinkTemplate != nil {
|
||||
carrier.RealnameLinkTemplate = *req.RealnameLinkTemplate
|
||||
}
|
||||
if carrier.RealnameLinkType == "template" && carrier.RealnameLinkTemplate == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "模板URL类型必须提供实名链接模板")
|
||||
}
|
||||
carrier.Updater = currentUserID
|
||||
|
||||
if err := s.carrierStore.Update(ctx, carrier); err != nil {
|
||||
@@ -169,13 +184,15 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
|
||||
func (s *Service) toResponse(c *model.Carrier) *dto.CarrierResponse {
|
||||
return &dto.CarrierResponse{
|
||||
ID: c.ID,
|
||||
CarrierCode: c.CarrierCode,
|
||||
CarrierName: c.CarrierName,
|
||||
CarrierType: c.CarrierType,
|
||||
Description: c.Description,
|
||||
Status: c.Status,
|
||||
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
||||
ID: c.ID,
|
||||
CarrierCode: c.CarrierCode,
|
||||
CarrierName: c.CarrierName,
|
||||
CarrierType: c.CarrierType,
|
||||
Description: c.Description,
|
||||
RealnameLinkType: c.RealnameLinkType,
|
||||
RealnameLinkTemplate: c.RealnameLinkTemplate,
|
||||
Status: c.Status,
|
||||
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -201,7 +202,7 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
}
|
||||
|
||||
func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *gorm.DB, order *model.Order, cardID uint) error {
|
||||
if order.IsPurchaseOnBehalf {
|
||||
if order.IsPurchaseOnBehalf || order.Source != constants.OrderSourceClient {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -285,7 +286,7 @@ func (s *Service) TriggerOneTimeCommissionForCard(ctx context.Context, order *mo
|
||||
}
|
||||
|
||||
func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx *gorm.DB, order *model.Order, deviceID uint) error {
|
||||
if order.IsPurchaseOnBehalf {
|
||||
if order.IsPurchaseOnBehalf || order.Source != constants.OrderSourceClient {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -564,6 +564,17 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
||||
}
|
||||
}
|
||||
|
||||
// 从资产获取当前 generation(用于订单快照)
|
||||
var assetGeneration int
|
||||
if validationResult.Card != nil {
|
||||
assetGeneration = validationResult.Card.Generation
|
||||
} else if validationResult.Device != nil {
|
||||
assetGeneration = validationResult.Device.Generation
|
||||
}
|
||||
if assetGeneration == 0 {
|
||||
assetGeneration = 1
|
||||
}
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
@@ -571,6 +582,8 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
||||
},
|
||||
OrderNo: s.orderStore.GenerateOrderNo(),
|
||||
OrderType: req.OrderType,
|
||||
Source: constants.OrderSourceAdmin,
|
||||
Generation: assetGeneration,
|
||||
BuyerType: orderBuyerType,
|
||||
BuyerID: orderBuyerID,
|
||||
IotCardID: req.IotCardID,
|
||||
|
||||
@@ -533,9 +533,9 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, pkg.ID)
|
||||
if err == nil && allocation != nil {
|
||||
resp.CostPrice = allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.RetailPrice = &allocation.RetailPrice
|
||||
profitMargin := allocation.RetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
// 代理查询时,shelf_status 返回自己分配记录的值,而非平台全局值
|
||||
resp.ShelfStatus = allocation.ShelfStatus
|
||||
}
|
||||
}
|
||||
@@ -595,9 +595,9 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
|
||||
if allocationMap != nil {
|
||||
if allocation, ok := allocationMap[pkg.ID]; ok {
|
||||
resp.CostPrice = allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.RetailPrice = &allocation.RetailPrice
|
||||
profitMargin := allocation.RetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
// 代理查询时,shelf_status 返回自己分配记录的值,而非平台全局值
|
||||
resp.ShelfStatus = allocation.ShelfStatus
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,44 +133,57 @@ func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, serie
|
||||
}
|
||||
|
||||
if sellerShopID > 0 {
|
||||
// 代理渠道:检查卖家代理的 allocation.shelf_status,不检查 package.shelf_status
|
||||
if err := s.validateAgentShelfStatus(ctx, sellerShopID, pkgID); err != nil {
|
||||
return nil, 0, err
|
||||
// 代理渠道:检查上架状态并获取分配记录,使用零售价
|
||||
allocation, allocErr := s.validateAgentAllocation(ctx, sellerShopID, pkgID)
|
||||
if allocErr != nil {
|
||||
return nil, 0, allocErr
|
||||
}
|
||||
// 零售价低于成本价时视为不可购买,防止亏损售卖
|
||||
if allocation.RetailPrice < allocation.CostPrice {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐价格配置异常,暂不可购买")
|
||||
}
|
||||
totalPrice += allocation.RetailPrice
|
||||
} else {
|
||||
// 平台自营渠道:检查 package.shelf_status
|
||||
if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
}
|
||||
totalPrice += pkg.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
packages = append(packages, pkg)
|
||||
totalPrice += pkg.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
return packages, totalPrice, nil
|
||||
}
|
||||
|
||||
// validateAgentShelfStatus 校验卖家代理的分配记录上架状态
|
||||
func (s *Service) validateAgentShelfStatus(ctx context.Context, sellerShopID, packageID uint) error {
|
||||
// 使用不带数据权限过滤的查询,避免 buyer ctx 的权限限制干扰系统级校验
|
||||
// validateAgentAllocation 校验卖家代理的分配记录上架状态,并返回分配记录
|
||||
func (s *Service) validateAgentAllocation(ctx context.Context, sellerShopID, packageID uint) (*model.ShopPackageAllocation, error) {
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackageForSystem(ctx, sellerShopID, packageID)
|
||||
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, "查询套餐分配记录失败")
|
||||
}
|
||||
|
||||
if allocation.ShelfStatus != constants.ShelfStatusOn {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
}
|
||||
|
||||
return nil
|
||||
return allocation, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, buyerType string) int64 {
|
||||
return pkg.SuggestedRetailPrice
|
||||
// GetPurchasePrice 获取购买价格
|
||||
// 代理渠道(sellerShopID > 0)返回 allocation.RetailPrice,平台渠道返回 Package.SuggestedRetailPrice
|
||||
func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, sellerShopID uint) (int64, error) {
|
||||
if sellerShopID > 0 {
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackageForSystem(ctx, sellerShopID, pkg.ID)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐分配记录失败")
|
||||
}
|
||||
return allocation.RetailPrice, nil
|
||||
}
|
||||
return pkg.SuggestedRetailPrice, nil
|
||||
}
|
||||
|
||||
// ValidateAdminOfflineCardPurchase 后台 offline 订单专用卡验证
|
||||
|
||||
@@ -306,18 +306,17 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
|
||||
// 6. 事务处理:更新订单状态、增加余额、更新累计充值、触发佣金
|
||||
now := time.Now()
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 6.1 更新充值订单状态(带状态检查,实现乐观锁)
|
||||
// 6.1 更新充值订单状态(带状态检查,使用事务内 tx 确保原子性)
|
||||
oldStatus := constants.RechargeStatusPending
|
||||
if err := s.assetRechargeStore.UpdateStatusWithOptimisticLock(ctx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil {
|
||||
if err := s.assetRechargeStore.UpdateStatusWithOptimisticLockDB(ctx, tx, recharge.ID, &oldStatus, constants.RechargeStatusPaid, &now, nil); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 状态已变更,幂等处理
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新充值订单状态失败")
|
||||
}
|
||||
|
||||
// 6.2 更新支付信息
|
||||
if err := s.assetRechargeStore.UpdatePaymentInfo(ctx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil {
|
||||
// 6.2 更新支付信息(使用事务内 tx)
|
||||
if err := s.assetRechargeStore.UpdatePaymentInfoWithDB(ctx, tx, recharge.ID, &paymentMethod, &paymentTransactionID); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新支付信息失败")
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
|
||||
PackageID: pkg.ID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: costPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
SeriesAllocationID: &seriesAllocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
@@ -65,37 +65,86 @@ func (s *Service) BatchUpdatePricing(ctx context.Context, req *dto.BatchUpdateCo
|
||||
return nil, errors.New(errors.CodeInvalidParam, "没有找到符合条件的分配记录")
|
||||
}
|
||||
|
||||
pricingTarget := req.PricingTarget
|
||||
if pricingTarget == "" {
|
||||
pricingTarget = "cost_price"
|
||||
}
|
||||
|
||||
updatedCount := 0
|
||||
now := time.Now()
|
||||
|
||||
affectedIDs := make([]uint, 0)
|
||||
skipped := make([]dto.BatchPricingSkipped, 0)
|
||||
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, allocation := range allocations {
|
||||
oldPrice := allocation.CostPrice
|
||||
newPrice := s.calculateAdjustedPrice(oldPrice, &req.PriceAdjustment)
|
||||
if pricingTarget == "retail_price" {
|
||||
oldRetailPrice := allocation.RetailPrice
|
||||
newRetailPrice := s.calculateAdjustedPrice(oldRetailPrice, &req.PriceAdjustment)
|
||||
if newRetailPrice == oldRetailPrice {
|
||||
continue
|
||||
}
|
||||
if newRetailPrice < allocation.CostPrice {
|
||||
skipped = append(skipped, dto.BatchPricingSkipped{
|
||||
AllocationID: allocation.ID,
|
||||
Reason: "零售价不能低于成本价",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if newPrice == oldPrice {
|
||||
continue
|
||||
}
|
||||
history := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldRetailPrice,
|
||||
NewCostPrice: newRetailPrice,
|
||||
ChangeReason: req.ChangeReason + "(零售价调整)",
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
if err := tx.Create(history).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建价格历史失败")
|
||||
}
|
||||
|
||||
history := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldPrice,
|
||||
NewCostPrice: newPrice,
|
||||
ChangeReason: req.ChangeReason,
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
allocation.RetailPrice = newRetailPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := tx.Save(allocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新零售价失败")
|
||||
}
|
||||
} else {
|
||||
oldPrice := allocation.CostPrice
|
||||
newPrice := s.calculateAdjustedPrice(oldPrice, &req.PriceAdjustment)
|
||||
if newPrice == oldPrice {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := tx.Create(history).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建价格历史失败")
|
||||
}
|
||||
// cost_price 锁定检查:存在下级分配记录时跳过
|
||||
var subCount int64
|
||||
tx.Model(&model.ShopPackageAllocation{}).
|
||||
Where("allocator_shop_id = ? AND package_id = ? AND deleted_at IS NULL", allocation.ShopID, allocation.PackageID).
|
||||
Count(&subCount)
|
||||
if subCount > 0 {
|
||||
skipped = append(skipped, dto.BatchPricingSkipped{
|
||||
AllocationID: allocation.ID,
|
||||
Reason: "存在下级分配记录,请先回收后再修改成本价",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
allocation.CostPrice = newPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := tx.Save(allocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新成本价失败")
|
||||
history := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldPrice,
|
||||
NewCostPrice: newPrice,
|
||||
ChangeReason: req.ChangeReason,
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
if err := tx.Create(history).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建价格历史失败")
|
||||
}
|
||||
|
||||
allocation.CostPrice = newPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := tx.Save(allocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新成本价失败")
|
||||
}
|
||||
}
|
||||
|
||||
affectedIDs = append(affectedIDs, allocation.ID)
|
||||
@@ -112,6 +161,7 @@ func (s *Service) BatchUpdatePricing(ctx context.Context, req *dto.BatchUpdateCo
|
||||
return &dto.BatchUpdateCostPriceResponse{
|
||||
UpdatedCount: updatedCount,
|
||||
AffectedIDs: affectedIDs,
|
||||
Skipped: skipped,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -159,13 +159,13 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
|
||||
// 合并全局 operator 和代理 amount
|
||||
tiers := make([]dto.GrantCommissionTierItem, 0, len(config.Tiers))
|
||||
for _, globalTier := range config.Tiers {
|
||||
tiers = append(tiers, dto.GrantCommissionTierItem{
|
||||
Operator: globalTier.Operator,
|
||||
Dimension: globalTier.Dimension,
|
||||
StatScope: globalTier.StatScope,
|
||||
Threshold: globalTier.Threshold,
|
||||
Amount: agentAmountMap[globalTier.Threshold],
|
||||
})
|
||||
tiers = append(tiers, dto.GrantCommissionTierItem{
|
||||
Operator: globalTier.Operator,
|
||||
Dimension: globalTier.Dimension,
|
||||
StatScope: globalTier.StatScope,
|
||||
Threshold: globalTier.Threshold,
|
||||
Amount: agentAmountMap[globalTier.Threshold],
|
||||
})
|
||||
}
|
||||
resp.CommissionTiers = tiers
|
||||
}
|
||||
@@ -218,7 +218,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "检查授权重复失败")
|
||||
}
|
||||
if exists {
|
||||
return nil, errors.New(errors.CodeConflict, "该代理已存在此系列授权")
|
||||
return nil, errors.New(errors.CodeConflict, "该代理已存在此系列授权")
|
||||
}
|
||||
|
||||
// 3. 确定 allocatorShopID(代理操作者必须自己有授权才能向下分配)
|
||||
@@ -332,6 +332,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.StatusEnabled,
|
||||
@@ -341,7 +342,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
||||
if err := txPkgStore.Create(ctx, pkgAlloc); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建套餐分配失败")
|
||||
}
|
||||
// 写成本价历史
|
||||
_ = txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: pkgAlloc.ID,
|
||||
OldCostPrice: 0,
|
||||
@@ -632,6 +632,16 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
|
||||
if findErr == nil {
|
||||
// 已有记录:更新成本价并写历史
|
||||
oldPrice := existing.CostPrice
|
||||
if oldPrice != item.CostPrice {
|
||||
// cost_price 锁定检查:存在下级分配记录时禁止修改
|
||||
var subCount int64
|
||||
tx.Model(&model.ShopPackageAllocation{}).
|
||||
Where("allocator_shop_id = ? AND package_id = ? AND deleted_at IS NULL", allocation.ShopID, item.PackageID).
|
||||
Count(&subCount)
|
||||
if subCount > 0 {
|
||||
return errors.New(errors.CodeForbidden, "存在下级分配记录,请先回收后再修改成本价")
|
||||
}
|
||||
}
|
||||
existing.CostPrice = item.CostPrice
|
||||
existing.Updater = operatorID
|
||||
if updateErr := txPkgStore.Update(ctx, existing); updateErr != nil {
|
||||
@@ -648,24 +658,22 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// W1: 校验套餐归属于该系列,防止跨系列套餐混入
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, item.PackageID)
|
||||
if pkgErr != nil || pkg.SeriesID != allocation.SeriesID {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
|
||||
}
|
||||
// W2: 代理操作时,校验分配者已拥有此套餐授权,防止越权分配
|
||||
if allocation.AllocatorShopID > 0 {
|
||||
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocation.AllocatorShopID, item.PackageID)
|
||||
if authErr != nil {
|
||||
return errors.New(errors.CodeForbidden, "无权限分配该套餐")
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, item.PackageID)
|
||||
if pkgErr != nil || pkg.SeriesID != allocation.SeriesID {
|
||||
return errors.New(errors.CodeInvalidParam, "套餐不属于该系列,无法添加到此授权")
|
||||
}
|
||||
if allocation.AllocatorShopID > 0 {
|
||||
_, authErr := s.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, allocation.AllocatorShopID, item.PackageID)
|
||||
if authErr != nil {
|
||||
return errors.New(errors.CodeForbidden, "无权限分配该套餐")
|
||||
}
|
||||
}
|
||||
}
|
||||
// 新建分配
|
||||
pkgAlloc := &model.ShopPackageAllocation{
|
||||
ShopID: allocation.ShopID,
|
||||
PackageID: item.PackageID,
|
||||
AllocatorShopID: allocation.AllocatorShopID,
|
||||
CostPrice: item.CostPrice,
|
||||
RetailPrice: pkg.SuggestedRetailPrice,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.StatusEnabled,
|
||||
|
||||
Reference in New Issue
Block a user