refactor: 一次性佣金配置从套餐级别提升到系列级别
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
主要变更: - 新增 tb_shop_series_allocation 表,存储系列级别的一次性佣金配置 - ShopPackageAllocation 移除 one_time_commission_amount 字段 - PackageSeries 新增 enable_one_time_commission 字段控制是否启用一次性佣金 - 新增 /api/admin/shop-series-allocations CRUD 接口 - 佣金计算逻辑改为从 ShopSeriesAllocation 获取一次性佣金金额 - 删除废弃的 ShopSeriesOneTimeCommissionTier 模型 - OpenAPI Tag '系列分配' 和 '单套餐分配' 合并为 '套餐分配' 迁移脚本: - 000042: 重构佣金套餐模型 - 000043: 简化佣金分配 - 000044: 一次性佣金分配重构 - 000045: PackageSeries 添加 enable_one_time_commission 字段 测试: - 新增验收测试 (shop_series_allocation, commission_calculation) - 新增流程测试 (one_time_commission_chain) - 删除过时的单元测试(已被验收测试覆盖)
This commit is contained in:
@@ -72,7 +72,7 @@ type services struct {
|
||||
}
|
||||
|
||||
func initServices(s *stores, deps *Dependencies) *services {
|
||||
purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package, s.ShopSeriesAllocation)
|
||||
purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package)
|
||||
accountAudit := accountAuditSvc.NewService(s.AccountOperationLog)
|
||||
account := accountSvc.New(s.Account, s.Role, s.AccountRole, s.ShopRole, s.Shop, s.Enterprise, accountAudit)
|
||||
|
||||
@@ -91,8 +91,9 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
deps.DB,
|
||||
s.CommissionRecord,
|
||||
s.Shop,
|
||||
s.ShopPackageAllocation,
|
||||
s.ShopSeriesAllocation,
|
||||
s.ShopSeriesOneTimeCommissionTier,
|
||||
s.PackageSeries,
|
||||
s.IotCard,
|
||||
s.Device,
|
||||
s.Wallet,
|
||||
@@ -100,6 +101,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
s.Order,
|
||||
s.OrderItem,
|
||||
s.Package,
|
||||
s.ShopSeriesCommissionStats,
|
||||
commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
deps.Logger,
|
||||
),
|
||||
@@ -108,21 +110,21 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
EnterpriseDevice: enterpriseDeviceSvc.New(deps.DB, s.Enterprise, s.Device, s.DeviceSimBinding, s.EnterpriseDeviceAuthorization, s.EnterpriseCardAuthorization, deps.Logger),
|
||||
Authorization: enterpriseCardSvc.NewAuthorizationService(s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger),
|
||||
MyCommission: myCommissionSvc.New(deps.DB, s.Shop, s.Wallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.WalletTransaction),
|
||||
IotCard: iotCardSvc.New(deps.DB, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, deps.Logger),
|
||||
IotCard: iotCardSvc.New(deps.DB, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, deps.Logger),
|
||||
IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient),
|
||||
Device: deviceSvc.New(deps.DB, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopSeriesAllocation, s.PackageSeries),
|
||||
Device: deviceSvc.New(deps.DB, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries),
|
||||
DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient),
|
||||
AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account),
|
||||
Carrier: carrierSvc.New(s.Carrier),
|
||||
PackageSeries: packageSeriesSvc.New(s.PackageSeries),
|
||||
Package: packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation, s.ShopSeriesAllocation),
|
||||
ShopSeriesAllocation: shopSeriesAllocationSvc.New(s.ShopSeriesAllocation, s.ShopSeriesAllocationConfig, s.ShopSeriesOneTimeCommissionTier, s.Shop, s.PackageSeries, s.Package),
|
||||
ShopPackageAllocation: shopPackageAllocationSvc.New(s.ShopPackageAllocation, s.ShopSeriesAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package),
|
||||
ShopPackageBatchAllocation: shopPackageBatchAllocationSvc.New(deps.DB, s.Package, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopSeriesAllocationConfig, s.ShopSeriesCommissionStats, s.Shop),
|
||||
Package: packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation),
|
||||
ShopSeriesAllocation: shopSeriesAllocationSvc.New(s.ShopSeriesAllocation, s.ShopPackageAllocation, s.Shop, s.PackageSeries),
|
||||
ShopPackageAllocation: shopPackageAllocationSvc.New(s.ShopPackageAllocation, s.ShopSeriesAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package, s.PackageSeries),
|
||||
ShopPackageBatchAllocation: shopPackageBatchAllocationSvc.New(deps.DB, s.Package, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.Shop),
|
||||
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
|
||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
PurchaseValidation: purchaseValidation,
|
||||
Order: orderSvc.New(deps.DB, s.Order, s.OrderItem, s.Wallet, purchaseValidation, s.ShopSeriesAllocationConfig, s.ShopSeriesAllocation, s.IotCard, s.Device, deps.WechatPayment, deps.QueueClient, deps.Logger),
|
||||
Recharge: rechargeSvc.New(deps.DB, s.Recharge, s.Wallet, s.WalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.CommissionRecord, deps.Logger),
|
||||
Order: orderSvc.New(deps.DB, s.Order, s.OrderItem, s.Wallet, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, deps.WechatPayment, deps.QueueClient, deps.Logger),
|
||||
Recharge: rechargeSvc.New(deps.DB, s.Recharge, s.Wallet, s.WalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, deps.Logger),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ type stores struct {
|
||||
PackageSeries *postgres.PackageSeriesStore
|
||||
Package *postgres.PackageStore
|
||||
ShopSeriesAllocation *postgres.ShopSeriesAllocationStore
|
||||
ShopSeriesOneTimeCommissionTier *postgres.ShopSeriesOneTimeCommissionTierStore
|
||||
ShopSeriesAllocationConfig *postgres.ShopSeriesAllocationConfigStore
|
||||
ShopPackageAllocation *postgres.ShopPackageAllocationStore
|
||||
ShopPackageAllocationPriceHistory *postgres.ShopPackageAllocationPriceHistoryStore
|
||||
ShopSeriesCommissionStats *postgres.ShopSeriesCommissionStatsStore
|
||||
@@ -73,8 +71,6 @@ func initStores(deps *Dependencies) *stores {
|
||||
PackageSeries: postgres.NewPackageSeriesStore(deps.DB),
|
||||
Package: postgres.NewPackageStore(deps.DB),
|
||||
ShopSeriesAllocation: postgres.NewShopSeriesAllocationStore(deps.DB),
|
||||
ShopSeriesOneTimeCommissionTier: postgres.NewShopSeriesOneTimeCommissionTierStore(deps.DB),
|
||||
ShopSeriesAllocationConfig: postgres.NewShopSeriesAllocationConfigStore(deps.DB),
|
||||
ShopPackageAllocation: postgres.NewShopPackageAllocationStore(deps.DB),
|
||||
ShopPackageAllocationPriceHistory: postgres.NewShopPackageAllocationPriceHistoryStore(deps.DB),
|
||||
ShopSeriesCommissionStats: postgres.NewShopSeriesCommissionStatsStore(deps.DB),
|
||||
|
||||
@@ -36,7 +36,7 @@ func (h *ShopSeriesAllocationHandler) Create(c *fiber.Ctx) error {
|
||||
func (h *ShopSeriesAllocationHandler) Get(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
return errors.New(errors.CodeInvalidParam, "无效的系列分配 ID")
|
||||
}
|
||||
|
||||
allocation, err := h.service.Get(c.UserContext(), uint(id))
|
||||
@@ -50,7 +50,7 @@ func (h *ShopSeriesAllocationHandler) Get(c *fiber.Ctx) error {
|
||||
func (h *ShopSeriesAllocationHandler) Update(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
return errors.New(errors.CodeInvalidParam, "无效的系列分配 ID")
|
||||
}
|
||||
|
||||
var req dto.UpdateShopSeriesAllocationRequest
|
||||
@@ -69,7 +69,7 @@ func (h *ShopSeriesAllocationHandler) Update(c *fiber.Ctx) error {
|
||||
func (h *ShopSeriesAllocationHandler) Delete(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
return errors.New(errors.CodeInvalidParam, "无效的系列分配 ID")
|
||||
}
|
||||
|
||||
if err := h.service.Delete(c.UserContext(), uint(id)); err != nil {
|
||||
@@ -92,21 +92,3 @@ func (h *ShopSeriesAllocationHandler) List(c *fiber.Ctx) error {
|
||||
|
||||
return response.SuccessWithPagination(c, allocations, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
func (h *ShopSeriesAllocationHandler) UpdateStatus(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
}
|
||||
|
||||
var req dto.UpdateStatusRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.service.UpdateStatus(c.UserContext(), uint(id), req.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -11,26 +13,126 @@ import (
|
||||
// 通过 shop_id 区分所有权:NULL=平台库存,有值=店铺所有
|
||||
type Device struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
DeviceNo string `gorm:"column:device_no;type:varchar(100);uniqueIndex:idx_device_no,where:deleted_at IS NULL;not null;comment:设备编号(唯一标识)" json:"device_no"`
|
||||
DeviceName string `gorm:"column:device_name;type:varchar(255);comment:设备名称" json:"device_name"`
|
||||
DeviceModel string `gorm:"column:device_model;type:varchar(100);comment:设备型号" json:"device_model"`
|
||||
DeviceType string `gorm:"column:device_type;type:varchar(50);comment:设备类型" json:"device_type"`
|
||||
MaxSimSlots int `gorm:"column:max_sim_slots;type:int;default:4;comment:最大插槽数量(默认4)" json:"max_sim_slots"`
|
||||
Manufacturer string `gorm:"column:manufacturer;type:varchar(255);comment:制造商" json:"manufacturer"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
ShopID *uint `gorm:"column:shop_id;index;comment:店铺ID(NULL=平台库存,有值=店铺所有)" json:"shop_id,omitempty"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
|
||||
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
|
||||
DeviceUsername string `gorm:"column:device_username;type:varchar(100);comment:设备登录用户名" json:"device_username"`
|
||||
DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"`
|
||||
DeviceAPIEndpoint string `gorm:"column:device_api_endpoint;type:varchar(500);comment:设备API端点" json:"device_api_endpoint"`
|
||||
SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"`
|
||||
FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放" json:"first_commission_paid"`
|
||||
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分)" json:"accumulated_recharge"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
DeviceNo string `gorm:"column:device_no;type:varchar(100);uniqueIndex:idx_device_no,where:deleted_at IS NULL;not null;comment:设备编号(唯一标识)" json:"device_no"`
|
||||
DeviceName string `gorm:"column:device_name;type:varchar(255);comment:设备名称" json:"device_name"`
|
||||
DeviceModel string `gorm:"column:device_model;type:varchar(100);comment:设备型号" json:"device_model"`
|
||||
DeviceType string `gorm:"column:device_type;type:varchar(50);comment:设备类型" json:"device_type"`
|
||||
MaxSimSlots int `gorm:"column:max_sim_slots;type:int;default:4;comment:最大插槽数量(默认4)" json:"max_sim_slots"`
|
||||
Manufacturer string `gorm:"column:manufacturer;type:varchar(255);comment:制造商" json:"manufacturer"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
ShopID *uint `gorm:"column:shop_id;index;comment:店铺ID(NULL=平台库存,有值=店铺所有)" json:"shop_id,omitempty"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
|
||||
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
|
||||
DeviceUsername string `gorm:"column:device_username;type:varchar(100);comment:设备登录用户名" json:"device_username"`
|
||||
DevicePasswordEncrypted string `gorm:"column:device_password_encrypted;type:varchar(255);comment:设备登录密码(加密)" json:"device_password_encrypted"`
|
||||
DeviceAPIEndpoint string `gorm:"column:device_api_endpoint;type:varchar(500);comment:设备API端点" json:"device_api_endpoint"`
|
||||
SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"`
|
||||
FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放(废弃,使用按系列追踪)" json:"first_commission_paid"`
|
||||
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分,废弃,使用按系列追踪)" json:"accumulated_recharge"`
|
||||
AccumulatedRechargeBySeriesJSON string `gorm:"column:accumulated_recharge_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的累计充值金额" json:"-"`
|
||||
FirstRechargeTriggeredBySeriesJSON string `gorm:"column:first_recharge_triggered_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的首充触发状态" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Device) TableName() string {
|
||||
return "tb_device"
|
||||
}
|
||||
|
||||
func (d *Device) GetAccumulatedRechargeBySeriesMap() (map[uint]int64, error) {
|
||||
result := make(map[uint]int64)
|
||||
if d.AccumulatedRechargeBySeriesJSON == "" || d.AccumulatedRechargeBySeriesJSON == "{}" {
|
||||
return result, nil
|
||||
}
|
||||
var raw map[string]int64
|
||||
if err := json.Unmarshal([]byte(d.AccumulatedRechargeBySeriesJSON), &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range raw {
|
||||
id, err := strconv.ParseUint(k, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result[uint(id)] = v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *Device) SetAccumulatedRechargeBySeriesMap(m map[uint]int64) error {
|
||||
raw := make(map[string]int64)
|
||||
for k, v := range m {
|
||||
raw[strconv.FormatUint(uint64(k), 10)] = v
|
||||
}
|
||||
data, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.AccumulatedRechargeBySeriesJSON = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) GetAccumulatedRechargeBySeries(seriesID uint) int64 {
|
||||
m, err := d.GetAccumulatedRechargeBySeriesMap()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return m[seriesID]
|
||||
}
|
||||
|
||||
func (d *Device) AddAccumulatedRechargeBySeries(seriesID uint, amount int64) error {
|
||||
m, err := d.GetAccumulatedRechargeBySeriesMap()
|
||||
if err != nil {
|
||||
m = make(map[uint]int64)
|
||||
}
|
||||
m[seriesID] += amount
|
||||
return d.SetAccumulatedRechargeBySeriesMap(m)
|
||||
}
|
||||
|
||||
func (d *Device) GetFirstRechargeTriggeredBySeriesMap() (map[uint]bool, error) {
|
||||
result := make(map[uint]bool)
|
||||
if d.FirstRechargeTriggeredBySeriesJSON == "" || d.FirstRechargeTriggeredBySeriesJSON == "{}" {
|
||||
return result, nil
|
||||
}
|
||||
var raw map[string]bool
|
||||
if err := json.Unmarshal([]byte(d.FirstRechargeTriggeredBySeriesJSON), &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range raw {
|
||||
id, err := strconv.ParseUint(k, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result[uint(id)] = v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *Device) SetFirstRechargeTriggeredBySeriesMap(m map[uint]bool) error {
|
||||
raw := make(map[string]bool)
|
||||
for k, v := range m {
|
||||
raw[strconv.FormatUint(uint64(k), 10)] = v
|
||||
}
|
||||
data, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.FirstRechargeTriggeredBySeriesJSON = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) IsFirstRechargeTriggeredBySeries(seriesID uint) bool {
|
||||
m, err := d.GetFirstRechargeTriggeredBySeriesMap()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return m[seriesID]
|
||||
}
|
||||
|
||||
func (d *Device) SetFirstRechargeTriggeredBySeries(seriesID uint, triggered bool) error {
|
||||
m, err := d.GetFirstRechargeTriggeredBySeriesMap()
|
||||
if err != nil {
|
||||
m = make(map[uint]bool)
|
||||
}
|
||||
m[seriesID] = triggered
|
||||
return d.SetFirstRechargeTriggeredBySeriesMap(m)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,16 @@ package dto
|
||||
|
||||
// CreatePackageRequest 创建套餐请求
|
||||
type CreatePackageRequest struct {
|
||||
PackageCode string `json:"package_code" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"套餐编码"`
|
||||
PackageName string `json:"package_name" validate:"required,min=1,max=255" required:"true" minLength:"1" maxLength:"255" description:"套餐名称"`
|
||||
SeriesID *uint `json:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
PackageType string `json:"package_type" validate:"required,oneof=formal addon" required:"true" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
DurationMonths int `json:"duration_months" validate:"required,min=1,max=120" required:"true" minimum:"1" maximum:"120" description:"套餐时长(月数)"`
|
||||
DataType *string `json:"data_type" validate:"omitempty,oneof=real virtual" description:"流量类型 (real:真流量, virtual:虚流量)"`
|
||||
RealDataMB *int64 `json:"real_data_mb" validate:"omitempty,min=0" minimum:"0" description:"真流量额度(MB)"`
|
||||
VirtualDataMB *int64 `json:"virtual_data_mb" validate:"omitempty,min=0" minimum:"0" description:"虚流量额度(MB)"`
|
||||
DataAmountMB *int64 `json:"data_amount_mb" validate:"omitempty,min=0" minimum:"0" description:"总流量额度(MB)"`
|
||||
Price int64 `json:"price" validate:"required,min=0" required:"true" minimum:"0" description:"套餐价格(分)"`
|
||||
SuggestedCostPrice *int64 `json:"suggested_cost_price" validate:"omitempty,min=0" minimum:"0" description:"建议成本价(分)"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price" validate:"omitempty,min=0" minimum:"0" description:"建议售价(分)"`
|
||||
PackageCode string `json:"package_code" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"套餐编码"`
|
||||
PackageName string `json:"package_name" validate:"required,min=1,max=255" required:"true" minLength:"1" maxLength:"255" description:"套餐名称"`
|
||||
SeriesID *uint `json:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
PackageType string `json:"package_type" validate:"required,oneof=formal addon" required:"true" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
DurationMonths int `json:"duration_months" validate:"required,min=1,max=120" required:"true" minimum:"1" maximum:"120" description:"套餐时长(月数)"`
|
||||
RealDataMB *int64 `json:"real_data_mb" validate:"omitempty,min=0" minimum:"0" description:"真流量额度(MB)"`
|
||||
VirtualDataMB *int64 `json:"virtual_data_mb" validate:"omitempty,min=0" minimum:"0" description:"虚流量额度(MB)"`
|
||||
EnableVirtualData bool `json:"enable_virtual_data" description:"是否启用虚流量"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price" validate:"omitempty,min=0" minimum:"0" description:"建议售价(分)"`
|
||||
CostPrice int64 `json:"cost_price" validate:"required,min=0" required:"true" minimum:"0" description:"成本价(分)"`
|
||||
}
|
||||
|
||||
// UpdatePackageRequest 更新套餐请求
|
||||
@@ -22,13 +20,11 @@ type UpdatePackageRequest struct {
|
||||
SeriesID *uint `json:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
PackageType *string `json:"package_type" validate:"omitempty,oneof=formal addon" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
DurationMonths *int `json:"duration_months" validate:"omitempty,min=1,max=120" minimum:"1" maximum:"120" description:"套餐时长(月数)"`
|
||||
DataType *string `json:"data_type" validate:"omitempty,oneof=real virtual" description:"流量类型 (real:真流量, virtual:虚流量)"`
|
||||
RealDataMB *int64 `json:"real_data_mb" validate:"omitempty,min=0" minimum:"0" description:"真流量额度(MB)"`
|
||||
VirtualDataMB *int64 `json:"virtual_data_mb" validate:"omitempty,min=0" minimum:"0" description:"虚流量额度(MB)"`
|
||||
DataAmountMB *int64 `json:"data_amount_mb" validate:"omitempty,min=0" minimum:"0" description:"总流量额度(MB)"`
|
||||
Price *int64 `json:"price" validate:"omitempty,min=0" minimum:"0" description:"套餐价格(分)"`
|
||||
SuggestedCostPrice *int64 `json:"suggested_cost_price" validate:"omitempty,min=0" minimum:"0" description:"建议成本价(分)"`
|
||||
EnableVirtualData *bool `json:"enable_virtual_data" description:"是否启用虚流量"`
|
||||
SuggestedRetailPrice *int64 `json:"suggested_retail_price" validate:"omitempty,min=0" minimum:"0" description:"建议售价(分)"`
|
||||
CostPrice *int64 `json:"cost_price" validate:"omitempty,min=0" minimum:"0" description:"成本价(分)"`
|
||||
}
|
||||
|
||||
// PackageListRequest 套餐列表请求
|
||||
@@ -61,28 +57,26 @@ type CommissionTierInfo struct {
|
||||
|
||||
// PackageResponse 套餐响应
|
||||
type PackageResponse struct {
|
||||
ID uint `json:"id" description:"套餐ID"`
|
||||
PackageCode string `json:"package_code" description:"套餐编码"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName *string `json:"series_name" description:"套餐系列名称"`
|
||||
PackageType string `json:"package_type" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
DurationMonths int `json:"duration_months" description:"套餐时长(月数)"`
|
||||
DataType string `json:"data_type" description:"流量类型 (real:真流量, virtual:虚流量)"`
|
||||
RealDataMB int64 `json:"real_data_mb" description:"真流量额度(MB)"`
|
||||
VirtualDataMB int64 `json:"virtual_data_mb" description:"虚流量额度(MB)"`
|
||||
DataAmountMB int64 `json:"data_amount_mb" description:"总流量额度(MB)"`
|
||||
Price int64 `json:"price" description:"套餐价格(分)"`
|
||||
SuggestedCostPrice int64 `json:"suggested_cost_price" description:"建议成本价(分)"`
|
||||
SuggestedRetailPrice int64 `json:"suggested_retail_price" description:"建议售价(分)"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
ShelfStatus int `json:"shelf_status" description:"上架状态 (1:上架, 2:下架)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
CostPrice *int64 `json:"cost_price,omitempty" description:"成本价(分,仅代理用户可见)"`
|
||||
ProfitMargin *int64 `json:"profit_margin,omitempty" description:"利润空间(分,仅代理用户可见)"`
|
||||
CurrentCommissionRate string `json:"current_commission_rate,omitempty" description:"当前返佣比例(仅代理用户可见)"`
|
||||
TierInfo *CommissionTierInfo `json:"tier_info,omitempty" description:"梯度返佣信息(仅代理用户可见)"`
|
||||
ID uint `json:"id" description:"套餐ID"`
|
||||
PackageCode string `json:"package_code" description:"套餐编码"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
SeriesID *uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName *string `json:"series_name" description:"套餐系列名称"`
|
||||
PackageType string `json:"package_type" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
DurationMonths int `json:"duration_months" description:"套餐时长(月数)"`
|
||||
RealDataMB int64 `json:"real_data_mb" description:"真流量额度(MB)"`
|
||||
VirtualDataMB int64 `json:"virtual_data_mb" description:"虚流量额度(MB)"`
|
||||
EnableVirtualData bool `json:"enable_virtual_data" description:"是否启用虚流量"`
|
||||
SuggestedRetailPrice int64 `json:"suggested_retail_price" description:"建议售价(分)"`
|
||||
CostPrice int64 `json:"cost_price" description:"成本价(分)"`
|
||||
OneTimeCommissionAmount *int64 `json:"one_time_commission_amount,omitempty" description:"一次性佣金金额(分,代理视角)"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
ShelfStatus int `json:"shelf_status" description:"上架状态 (1:上架, 2:下架)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
ProfitMargin *int64 `json:"profit_margin,omitempty" description:"利润空间(分,仅代理用户可见)"`
|
||||
CurrentCommissionRate string `json:"current_commission_rate,omitempty" description:"当前返佣比例(仅代理用户可见)"`
|
||||
TierInfo *CommissionTierInfo `json:"tier_info,omitempty" description:"梯度返佣信息(仅代理用户可见)"`
|
||||
}
|
||||
|
||||
// UpdatePackageParams 更新套餐聚合参数
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
package dto
|
||||
|
||||
// OneTimeCommissionTierDTO 一次性佣金梯度配置
|
||||
type OneTimeCommissionTierDTO struct {
|
||||
Dimension string `json:"dimension" validate:"required,oneof=sales_count sales_amount" required:"true" description:"统计维度 (sales_count:销量, sales_amount:销售额)"`
|
||||
StatScope string `json:"stat_scope" validate:"omitempty,oneof=self self_and_sub" description:"统计范围 (self:仅自己, self_and_sub:自己+下级)"`
|
||||
Threshold int64 `json:"threshold" validate:"required,min=0" required:"true" minimum:"0" description:"达标阈值"`
|
||||
Amount int64 `json:"amount" validate:"required,min=0" required:"true" minimum:"0" description:"佣金金额(分)"`
|
||||
}
|
||||
|
||||
// SeriesOneTimeCommissionConfigDTO 一次性佣金规则配置
|
||||
type SeriesOneTimeCommissionConfigDTO struct {
|
||||
Enable bool `json:"enable" description:"是否启用一次性佣金"`
|
||||
TriggerType string `json:"trigger_type" validate:"omitempty,oneof=first_recharge accumulated_recharge" description:"触发类型 (first_recharge:首充, accumulated_recharge:累计充值)"`
|
||||
Threshold int64 `json:"threshold" validate:"omitempty,min=0" minimum:"0" description:"触发阈值(分)"`
|
||||
CommissionType string `json:"commission_type" validate:"omitempty,oneof=fixed tiered" description:"佣金类型 (fixed:固定, tiered:梯度)"`
|
||||
CommissionAmount int64 `json:"commission_amount" validate:"omitempty,min=0" minimum:"0" description:"固定佣金金额(分),commission_type=fixed时使用"`
|
||||
Tiers []OneTimeCommissionTierDTO `json:"tiers" validate:"omitempty,dive" description:"梯度配置列表,commission_type=tiered时使用"`
|
||||
ValidityType string `json:"validity_type" validate:"omitempty,oneof=permanent fixed_date relative" description:"时效类型 (permanent:永久, fixed_date:固定日期, relative:相对时长)"`
|
||||
ValidityValue string `json:"validity_value" validate:"omitempty" description:"时效值(日期或月数)"`
|
||||
EnableForceRecharge bool `json:"enable_force_recharge" description:"是否启用强充"`
|
||||
ForceCalcType string `json:"force_calc_type" validate:"omitempty,oneof=fixed dynamic" description:"强充计算类型 (fixed:固定, dynamic:动态)"`
|
||||
ForceAmount int64 `json:"force_amount" validate:"omitempty,min=0" minimum:"0" description:"强充金额(分)"`
|
||||
}
|
||||
|
||||
// CreatePackageSeriesRequest 创建套餐系列请求
|
||||
type CreatePackageSeriesRequest struct {
|
||||
SeriesCode string `json:"series_code" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"系列编码"`
|
||||
SeriesName string `json:"series_name" validate:"required,min=1,max=255" required:"true" minLength:"1" maxLength:"255" description:"系列名称"`
|
||||
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"描述"`
|
||||
SeriesCode string `json:"series_code" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"系列编码"`
|
||||
SeriesName string `json:"series_name" validate:"required,min=1,max=255" required:"true" minLength:"1" maxLength:"255" description:"系列名称"`
|
||||
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"描述"`
|
||||
OneTimeCommissionConfig *SeriesOneTimeCommissionConfigDTO `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金规则配置"`
|
||||
}
|
||||
|
||||
// UpdatePackageSeriesRequest 更新套餐系列请求
|
||||
type UpdatePackageSeriesRequest struct {
|
||||
SeriesName *string `json:"series_name" validate:"omitempty,min=1,max=255" minLength:"1" maxLength:"255" description:"系列名称"`
|
||||
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"描述"`
|
||||
SeriesName *string `json:"series_name" validate:"omitempty,min=1,max=255" minLength:"1" maxLength:"255" description:"系列名称"`
|
||||
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"描述"`
|
||||
OneTimeCommissionConfig *SeriesOneTimeCommissionConfigDTO `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金规则配置"`
|
||||
}
|
||||
|
||||
// PackageSeriesListRequest 套餐系列列表请求
|
||||
type PackageSeriesListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
SeriesName *string `json:"series_name" query:"series_name" validate:"omitempty,max=255" maxLength:"255" description:"系列名称(模糊搜索)"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2" description:"状态 (1:启用, 2:禁用)"`
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
SeriesName *string `json:"series_name" query:"series_name" validate:"omitempty,max=255" maxLength:"255" description:"系列名称(模糊搜索)"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2" description:"状态 (1:启用, 2:禁用)"`
|
||||
EnableOneTimeCommission *bool `json:"enable_one_time_commission" query:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
}
|
||||
|
||||
// UpdatePackageSeriesStatusRequest 更新套餐系列状态请求
|
||||
@@ -28,13 +54,15 @@ type UpdatePackageSeriesStatusRequest struct {
|
||||
|
||||
// PackageSeriesResponse 套餐系列响应
|
||||
type PackageSeriesResponse struct {
|
||||
ID uint `json:"id" description:"系列ID"`
|
||||
SeriesCode string `json:"series_code" description:"系列编码"`
|
||||
SeriesName string `json:"series_name" description:"系列名称"`
|
||||
Description string `json:"description" description:"描述"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
ID uint `json:"id" description:"系列ID"`
|
||||
SeriesCode string `json:"series_code" description:"系列编码"`
|
||||
SeriesName string `json:"series_name" description:"系列名称"`
|
||||
Description string `json:"description" description:"描述"`
|
||||
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
OneTimeCommissionConfig *SeriesOneTimeCommissionConfigDTO `json:"one_time_commission_config,omitempty" description:"一次性佣金规则配置"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// UpdatePackageSeriesParams 更新套餐系列聚合参数
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
package dto
|
||||
|
||||
// CreateShopPackageAllocationRequest 创建单套餐分配请求
|
||||
type CreateShopPackageAllocationRequest struct {
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"被分配的店铺ID"`
|
||||
PackageID uint `json:"package_id" validate:"required" required:"true" description:"套餐ID"`
|
||||
CostPrice int64 `json:"cost_price" validate:"required,min=0" required:"true" minimum:"0" description:"覆盖的成本价(分)"`
|
||||
CostPrice int64 `json:"cost_price" validate:"required,min=0" required:"true" minimum:"0" description:"该代理的成本价(分)"`
|
||||
}
|
||||
|
||||
// UpdateShopPackageAllocationRequest 更新单套餐分配请求
|
||||
type UpdateShopPackageAllocationRequest struct {
|
||||
CostPrice *int64 `json:"cost_price" validate:"omitempty,min=0" minimum:"0" description:"覆盖的成本价(分)"`
|
||||
CostPrice *int64 `json:"cost_price" validate:"omitempty,min=0" minimum:"0" description:"该代理的成本价(分)"`
|
||||
}
|
||||
|
||||
// ShopPackageAllocationListRequest 单套餐分配列表请求
|
||||
type ShopPackageAllocationListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" validate:"omitempty" description:"被分配的店铺ID"`
|
||||
PackageID *uint `json:"package_id" query:"package_id" validate:"omitempty" description:"套餐ID"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2" description:"状态 (1:启用, 2:禁用)"`
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" validate:"omitempty" description:"被分配的店铺ID"`
|
||||
PackageID *uint `json:"package_id" query:"package_id" validate:"omitempty" description:"套餐ID"`
|
||||
SeriesAllocationID *uint `json:"series_allocation_id" query:"series_allocation_id" validate:"omitempty" description:"系列分配ID"`
|
||||
AllocatorShopID *uint `json:"allocator_shop_id" query:"allocator_shop_id" validate:"omitempty" description:"分配者店铺ID"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2" description:"状态 (1:启用, 2:禁用)"`
|
||||
}
|
||||
|
||||
// UpdateShopPackageAllocationStatusRequest 更新单套餐分配状态请求
|
||||
@@ -26,23 +25,25 @@ type UpdateShopPackageAllocationStatusRequest struct {
|
||||
Status int `json:"status" validate:"required,oneof=1 2" required:"true" description:"状态 (1:启用, 2:禁用)"`
|
||||
}
|
||||
|
||||
// ShopPackageAllocationResponse 单套餐分配响应
|
||||
type ShopPackageAllocationResponse struct {
|
||||
ID uint `json:"id" description:"分配ID"`
|
||||
ShopID uint `json:"shop_id" description:"被分配的店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"被分配的店铺名称"`
|
||||
PackageID uint `json:"package_id" description:"套餐ID"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
PackageCode string `json:"package_code" description:"套餐编码"`
|
||||
AllocationID uint `json:"allocation_id" description:"关联的系列分配ID"`
|
||||
CostPrice int64 `json:"cost_price" description:"覆盖的成本价(分)"`
|
||||
CalculatedCostPrice int64 `json:"calculated_cost_price" description:"原计算成本价(分),供参考"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
ID uint `json:"id" description:"分配ID"`
|
||||
ShopID uint `json:"shop_id" description:"被分配的店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"被分配的店铺名称"`
|
||||
PackageID uint `json:"package_id" description:"套餐ID"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
PackageCode string `json:"package_code" description:"套餐编码"`
|
||||
SeriesID uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
SeriesAllocationID *uint `json:"series_allocation_id" description:"关联的系列分配ID"`
|
||||
AllocatorShopID uint `json:"allocator_shop_id" description:"分配者店铺ID,0表示平台分配"`
|
||||
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
|
||||
CostPrice int64 `json:"cost_price" description:"该代理的成本价(分)"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// ShopPackageAllocationPageResult 单套餐分配分页结果
|
||||
// ShopPackageAllocationPageResult 套餐分配分页结果
|
||||
type ShopPackageAllocationPageResult struct {
|
||||
List []*ShopPackageAllocationResponse `json:"list" description:"分配列表"`
|
||||
Total int64 `json:"total" description:"总数"`
|
||||
@@ -51,13 +52,13 @@ type ShopPackageAllocationPageResult struct {
|
||||
TotalPages int `json:"total_pages" description:"总页数"`
|
||||
}
|
||||
|
||||
// UpdateShopPackageAllocationParams 更新单套餐分配聚合参数
|
||||
// UpdateShopPackageAllocationParams 更新套餐分配聚合参数
|
||||
type UpdateShopPackageAllocationParams struct {
|
||||
IDReq
|
||||
UpdateShopPackageAllocationRequest
|
||||
}
|
||||
|
||||
// UpdateShopPackageAllocationStatusParams 更新单套餐分配状态聚合参数
|
||||
// UpdateShopPackageAllocationStatusParams 更新套餐分配状态聚合参数
|
||||
type UpdateShopPackageAllocationStatusParams struct {
|
||||
IDReq
|
||||
UpdateShopPackageAllocationStatusRequest
|
||||
|
||||
@@ -8,15 +8,14 @@ type PriceAdjustment struct {
|
||||
|
||||
// BatchAllocatePackagesRequest 批量分配套餐请求
|
||||
type BatchAllocatePackagesRequest struct {
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"被分配的店铺ID"`
|
||||
SeriesID uint `json:"series_id" validate:"required" required:"true" description:"套餐系列ID"`
|
||||
PriceAdjustment *PriceAdjustment `json:"price_adjustment" validate:"omitempty" description:"可选加价配置"`
|
||||
BaseCommission BaseCommissionConfig `json:"base_commission" validate:"required" required:"true" description:"基础返佣配置"`
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"被分配的店铺ID"`
|
||||
SeriesID uint `json:"series_id" validate:"required" required:"true" description:"套餐系列ID"`
|
||||
PriceAdjustment *PriceAdjustment `json:"price_adjustment" validate:"omitempty" description:"可选加价配置"`
|
||||
OneTimeCommissionAmount *int64 `json:"one_time_commission_amount" validate:"omitempty,min=0" minimum:"0" description:"该代理能拿到的一次性佣金(分)"`
|
||||
}
|
||||
|
||||
// BatchAllocatePackagesResponse 批量分配套餐响应
|
||||
type BatchAllocatePackagesResponse struct {
|
||||
AllocationID uint `json:"allocation_id" description:"系列分配ID"`
|
||||
TotalPackages int `json:"total_packages" description:"总套餐数"`
|
||||
AllocatedCount int `json:"allocated_count" description:"成功分配数量"`
|
||||
SkippedCount int `json:"skipped_count" description:"跳过数量(已存在)"`
|
||||
|
||||
@@ -1,86 +1,58 @@
|
||||
package dto
|
||||
|
||||
// BaseCommissionConfig 基础返佣配置
|
||||
type BaseCommissionConfig struct {
|
||||
Mode string `json:"mode" validate:"required,oneof=fixed percent" required:"true" description:"返佣模式 (fixed:固定金额, percent:百分比)"`
|
||||
Value int64 `json:"value" validate:"required,min=0" required:"true" minimum:"0" description:"返佣值(分或千分比,如200=20%)"`
|
||||
}
|
||||
|
||||
// OneTimeCommissionConfig 一次性佣金配置
|
||||
type OneTimeCommissionConfig struct {
|
||||
Type string `json:"type" validate:"required,oneof=fixed tiered" required:"true" description:"一次性佣金类型 (fixed:固定, tiered:梯度)"`
|
||||
Trigger string `json:"trigger" validate:"required,oneof=single_recharge accumulated_recharge" required:"true" description:"触发条件 (single_recharge:单次充值, accumulated_recharge:累计充值)"`
|
||||
Threshold int64 `json:"threshold" validate:"required,min=1" required:"true" minimum:"1" description:"最低阈值(分)"`
|
||||
Mode string `json:"mode" validate:"omitempty,oneof=fixed percent" description:"返佣模式 (fixed:固定金额, percent:百分比) - 固定类型时必填"`
|
||||
Value int64 `json:"value" validate:"omitempty,min=1" minimum:"1" description:"佣金金额(分)或比例(千分比)- 固定类型时必填"`
|
||||
Tiers []OneTimeCommissionTierEntry `json:"tiers" validate:"omitempty,dive" description:"梯度档位列表 - 梯度类型时必填"`
|
||||
}
|
||||
|
||||
// OneTimeCommissionTierEntry 一次性佣金梯度档位条目
|
||||
type OneTimeCommissionTierEntry struct {
|
||||
TierType string `json:"tier_type" validate:"required,oneof=sales_count sales_amount" required:"true" description:"梯度类型 (sales_count:销量, sales_amount:销售额)"`
|
||||
Threshold int64 `json:"threshold" validate:"required,min=1" required:"true" minimum:"1" description:"梯度阈值(销量或销售额分)"`
|
||||
Mode string `json:"mode" validate:"required,oneof=fixed percent" required:"true" description:"返佣模式 (fixed:固定金额, percent:百分比)"`
|
||||
Value int64 `json:"value" validate:"required,min=1" required:"true" minimum:"1" description:"返佣值(分或千分比)"`
|
||||
}
|
||||
|
||||
// CreateShopSeriesAllocationRequest 创建套餐系列分配请求
|
||||
type CreateShopSeriesAllocationRequest struct {
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"被分配的店铺ID"`
|
||||
SeriesID uint `json:"series_id" validate:"required" required:"true" description:"套餐系列ID"`
|
||||
BaseCommission BaseCommissionConfig `json:"base_commission" validate:"required" required:"true" description:"基础返佣配置"`
|
||||
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金配置(启用一次性佣金时必填)"`
|
||||
EnableForceRecharge *bool `json:"enable_force_recharge,omitempty" description:"是否启用强充(累计充值强充)"`
|
||||
ForceRechargeAmount *int64 `json:"force_recharge_amount,omitempty" description:"强充金额(分,0表示使用阈值金额)"`
|
||||
ForceRechargeTriggerType *int `json:"force_recharge_trigger_type,omitempty" description:"强充触发类型(1:单次充值, 2:累计充值)"`
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"被分配的店铺ID"`
|
||||
SeriesID uint `json:"series_id" validate:"required" required:"true" description:"套餐系列ID"`
|
||||
OneTimeCommissionAmount int64 `json:"one_time_commission_amount" validate:"required,min=0" required:"true" minimum:"0" description:"该代理能拿的一次性佣金金额上限(分)"`
|
||||
EnableOneTimeCommission *bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
OneTimeCommissionTrigger string `json:"one_time_commission_trigger" validate:"omitempty,oneof=first_recharge accumulated_recharge" description:"一次性佣金触发类型 (first_recharge:首次充值, accumulated_recharge:累计充值)"`
|
||||
OneTimeCommissionThreshold *int64 `json:"one_time_commission_threshold" validate:"omitempty,min=0" minimum:"0" description:"一次性佣金触发阈值(分)"`
|
||||
EnableForceRecharge *bool `json:"enable_force_recharge" description:"是否启用强制充值"`
|
||||
ForceRechargeAmount *int64 `json:"force_recharge_amount" validate:"omitempty,min=0" minimum:"0" description:"强制充值金额(分)"`
|
||||
ForceRechargeTriggerType *int `json:"force_recharge_trigger_type" validate:"omitempty,oneof=1 2" description:"强充触发类型 (1:单次充值, 2:累计充值)"`
|
||||
}
|
||||
|
||||
// UpdateShopSeriesAllocationRequest 更新套餐系列分配请求
|
||||
type UpdateShopSeriesAllocationRequest struct {
|
||||
BaseCommission *BaseCommissionConfig `json:"base_commission" validate:"omitempty" description:"基础返佣配置"`
|
||||
EnableOneTimeCommission *bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config" validate:"omitempty" description:"一次性佣金配置"`
|
||||
EnableForceRecharge *bool `json:"enable_force_recharge,omitempty" description:"是否启用强充(累计充值强充)"`
|
||||
ForceRechargeAmount *int64 `json:"force_recharge_amount,omitempty" description:"强充金额(分,0表示使用阈值金额)"`
|
||||
ForceRechargeTriggerType *int `json:"force_recharge_trigger_type,omitempty" description:"强充触发类型(1:单次充值, 2:累计充值)"`
|
||||
OneTimeCommissionAmount *int64 `json:"one_time_commission_amount" validate:"omitempty,min=0" minimum:"0" description:"该代理能拿的一次性佣金金额上限(分)"`
|
||||
EnableOneTimeCommission *bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
OneTimeCommissionTrigger *string `json:"one_time_commission_trigger" validate:"omitempty,oneof=first_recharge accumulated_recharge" description:"一次性佣金触发类型"`
|
||||
OneTimeCommissionThreshold *int64 `json:"one_time_commission_threshold" validate:"omitempty,min=0" minimum:"0" description:"一次性佣金触发阈值(分)"`
|
||||
EnableForceRecharge *bool `json:"enable_force_recharge" description:"是否启用强制充值"`
|
||||
ForceRechargeAmount *int64 `json:"force_recharge_amount" validate:"omitempty,min=0" minimum:"0" description:"强制充值金额(分)"`
|
||||
ForceRechargeTriggerType *int `json:"force_recharge_trigger_type" validate:"omitempty,oneof=1 2" description:"强充触发类型 (1:单次充值, 2:累计充值)"`
|
||||
Status *int `json:"status" validate:"omitempty,oneof=1 2" description:"状态 (1:启用, 2:禁用)"`
|
||||
}
|
||||
|
||||
// ShopSeriesAllocationListRequest 套餐系列分配列表请求
|
||||
type ShopSeriesAllocationListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" validate:"omitempty" description:"被分配的店铺ID"`
|
||||
SeriesID *uint `json:"series_id" query:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2" description:"状态 (1:启用, 2:禁用)"`
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" validate:"omitempty" description:"被分配的店铺ID"`
|
||||
SeriesID *uint `json:"series_id" query:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
AllocatorShopID *uint `json:"allocator_shop_id" query:"allocator_shop_id" validate:"omitempty" description:"分配者店铺ID"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,oneof=1 2" description:"状态 (1:启用, 2:禁用)"`
|
||||
}
|
||||
|
||||
// UpdateShopSeriesAllocationStatusRequest 更新套餐系列分配状态请求
|
||||
type UpdateShopSeriesAllocationStatusRequest struct {
|
||||
Status int `json:"status" validate:"required,oneof=1 2" required:"true" description:"状态 (1:启用, 2:禁用)"`
|
||||
}
|
||||
|
||||
// ShopSeriesAllocationResponse 套餐系列分配响应
|
||||
type ShopSeriesAllocationResponse struct {
|
||||
ID uint `json:"id" description:"分配ID"`
|
||||
ShopID uint `json:"shop_id" description:"被分配的店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"被分配的店铺名称"`
|
||||
SeriesID uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
AllocatorShopID uint `json:"allocator_shop_id" description:"分配者店铺ID"`
|
||||
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
|
||||
BaseCommission BaseCommissionConfig `json:"base_commission" description:"基础返佣配置"`
|
||||
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
OneTimeCommissionConfig *OneTimeCommissionConfig `json:"one_time_commission_config,omitempty" description:"一次性佣金配置"`
|
||||
EnableForceRecharge bool `json:"enable_force_recharge" description:"是否启用强充"`
|
||||
ForceRechargeAmount int64 `json:"force_recharge_amount" description:"强充金额(分)"`
|
||||
ForceRechargeTriggerType int `json:"force_recharge_trigger_type" description:"强充触发类型(1:单次充值, 2:累计充值)"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
ID uint `json:"id" description:"分配ID"`
|
||||
ShopID uint `json:"shop_id" description:"被分配的店铺ID"`
|
||||
ShopName string `json:"shop_name" description:"被分配的店铺名称"`
|
||||
SeriesID uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
SeriesCode string `json:"series_code" description:"套餐系列编码"`
|
||||
AllocatorShopID uint `json:"allocator_shop_id" description:"分配者店铺ID,0表示平台分配"`
|
||||
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
|
||||
OneTimeCommissionAmount int64 `json:"one_time_commission_amount" description:"该代理能拿的一次性佣金金额上限(分)"`
|
||||
EnableOneTimeCommission bool `json:"enable_one_time_commission" description:"是否启用一次性佣金"`
|
||||
OneTimeCommissionTrigger string `json:"one_time_commission_trigger" description:"一次性佣金触发类型"`
|
||||
OneTimeCommissionThreshold int64 `json:"one_time_commission_threshold" description:"一次性佣金触发阈值(分)"`
|
||||
EnableForceRecharge bool `json:"enable_force_recharge" description:"是否启用强制充值"`
|
||||
ForceRechargeAmount int64 `json:"force_recharge_amount" description:"强制充值金额(分)"`
|
||||
ForceRechargeTriggerType int `json:"force_recharge_trigger_type" description:"强充触发类型 (1:单次充值, 2:累计充值)"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// ShopSeriesAllocationPageResult 套餐系列分配分页结果
|
||||
type ShopSeriesAllocationPageResult struct {
|
||||
List []*ShopSeriesAllocationResponse `json:"list" description:"分配列表"`
|
||||
Total int64 `json:"total" description:"总数"`
|
||||
@@ -89,14 +61,7 @@ type ShopSeriesAllocationPageResult struct {
|
||||
TotalPages int `json:"total_pages" description:"总页数"`
|
||||
}
|
||||
|
||||
// UpdateShopSeriesAllocationParams 更新套餐系列分配聚合参数
|
||||
type UpdateShopSeriesAllocationParams struct {
|
||||
IDReq
|
||||
UpdateShopSeriesAllocationRequest
|
||||
}
|
||||
|
||||
// UpdateShopSeriesAllocationStatusParams 更新套餐系列分配状态聚合参数
|
||||
type UpdateShopSeriesAllocationStatusParams struct {
|
||||
IDReq
|
||||
UpdateShopSeriesAllocationStatusRequest
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -11,35 +13,135 @@ import (
|
||||
// 通过 shop_id 区分所有权:NULL=平台所有,有值=店铺所有
|
||||
type IotCard struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
ICCID string `gorm:"column:iccid;type:varchar(20);uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识,电信19位/其他20位)" json:"iccid"`
|
||||
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';not null;comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
|
||||
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);comment:运营商类型(CMCC/CUCC/CTCC/CBN),导入时快照" json:"carrier_type"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);comment:运营商名称,导入时快照" json:"carrier_name"`
|
||||
IMSI string `gorm:"column:imsi;type:varchar(50);comment:IMSI" json:"imsi"`
|
||||
MSISDN string `gorm:"column:msisdn;type:varchar(20);comment:MSISDN(手机号码)" json:"msisdn"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
Supplier string `gorm:"column:supplier;type:varchar(255);comment:供应商" json:"supplier"`
|
||||
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"`
|
||||
DistributePrice int64 `gorm:"column:distribute_price;type:bigint;default:0;comment:分销价(分为单位)" json:"distribute_price"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
|
||||
ShopID *uint `gorm:"column:shop_id;index;comment:店铺ID(NULL=平台所有,有值=店铺所有)" json:"shop_id,omitempty"`
|
||||
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
|
||||
ActivationStatus int `gorm:"column:activation_status;type:int;default:0;not null;comment:激活状态 0-未激活 1-已激活" json:"activation_status"`
|
||||
RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;not null;comment:实名状态 0-未实名 1-已实名(行业卡可以保持0)" json:"real_name_status"`
|
||||
NetworkStatus int `gorm:"column:network_status;type:int;default:0;not null;comment:网络状态 0-停机 1-开机" json:"network_status"`
|
||||
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:累计流量使用(MB)" json:"data_usage_mb"`
|
||||
EnablePolling bool `gorm:"column:enable_polling;type:boolean;default:true;comment:是否参与轮询 true-参与 false-不参与" json:"enable_polling"`
|
||||
LastDataCheckAt *time.Time `gorm:"column:last_data_check_at;comment:最后一次流量检查时间" json:"last_data_check_at"`
|
||||
LastRealNameCheckAt *time.Time `gorm:"column:last_real_name_check_at;comment:最后一次实名检查时间" json:"last_real_name_check_at"`
|
||||
LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"`
|
||||
SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"`
|
||||
FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放" json:"first_commission_paid"`
|
||||
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分)" json:"accumulated_recharge"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
ICCID string `gorm:"column:iccid;type:varchar(20);uniqueIndex:idx_iot_card_iccid,where:deleted_at IS NULL;not null;comment:ICCID(唯一标识,电信19位/其他20位)" json:"iccid"`
|
||||
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';not null;comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
|
||||
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);comment:运营商类型(CMCC/CUCC/CTCC/CBN),导入时快照" json:"carrier_type"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);comment:运营商名称,导入时快照" json:"carrier_name"`
|
||||
IMSI string `gorm:"column:imsi;type:varchar(50);comment:IMSI" json:"imsi"`
|
||||
MSISDN string `gorm:"column:msisdn;type:varchar(20);comment:MSISDN(手机号码)" json:"msisdn"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
Supplier string `gorm:"column:supplier;type:varchar(255);comment:供应商" json:"supplier"`
|
||||
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"`
|
||||
DistributePrice int64 `gorm:"column:distribute_price;type:bigint;default:0;comment:分销价(分为单位)" json:"distribute_price"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-在库 2-已分销 3-已激活 4-已停用" json:"status"`
|
||||
ShopID *uint `gorm:"column:shop_id;index;comment:店铺ID(NULL=平台所有,有值=店铺所有)" json:"shop_id,omitempty"`
|
||||
ActivatedAt *time.Time `gorm:"column:activated_at;comment:激活时间" json:"activated_at"`
|
||||
ActivationStatus int `gorm:"column:activation_status;type:int;default:0;not null;comment:激活状态 0-未激活 1-已激活" json:"activation_status"`
|
||||
RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;not null;comment:实名状态 0-未实名 1-已实名(行业卡可以保持0)" json:"real_name_status"`
|
||||
NetworkStatus int `gorm:"column:network_status;type:int;default:0;not null;comment:网络状态 0-停机 1-开机" json:"network_status"`
|
||||
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:累计流量使用(MB)" json:"data_usage_mb"`
|
||||
EnablePolling bool `gorm:"column:enable_polling;type:boolean;default:true;comment:是否参与轮询 true-参与 false-不参与" json:"enable_polling"`
|
||||
LastDataCheckAt *time.Time `gorm:"column:last_data_check_at;comment:最后一次流量检查时间" json:"last_data_check_at"`
|
||||
LastRealNameCheckAt *time.Time `gorm:"column:last_real_name_check_at;comment:最后一次实名检查时间" json:"last_real_name_check_at"`
|
||||
LastSyncTime *time.Time `gorm:"column:last_sync_time;comment:最后一次与Gateway同步时间" json:"last_sync_time"`
|
||||
SeriesID *uint `gorm:"column:series_id;index;comment:套餐系列ID(关联PackageSeries)" json:"series_id,omitempty"`
|
||||
FirstCommissionPaid bool `gorm:"column:first_commission_paid;type:boolean;default:false;comment:一次性佣金是否已发放(废弃,使用按系列追踪)" json:"first_commission_paid"`
|
||||
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分,废弃,使用按系列追踪)" json:"accumulated_recharge"`
|
||||
AccumulatedRechargeBySeriesJSON string `gorm:"column:accumulated_recharge_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的累计充值金额" json:"-"`
|
||||
FirstRechargeTriggeredBySeriesJSON string `gorm:"column:first_recharge_triggered_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的首充触发状态" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (IotCard) TableName() string {
|
||||
return "tb_iot_card"
|
||||
}
|
||||
|
||||
func (c *IotCard) GetAccumulatedRechargeBySeriesMap() (map[uint]int64, error) {
|
||||
result := make(map[uint]int64)
|
||||
if c.AccumulatedRechargeBySeriesJSON == "" || c.AccumulatedRechargeBySeriesJSON == "{}" {
|
||||
return result, nil
|
||||
}
|
||||
var raw map[string]int64
|
||||
if err := json.Unmarshal([]byte(c.AccumulatedRechargeBySeriesJSON), &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range raw {
|
||||
id, err := strconv.ParseUint(k, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result[uint(id)] = v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *IotCard) SetAccumulatedRechargeBySeriesMap(m map[uint]int64) error {
|
||||
raw := make(map[string]int64)
|
||||
for k, v := range m {
|
||||
raw[strconv.FormatUint(uint64(k), 10)] = v
|
||||
}
|
||||
data, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.AccumulatedRechargeBySeriesJSON = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *IotCard) GetAccumulatedRechargeBySeries(seriesID uint) int64 {
|
||||
m, err := c.GetAccumulatedRechargeBySeriesMap()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return m[seriesID]
|
||||
}
|
||||
|
||||
func (c *IotCard) AddAccumulatedRechargeBySeries(seriesID uint, amount int64) error {
|
||||
m, err := c.GetAccumulatedRechargeBySeriesMap()
|
||||
if err != nil {
|
||||
m = make(map[uint]int64)
|
||||
}
|
||||
m[seriesID] += amount
|
||||
return c.SetAccumulatedRechargeBySeriesMap(m)
|
||||
}
|
||||
|
||||
func (c *IotCard) GetFirstRechargeTriggeredBySeriesMap() (map[uint]bool, error) {
|
||||
result := make(map[uint]bool)
|
||||
if c.FirstRechargeTriggeredBySeriesJSON == "" || c.FirstRechargeTriggeredBySeriesJSON == "{}" {
|
||||
return result, nil
|
||||
}
|
||||
var raw map[string]bool
|
||||
if err := json.Unmarshal([]byte(c.FirstRechargeTriggeredBySeriesJSON), &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range raw {
|
||||
id, err := strconv.ParseUint(k, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result[uint(id)] = v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *IotCard) SetFirstRechargeTriggeredBySeriesMap(m map[uint]bool) error {
|
||||
raw := make(map[string]bool)
|
||||
for k, v := range m {
|
||||
raw[strconv.FormatUint(uint64(k), 10)] = v
|
||||
}
|
||||
data, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.FirstRechargeTriggeredBySeriesJSON = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *IotCard) IsFirstRechargeTriggeredBySeries(seriesID uint) bool {
|
||||
m, err := c.GetFirstRechargeTriggeredBySeriesMap()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return m[seriesID]
|
||||
}
|
||||
|
||||
func (c *IotCard) SetFirstRechargeTriggeredBySeries(seriesID uint, triggered bool) error {
|
||||
m, err := c.GetFirstRechargeTriggeredBySeriesMap()
|
||||
if err != nil {
|
||||
m = make(map[uint]bool)
|
||||
}
|
||||
m[seriesID] = triggered
|
||||
return c.SetFirstRechargeTriggeredBySeriesMap(m)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -10,11 +11,13 @@ import (
|
||||
// 套餐的分组,用于一次性分佣规则配置
|
||||
type PackageSeries struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
SeriesCode string `gorm:"column:series_code;type:varchar(100);uniqueIndex:idx_package_series_code,where:deleted_at IS NULL;not null;comment:系列编码" json:"series_code"`
|
||||
SeriesName string `gorm:"column:series_name;type:varchar(255);not null;comment:系列名称" json:"series_name"`
|
||||
Description string `gorm:"column:description;type:text;comment:描述" json:"description"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
SeriesCode string `gorm:"column:series_code;type:varchar(100);uniqueIndex:idx_package_series_code,where:deleted_at IS NULL;not null;comment:系列编码" json:"series_code"`
|
||||
SeriesName string `gorm:"column:series_name;type:varchar(255);not null;comment:系列名称" json:"series_name"`
|
||||
Description string `gorm:"column:description;type:text;comment:描述" json:"description"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
OneTimeCommissionConfigJSON string `gorm:"column:one_time_commission_config;type:jsonb;default:'{}';comment:一次性佣金规则配置" json:"-"`
|
||||
EnableOneTimeCommission bool `gorm:"column:enable_one_time_commission;default:false;comment:是否启用一次性佣金(顶层字段,支持SQL索引)" json:"enable_one_time_commission"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
@@ -32,13 +35,11 @@ type Package struct {
|
||||
SeriesID uint `gorm:"column:series_id;index;comment:套餐系列ID" json:"series_id"`
|
||||
PackageType string `gorm:"column:package_type;type:varchar(50);not null;comment:套餐类型 formal-正式套餐 addon-附加套餐" json:"package_type"`
|
||||
DurationMonths int `gorm:"column:duration_months;type:int;not null;comment:套餐时长(月数) 1-月套餐 12-年套餐" json:"duration_months"`
|
||||
DataType string `gorm:"column:data_type;type:varchar(20);comment:流量类型 real-真流量 virtual-虚流量" json:"data_type"`
|
||||
RealDataMB int64 `gorm:"column:real_data_mb;type:bigint;default:0;comment:真流量额度(MB)" json:"real_data_mb"`
|
||||
VirtualDataMB int64 `gorm:"column:virtual_data_mb;type:bigint;default:0;comment:虚流量额度(MB,用于停机判断)" json:"virtual_data_mb"`
|
||||
DataAmountMB int64 `gorm:"column:data_amount_mb;type:bigint;default:0;comment:总流量额度(MB)" json:"data_amount_mb"`
|
||||
Price int64 `gorm:"column:price;type:bigint;not null;comment:套餐价格(分为单位)" json:"price"`
|
||||
EnableVirtualData bool `gorm:"column:enable_virtual_data;type:boolean;default:false;not null;comment:是否启用虚流量" json:"enable_virtual_data"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
SuggestedCostPrice int64 `gorm:"column:suggested_cost_price;type:bigint;default:0;comment:建议成本价(分为单位)" json:"suggested_cost_price"`
|
||||
CostPrice int64 `gorm:"column:cost_price;type:bigint;default:0;comment:成本价(分为单位)" json:"cost_price"`
|
||||
SuggestedRetailPrice int64 `gorm:"column:suggested_retail_price;type:bigint;default:0;comment:建议售价(分为单位)" json:"suggested_retail_price"`
|
||||
ShelfStatus int `gorm:"column:shelf_status;type:int;default:2;not null;comment:上架状态 1-上架 2-下架" json:"shelf_status"`
|
||||
}
|
||||
@@ -72,3 +73,68 @@ type PackageUsage struct {
|
||||
func (PackageUsage) TableName() string {
|
||||
return "tb_package_usage"
|
||||
}
|
||||
|
||||
// OneTimeCommissionConfig 一次性佣金规则配置
|
||||
type OneTimeCommissionConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
TriggerType string `json:"trigger_type"`
|
||||
Threshold int64 `json:"threshold"`
|
||||
CommissionType string `json:"commission_type"`
|
||||
CommissionAmount int64 `json:"commission_amount"`
|
||||
Tiers []OneTimeCommissionTier `json:"tiers,omitempty"`
|
||||
ValidityType string `json:"validity_type"`
|
||||
ValidityValue string `json:"validity_value"`
|
||||
EnableForceRecharge bool `json:"enable_force_recharge"`
|
||||
ForceCalcType string `json:"force_calc_type"`
|
||||
ForceAmount int64 `json:"force_amount"`
|
||||
}
|
||||
|
||||
// OneTimeCommissionTier 一次性佣金梯度配置
|
||||
type OneTimeCommissionTier struct {
|
||||
Dimension string `json:"dimension"`
|
||||
StatScope string `json:"stat_scope"`
|
||||
Threshold int64 `json:"threshold"`
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
|
||||
const (
|
||||
OneTimeCommissionTriggerFirstRecharge = "first_recharge"
|
||||
OneTimeCommissionTriggerAccumulatedRecharge = "accumulated_recharge"
|
||||
|
||||
OneTimeCommissionValidityPermanent = "permanent"
|
||||
OneTimeCommissionValidityFixedDate = "fixed_date"
|
||||
OneTimeCommissionValidityRelative = "relative"
|
||||
|
||||
OneTimeCommissionForceCalcFixed = "fixed"
|
||||
OneTimeCommissionForceCalcDynamic = "dynamic"
|
||||
|
||||
OneTimeCommissionStatScopeSelf = "self"
|
||||
OneTimeCommissionStatScopeSelfAndSub = "self_and_sub"
|
||||
|
||||
TierTypeSalesCount = "sales_count"
|
||||
TierTypeSalesAmount = "sales_amount"
|
||||
)
|
||||
|
||||
func (ps *PackageSeries) GetOneTimeCommissionConfig() (*OneTimeCommissionConfig, error) {
|
||||
if ps.OneTimeCommissionConfigJSON == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var config OneTimeCommissionConfig
|
||||
if err := json.Unmarshal([]byte(ps.OneTimeCommissionConfigJSON), &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (ps *PackageSeries) SetOneTimeCommissionConfig(config *OneTimeCommissionConfig) error {
|
||||
if config == nil {
|
||||
ps.OneTimeCommissionConfigJSON = ""
|
||||
return nil
|
||||
}
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ps.OneTimeCommissionConfigJSON = string(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,17 +4,15 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShopPackageAllocation 店铺单套餐分配模型
|
||||
// 用于对单个套餐设置覆盖成本价,优先级高于系列级别的加价计算
|
||||
// 适用于特殊定价场景(如某个套餐给特定代理优惠价)
|
||||
type ShopPackageAllocation struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
ShopID uint `gorm:"column:shop_id;index;not null;comment:被分配的店铺ID" json:"shop_id"`
|
||||
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
|
||||
AllocationID uint `gorm:"column:allocation_id;index;not null;comment:关联的系列分配ID" json:"allocation_id"`
|
||||
CostPrice int64 `gorm:"column:cost_price;type:bigint;not null;comment:覆盖的成本价(分)" json:"cost_price"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
ShopID uint `gorm:"column:shop_id;index;not null;comment:被分配的店铺ID" json:"shop_id"`
|
||||
PackageID uint `gorm:"column:package_id;index;not null;comment:套餐ID" json:"package_id"`
|
||||
AllocatorShopID uint `gorm:"column:allocator_shop_id;index;not null;default:0;comment:分配者店铺ID,0表示平台分配" json:"allocator_shop_id"`
|
||||
CostPrice int64 `gorm:"column:cost_price;type:bigint;not null;comment:该代理的成本价(分)" json:"cost_price"`
|
||||
SeriesAllocationID *uint `gorm:"column:series_allocation_id;index;comment:关联的系列分配ID" json:"series_allocation_id"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -4,59 +4,22 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShopSeriesAllocation 店铺套餐系列分配模型
|
||||
// 记录上级店铺为下级店铺分配的套餐系列,包含基础返佣配置和梯度返佣开关
|
||||
// 分配者只能分配自己已被分配的套餐系列,且只能分配给直属下级
|
||||
type ShopSeriesAllocation struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
ShopID uint `gorm:"column:shop_id;index;not null;comment:被分配的店铺ID" json:"shop_id"`
|
||||
SeriesID uint `gorm:"column:series_id;index;not null;comment:套餐系列ID" json:"series_id"`
|
||||
AllocatorShopID uint `gorm:"column:allocator_shop_id;index;not null;comment:分配者店铺ID(上级)" json:"allocator_shop_id"`
|
||||
BaseCommissionMode string `gorm:"column:base_commission_mode;type:varchar(20);not null;default:percent;comment:基础返佣模式 fixed-固定金额 percent-百分比" json:"base_commission_mode"`
|
||||
BaseCommissionValue int64 `gorm:"column:base_commission_value;type:bigint;not null;default:0;comment:基础返佣值(分或千分比,如200=20%)" json:"base_commission_value"`
|
||||
|
||||
// 一次性佣金配置
|
||||
EnableOneTimeCommission bool `gorm:"column:enable_one_time_commission;type:boolean;not null;default:false;comment:是否启用一次性佣金" json:"enable_one_time_commission"`
|
||||
OneTimeCommissionType string `gorm:"column:one_time_commission_type;type:varchar(20);comment:一次性佣金类型 fixed-固定 tiered-梯度" json:"one_time_commission_type"`
|
||||
OneTimeCommissionTrigger string `gorm:"column:one_time_commission_trigger;type:varchar(30);comment:触发条件 single_recharge-单次充值 accumulated_recharge-累计充值" json:"one_time_commission_trigger"`
|
||||
OneTimeCommissionThreshold int64 `gorm:"column:one_time_commission_threshold;type:bigint;default:0;comment:最低阈值(分)" json:"one_time_commission_threshold"`
|
||||
OneTimeCommissionMode string `gorm:"column:one_time_commission_mode;type:varchar(20);comment:返佣模式 fixed-固定金额 percent-百分比" json:"one_time_commission_mode"`
|
||||
OneTimeCommissionValue int64 `gorm:"column:one_time_commission_value;type:bigint;default:0;comment:佣金金额(分)或比例(千分比)" json:"one_time_commission_value"`
|
||||
|
||||
// 强充配置
|
||||
EnableForceRecharge bool `gorm:"column:enable_force_recharge;type:boolean;default:false;comment:是否启用强充(累计充值时可选)" json:"enable_force_recharge"`
|
||||
ForceRechargeAmount int64 `gorm:"column:force_recharge_amount;type:bigint;default:0;comment:强充金额(分,0表示使用阈值金额)" json:"force_recharge_amount"`
|
||||
ForceRechargeTriggerType int `gorm:"column:force_recharge_trigger_type;type:int;default:2;comment:强充触发类型(1:单次充值, 2:累计充值)" json:"force_recharge_trigger_type"`
|
||||
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
BaseModel `gorm:"embedded"`
|
||||
ShopID uint `gorm:"column:shop_id;index;not null;comment:被分配的店铺ID" json:"shop_id"`
|
||||
SeriesID uint `gorm:"column:series_id;index;not null;comment:套餐系列ID" json:"series_id"`
|
||||
AllocatorShopID uint `gorm:"column:allocator_shop_id;index;not null;default:0;comment:分配者店铺ID,0表示平台分配" json:"allocator_shop_id"`
|
||||
OneTimeCommissionAmount int64 `gorm:"column:one_time_commission_amount;type:bigint;default:0;not null;comment:该代理能拿的一次性佣金金额上限(分)" json:"one_time_commission_amount"`
|
||||
EnableOneTimeCommission bool `gorm:"column:enable_one_time_commission;default:false;not null;comment:是否启用一次性佣金" json:"enable_one_time_commission"`
|
||||
OneTimeCommissionTrigger string `gorm:"column:one_time_commission_trigger;type:varchar(50);comment:一次性佣金触发类型" json:"one_time_commission_trigger"`
|
||||
OneTimeCommissionThreshold int64 `gorm:"column:one_time_commission_threshold;type:bigint;default:0;not null;comment:一次性佣金触发阈值(分)" json:"one_time_commission_threshold"`
|
||||
EnableForceRecharge bool `gorm:"column:enable_force_recharge;default:false;not null;comment:是否启用强制充值" json:"enable_force_recharge"`
|
||||
ForceRechargeAmount int64 `gorm:"column:force_recharge_amount;type:bigint;default:0;not null;comment:强制充值金额(分)" json:"force_recharge_amount"`
|
||||
ForceRechargeTriggerType int `gorm:"column:force_recharge_trigger_type;type:int;default:2;not null;comment:强充触发类型 1-单次充值 2-累计充值" json:"force_recharge_trigger_type"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (ShopSeriesAllocation) TableName() string {
|
||||
return "tb_shop_series_allocation"
|
||||
}
|
||||
|
||||
// 返佣模式常量
|
||||
const (
|
||||
// CommissionModeFixed 固定金额返佣
|
||||
CommissionModeFixed = "fixed"
|
||||
// CommissionModePercent 百分比返佣(千分比)
|
||||
CommissionModePercent = "percent"
|
||||
)
|
||||
|
||||
// 一次性佣金类型常量
|
||||
const (
|
||||
// OneTimeCommissionTypeFixed 固定一次性佣金
|
||||
OneTimeCommissionTypeFixed = "fixed"
|
||||
// OneTimeCommissionTypeTiered 梯度一次性佣金
|
||||
OneTimeCommissionTypeTiered = "tiered"
|
||||
)
|
||||
|
||||
// 一次性佣金触发类型常量
|
||||
const (
|
||||
// OneTimeCommissionTriggerSingleRecharge 单次充值触发
|
||||
OneTimeCommissionTriggerSingleRecharge = "single_recharge"
|
||||
// OneTimeCommissionTriggerAccumulatedRecharge 累计充值触发
|
||||
OneTimeCommissionTriggerAccumulatedRecharge = "accumulated_recharge"
|
||||
)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShopSeriesAllocationConfig 套餐系列分配配置版本模型
|
||||
// 记录返佣配置的历史版本,订单创建时锁定配置版本
|
||||
// 支持配置追溯和数据一致性保障
|
||||
type ShopSeriesAllocationConfig struct {
|
||||
gorm.Model
|
||||
AllocationID uint `gorm:"column:allocation_id;index;not null;comment:关联的分配ID" json:"allocation_id"`
|
||||
Version int `gorm:"column:version;type:int;not null;comment:配置版本号" json:"version"`
|
||||
BaseCommissionMode string `gorm:"column:base_commission_mode;type:varchar(20);not null;comment:基础返佣模式(配置快照)" json:"base_commission_mode"`
|
||||
BaseCommissionValue int64 `gorm:"column:base_commission_value;type:bigint;not null;comment:基础返佣值(配置快照)" json:"base_commission_value"`
|
||||
EffectiveFrom time.Time `gorm:"column:effective_from;type:timestamptz;not null;comment:生效开始时间" json:"effective_from"`
|
||||
EffectiveTo *time.Time `gorm:"column:effective_to;type:timestamptz;comment:生效结束时间(NULL表示当前生效)" json:"effective_to"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (ShopSeriesAllocationConfig) TableName() string {
|
||||
return "tb_shop_series_allocation_config"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShopSeriesOneTimeCommissionTier 一次性佣金梯度配置模型
|
||||
// 记录基于销售业绩的一次性佣金梯度档位
|
||||
// 当系列分配的累计销量或销售额达到不同阈值时,返不同的一次性佣金金额
|
||||
type ShopSeriesOneTimeCommissionTier struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
AllocationID uint `gorm:"column:allocation_id;index;not null;comment:系列分配ID" json:"allocation_id"`
|
||||
TierType string `gorm:"column:tier_type;type:varchar(20);not null;comment:梯度类型 sales_count-销量 sales_amount-销售额" json:"tier_type"`
|
||||
ThresholdValue int64 `gorm:"column:threshold_value;type:bigint;not null;comment:梯度阈值(销量或销售额分)" json:"threshold_value"`
|
||||
CommissionMode string `gorm:"column:commission_mode;type:varchar(20);not null;default:fixed;comment:返佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
|
||||
CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:返佣值(分或千分比)" json:"commission_value"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-停用" json:"status"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (ShopSeriesOneTimeCommissionTier) TableName() string {
|
||||
return "tb_shop_series_one_time_commission_tier"
|
||||
}
|
||||
|
||||
// 梯度类型常量
|
||||
const (
|
||||
// TierTypeSalesCount 销量梯度
|
||||
TierTypeSalesCount = "sales_count"
|
||||
// TierTypeSalesAmount 销售额梯度
|
||||
TierTypeSalesAmount = "sales_amount"
|
||||
)
|
||||
|
||||
// 返佣模式常量在 shop_series_allocation.go 中定义
|
||||
// CommissionModeFixed = "fixed"
|
||||
// CommissionModePercent = "percent"
|
||||
@@ -14,7 +14,7 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
|
||||
Register(allocations, doc, groupPath, "GET", "", handler.List, RouteSpec{
|
||||
Summary: "单套餐分配列表",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.ShopPackageAllocationListRequest),
|
||||
Output: new(dto.ShopPackageAllocationPageResult),
|
||||
Auth: true,
|
||||
@@ -22,7 +22,7 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
|
||||
Register(allocations, doc, groupPath, "POST", "", handler.Create, RouteSpec{
|
||||
Summary: "创建单套餐分配",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.CreateShopPackageAllocationRequest),
|
||||
Output: new(dto.ShopPackageAllocationResponse),
|
||||
Auth: true,
|
||||
@@ -30,7 +30,7 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
|
||||
Register(allocations, doc, groupPath, "GET", "/:id", handler.Get, RouteSpec{
|
||||
Summary: "获取单套餐分配详情",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.ShopPackageAllocationResponse),
|
||||
Auth: true,
|
||||
@@ -38,7 +38,7 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
|
||||
Register(allocations, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
|
||||
Summary: "更新单套餐分配",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.UpdateShopPackageAllocationParams),
|
||||
Output: new(dto.ShopPackageAllocationResponse),
|
||||
Auth: true,
|
||||
@@ -46,7 +46,7 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
|
||||
Register(allocations, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
|
||||
Summary: "删除单套餐分配",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
@@ -54,7 +54,7 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
|
||||
Register(allocations, doc, groupPath, "PUT", "/:id/status", handler.UpdateStatus, RouteSpec{
|
||||
Summary: "更新单套餐分配状态",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.UpdateStatusParams),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
@@ -62,7 +62,7 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
|
||||
Register(allocations, doc, groupPath, "PUT", "/:id/cost-price", handler.UpdateCostPrice, RouteSpec{
|
||||
Summary: "更新单套餐分配成本价",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.ShopPackageAllocationResponse),
|
||||
Auth: true,
|
||||
|
||||
@@ -8,56 +8,47 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
// registerShopSeriesAllocationRoutes 注册套餐系列分配相关路由
|
||||
func registerShopSeriesAllocationRoutes(router fiber.Router, handler *admin.ShopSeriesAllocationHandler, doc *openapi.Generator, basePath string) {
|
||||
allocations := router.Group("/shop-series-allocations")
|
||||
groupPath := basePath + "/shop-series-allocations"
|
||||
|
||||
Register(allocations, doc, groupPath, "GET", "", handler.List, RouteSpec{
|
||||
Summary: "套餐系列分配列表",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Summary: "系列分配列表",
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.ShopSeriesAllocationListRequest),
|
||||
Output: new(dto.ShopSeriesAllocationPageResult),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "POST", "", handler.Create, RouteSpec{
|
||||
Summary: "创建套餐系列分配",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Summary: "创建系列分配",
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.CreateShopSeriesAllocationRequest),
|
||||
Output: new(dto.ShopSeriesAllocationResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "GET", "/:id", handler.Get, RouteSpec{
|
||||
Summary: "获取套餐系列分配详情",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Summary: "获取系列分配详情",
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.ShopSeriesAllocationResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
|
||||
Summary: "更新套餐系列分配",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Summary: "更新系列分配",
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.UpdateShopSeriesAllocationParams),
|
||||
Output: new(dto.ShopSeriesAllocationResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
|
||||
Summary: "删除套餐系列分配",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Summary: "删除系列分配",
|
||||
Tags: []string{"套餐分配"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "PUT", "/:id/status", handler.UpdateStatus, RouteSpec{
|
||||
Summary: "更新套餐系列分配状态",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Input: new(dto.UpdateStatusParams),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,34 +9,36 @@ import (
|
||||
"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/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
commissionRecordStore *postgres.CommissionRecordStore
|
||||
shopStore *postgres.ShopStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
shopSeriesOneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
walletStore *postgres.WalletStore
|
||||
walletTransactionStore *postgres.WalletTransactionStore
|
||||
orderStore *postgres.OrderStore
|
||||
orderItemStore *postgres.OrderItemStore
|
||||
packageStore *postgres.PackageStore
|
||||
commissionStatsService *commission_stats.Service
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
commissionRecordStore *postgres.CommissionRecordStore
|
||||
shopStore *postgres.ShopStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
walletStore *postgres.WalletStore
|
||||
walletTransactionStore *postgres.WalletTransactionStore
|
||||
orderStore *postgres.OrderStore
|
||||
orderItemStore *postgres.OrderItemStore
|
||||
packageStore *postgres.PackageStore
|
||||
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
commissionStatsService *commission_stats.Service
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
commissionRecordStore *postgres.CommissionRecordStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
shopSeriesOneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
walletStore *postgres.WalletStore,
|
||||
@@ -44,24 +46,27 @@ func New(
|
||||
orderStore *postgres.OrderStore,
|
||||
orderItemStore *postgres.OrderItemStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore,
|
||||
commissionStatsService *commission_stats.Service,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
commissionRecordStore: commissionRecordStore,
|
||||
shopStore: shopStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
shopSeriesOneTimeCommissionTierStore: shopSeriesOneTimeCommissionTierStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
walletStore: walletStore,
|
||||
walletTransactionStore: walletTransactionStore,
|
||||
orderStore: orderStore,
|
||||
orderItemStore: orderItemStore,
|
||||
packageStore: packageStore,
|
||||
commissionStatsService: commissionStatsService,
|
||||
logger: logger,
|
||||
db: db,
|
||||
commissionRecordStore: commissionRecordStore,
|
||||
shopStore: shopStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
walletStore: walletStore,
|
||||
walletTransactionStore: walletTransactionStore,
|
||||
orderStore: orderStore,
|
||||
orderItemStore: orderItemStore,
|
||||
packageStore: packageStore,
|
||||
commissionStatsStore: commissionStatsStore,
|
||||
commissionStatsService: commissionStatsService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +151,14 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
})
|
||||
}
|
||||
|
||||
// 获取订单明细以获取套餐ID(用于成本价查询)
|
||||
orderItems, err := s.orderItemStore.ListByOrderID(ctx, order.ID)
|
||||
if err != nil || len(orderItems) == 0 {
|
||||
s.logger.Warn("获取订单明细失败或订单无明细,跳过成本价差佣金计算", zap.Uint("order_id", order.ID), zap.Error(err))
|
||||
return records, nil
|
||||
}
|
||||
packageID := orderItems[0].PackageID
|
||||
|
||||
childCostPrice := order.SellerCostPrice
|
||||
currentShopID := sellerShop.ParentID
|
||||
|
||||
@@ -156,13 +169,13 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
break
|
||||
}
|
||||
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, currentShop.ID, *order.SeriesID)
|
||||
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, currentShop.ID, packageID)
|
||||
if err != nil {
|
||||
s.logger.Warn("上级店铺未分配该系列,跳过", zap.Uint("shop_id", currentShop.ID), zap.Uint("series_id", *order.SeriesID))
|
||||
s.logger.Warn("上级店铺未分配该套餐,跳过", zap.Uint("shop_id", currentShop.ID), zap.Uint("package_id", packageID))
|
||||
break
|
||||
}
|
||||
|
||||
myCostPrice := s.calculateCostPrice(allocation, order.TotalAmount)
|
||||
myCostPrice := allocation.CostPrice
|
||||
profit := childCostPrice - myCostPrice
|
||||
if profit > 0 {
|
||||
records = append(records, &model.CommissionRecord{
|
||||
@@ -187,12 +200,7 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (s *Service) calculateCostPrice(allocation *model.ShopSeriesAllocation, orderAmount int64) int64 {
|
||||
return utils.CalculateCostPrice(allocation, orderAmount)
|
||||
}
|
||||
|
||||
func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *gorm.DB, order *model.Order, cardID uint) error {
|
||||
// 代购订单不触发一次性佣金和累计充值更新
|
||||
if order.IsPurchaseOnBehalf {
|
||||
return nil
|
||||
}
|
||||
@@ -206,79 +214,64 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
|
||||
return nil
|
||||
}
|
||||
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *card.ShopID, *card.SeriesID)
|
||||
seriesID := *card.SeriesID
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, seriesID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "获取系列分配失败")
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "获取套餐系列失败")
|
||||
}
|
||||
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
config, err := series.GetOneTimeCommissionConfig()
|
||||
if err != nil || config == nil || !config.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||
newAccumulated := card.AccumulatedRecharge + order.TotalAmount
|
||||
if s.isOneTimeCommissionExpired(config, card.ActivatedAt) {
|
||||
s.logger.Info("一次性佣金规则已过期,跳过",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.Uint("series_id", seriesID),
|
||||
zap.String("validity_type", config.ValidityType))
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
|
||||
accumulatedBySeries := card.GetAccumulatedRechargeBySeries(seriesID)
|
||||
newAccumulated := accumulatedBySeries + order.TotalAmount
|
||||
card.AddAccumulatedRechargeBySeries(seriesID, order.TotalAmount)
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
|
||||
Update("accumulated_recharge", newAccumulated).Error; err != nil {
|
||||
Update("accumulated_recharge_by_series", card.AccumulatedRechargeBySeriesJSON).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡累计充值金额失败")
|
||||
}
|
||||
card.AccumulatedRecharge = newAccumulated
|
||||
|
||||
if card.IsFirstRechargeTriggeredBySeries(seriesID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if newAccumulated < config.Threshold {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if card.FirstCommissionPaid {
|
||||
if card.IsFirstRechargeTriggeredBySeries(seriesID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rechargeAmount int64
|
||||
switch allocation.OneTimeCommissionTrigger {
|
||||
case model.OneTimeCommissionTriggerSingleRecharge:
|
||||
rechargeAmount = order.TotalAmount
|
||||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||||
rechargeAmount = card.AccumulatedRecharge
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if rechargeAmount < allocation.OneTimeCommissionThreshold {
|
||||
return nil
|
||||
}
|
||||
|
||||
commissionAmount, err := s.calculateOneTimeCommission(ctx, allocation, order.TotalAmount)
|
||||
records, err := s.calculateChainOneTimeCommission(ctx, *card.ShopID, seriesID, order, &cardID, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "计算一次性佣金失败")
|
||||
return err
|
||||
}
|
||||
|
||||
if commissionAmount <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if card.ShopID == nil {
|
||||
return errors.New(errors.CodeInvalidParam, "卡未归属任何店铺,无法发放佣金")
|
||||
}
|
||||
|
||||
record := &model.CommissionRecord{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
Updater: order.Updater,
|
||||
},
|
||||
ShopID: *card.ShopID,
|
||||
OrderID: order.ID,
|
||||
IotCardID: &cardID,
|
||||
CommissionSource: model.CommissionSourceOneTime,
|
||||
Amount: commissionAmount,
|
||||
Status: model.CommissionStatusReleased,
|
||||
Remark: "一次性佣金",
|
||||
}
|
||||
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
|
||||
}
|
||||
|
||||
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||
for _, record := range records {
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
|
||||
}
|
||||
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||
}
|
||||
}
|
||||
|
||||
card.SetFirstRechargeTriggeredBySeries(seriesID, true)
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
|
||||
Update("first_commission_paid", true).Error; err != nil {
|
||||
Update("first_recharge_triggered_by_series", card.FirstRechargeTriggeredBySeriesJSON).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
|
||||
}
|
||||
|
||||
@@ -292,7 +285,6 @@ 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 {
|
||||
return nil
|
||||
}
|
||||
@@ -306,79 +298,64 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
|
||||
return nil
|
||||
}
|
||||
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *device.ShopID, *device.SeriesID)
|
||||
seriesID := *device.SeriesID
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, seriesID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "获取系列分配失败")
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "获取套餐系列失败")
|
||||
}
|
||||
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
config, err := series.GetOneTimeCommissionConfig()
|
||||
if err != nil || config == nil || !config.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||
newAccumulated := device.AccumulatedRecharge + order.TotalAmount
|
||||
if s.isOneTimeCommissionExpired(config, device.ActivatedAt) {
|
||||
s.logger.Info("一次性佣金规则已过期,跳过",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Uint("series_id", seriesID),
|
||||
zap.String("validity_type", config.ValidityType))
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
|
||||
accumulatedBySeries := device.GetAccumulatedRechargeBySeries(seriesID)
|
||||
newAccumulated := accumulatedBySeries + order.TotalAmount
|
||||
device.AddAccumulatedRechargeBySeries(seriesID, order.TotalAmount)
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).
|
||||
Update("accumulated_recharge", newAccumulated).Error; err != nil {
|
||||
Update("accumulated_recharge_by_series", device.AccumulatedRechargeBySeriesJSON).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备累计充值金额失败")
|
||||
}
|
||||
device.AccumulatedRecharge = newAccumulated
|
||||
|
||||
if device.IsFirstRechargeTriggeredBySeries(seriesID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if newAccumulated < config.Threshold {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if device.FirstCommissionPaid {
|
||||
if device.IsFirstRechargeTriggeredBySeries(seriesID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rechargeAmount int64
|
||||
switch allocation.OneTimeCommissionTrigger {
|
||||
case model.OneTimeCommissionTriggerSingleRecharge:
|
||||
rechargeAmount = order.TotalAmount
|
||||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||||
rechargeAmount = device.AccumulatedRecharge
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if rechargeAmount < allocation.OneTimeCommissionThreshold {
|
||||
return nil
|
||||
}
|
||||
|
||||
commissionAmount, err := s.calculateOneTimeCommission(ctx, allocation, order.TotalAmount)
|
||||
records, err := s.calculateChainOneTimeCommission(ctx, *device.ShopID, seriesID, order, nil, &deviceID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "计算一次性佣金失败")
|
||||
return err
|
||||
}
|
||||
|
||||
if commissionAmount <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if device.ShopID == nil {
|
||||
return errors.New(errors.CodeInvalidParam, "设备未归属任何店铺,无法发放佣金")
|
||||
}
|
||||
|
||||
record := &model.CommissionRecord{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
Updater: order.Updater,
|
||||
},
|
||||
ShopID: *device.ShopID,
|
||||
OrderID: order.ID,
|
||||
DeviceID: &deviceID,
|
||||
CommissionSource: model.CommissionSourceOneTime,
|
||||
Amount: commissionAmount,
|
||||
Status: model.CommissionStatusReleased,
|
||||
Remark: "一次性佣金(设备)",
|
||||
}
|
||||
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
|
||||
}
|
||||
|
||||
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||
for _, record := range records {
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
|
||||
}
|
||||
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||
}
|
||||
}
|
||||
|
||||
device.SetFirstRechargeTriggeredBySeries(seriesID, true)
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).
|
||||
Update("first_commission_paid", true).Error; err != nil {
|
||||
Update("first_recharge_triggered_by_series", device.FirstRechargeTriggeredBySeriesJSON).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败")
|
||||
}
|
||||
|
||||
@@ -391,74 +368,197 @@ func (s *Service) TriggerOneTimeCommissionForDevice(ctx context.Context, order *
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) calculateOneTimeCommission(ctx context.Context, allocation *model.ShopSeriesAllocation, orderAmount int64) (int64, error) {
|
||||
switch allocation.OneTimeCommissionType {
|
||||
case model.OneTimeCommissionTypeFixed:
|
||||
return s.calculateFixedCommission(allocation.OneTimeCommissionMode, allocation.OneTimeCommissionValue, orderAmount), nil
|
||||
case model.OneTimeCommissionTypeTiered:
|
||||
return s.calculateTieredCommission(ctx, allocation.ID, orderAmount)
|
||||
func (s *Service) isOneTimeCommissionExpired(config *model.OneTimeCommissionConfig, activatedAt *time.Time) bool {
|
||||
if config == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
switch config.ValidityType {
|
||||
case model.OneTimeCommissionValidityPermanent:
|
||||
return false
|
||||
|
||||
case model.OneTimeCommissionValidityFixedDate:
|
||||
if config.ValidityValue == "" {
|
||||
return false
|
||||
}
|
||||
expiryDate, err := time.Parse("2006-01-02", config.ValidityValue)
|
||||
if err != nil {
|
||||
s.logger.Warn("解析一次性佣金到期日期失败",
|
||||
zap.String("validity_value", config.ValidityValue),
|
||||
zap.Error(err))
|
||||
return false
|
||||
}
|
||||
expiryDate = expiryDate.Add(24*time.Hour - time.Second)
|
||||
return now.After(expiryDate)
|
||||
|
||||
case model.OneTimeCommissionValidityRelative:
|
||||
if activatedAt == nil {
|
||||
return false
|
||||
}
|
||||
if config.ValidityValue == "" {
|
||||
return false
|
||||
}
|
||||
months := 0
|
||||
if _, err := fmt.Sscanf(config.ValidityValue, "%d", &months); err != nil || months <= 0 {
|
||||
s.logger.Warn("解析一次性佣金相对时长失败",
|
||||
zap.String("validity_value", config.ValidityValue),
|
||||
zap.Error(err))
|
||||
return false
|
||||
}
|
||||
expiryTime := activatedAt.AddDate(0, months, 0)
|
||||
return now.After(expiryTime)
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) calculateFixedCommission(mode string, value int64, orderAmount int64) int64 {
|
||||
if mode == model.CommissionModeFixed {
|
||||
return value
|
||||
} else if mode == model.CommissionModePercent {
|
||||
return orderAmount * value / 1000
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func (s *Service) calculateChainOneTimeCommission(ctx context.Context, bottomShopID uint, seriesID uint, order *model.Order, cardID *uint, deviceID *uint) ([]*model.CommissionRecord, error) {
|
||||
var records []*model.CommissionRecord
|
||||
|
||||
func (s *Service) calculateTieredCommission(ctx context.Context, allocationID uint, orderAmount int64) (int64, error) {
|
||||
tiers, err := s.shopSeriesOneTimeCommissionTierStore.ListByAllocationID(ctx, allocationID)
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, seriesID)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "获取梯度配置失败")
|
||||
s.logger.Warn("获取套餐系列失败,跳过一次性佣金", zap.Uint("series_id", seriesID), zap.Error(err))
|
||||
return records, nil
|
||||
}
|
||||
|
||||
config, err := series.GetOneTimeCommissionConfig()
|
||||
if err != nil || config == nil || !config.Enable {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
bottomSeriesAllocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, bottomShopID, seriesID)
|
||||
if err != nil {
|
||||
s.logger.Warn("底层店铺未分配该系列,跳过一次性佣金", zap.Uint("shop_id", bottomShopID), zap.Uint("series_id", seriesID))
|
||||
return records, nil
|
||||
}
|
||||
|
||||
bottomShop, err := s.shopStore.GetByID(ctx, bottomShopID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "获取店铺信息失败")
|
||||
}
|
||||
|
||||
childAmountGiven := int64(0)
|
||||
currentShopID := bottomShopID
|
||||
currentShop := bottomShop
|
||||
currentSeriesAllocation := bottomSeriesAllocation
|
||||
|
||||
for {
|
||||
var myAmount int64
|
||||
|
||||
if config.CommissionType == "tiered" && len(config.Tiers) > 0 {
|
||||
tieredAmount, tierErr := s.matchOneTimeCommissionTier(ctx, currentShopID, seriesID, currentSeriesAllocation.ID, config.Tiers)
|
||||
if tierErr != nil {
|
||||
s.logger.Warn("匹配梯度佣金失败,使用固定金额", zap.Uint("shop_id", currentShopID), zap.Error(tierErr))
|
||||
myAmount = currentSeriesAllocation.OneTimeCommissionAmount
|
||||
} else {
|
||||
myAmount = tieredAmount
|
||||
}
|
||||
} else {
|
||||
myAmount = currentSeriesAllocation.OneTimeCommissionAmount
|
||||
}
|
||||
|
||||
actualProfit := myAmount - childAmountGiven
|
||||
|
||||
if actualProfit > 0 {
|
||||
remark := "一次性佣金"
|
||||
if deviceID != nil {
|
||||
remark = "一次性佣金(设备)"
|
||||
}
|
||||
|
||||
records = append(records, &model.CommissionRecord{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
Updater: order.Updater,
|
||||
},
|
||||
ShopID: currentShopID,
|
||||
OrderID: order.ID,
|
||||
IotCardID: cardID,
|
||||
DeviceID: deviceID,
|
||||
CommissionSource: model.CommissionSourceOneTime,
|
||||
Amount: actualProfit,
|
||||
Status: model.CommissionStatusReleased,
|
||||
Remark: remark,
|
||||
})
|
||||
}
|
||||
|
||||
if currentShop.ParentID == nil || *currentShop.ParentID == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
parentShopID := *currentShop.ParentID
|
||||
parentSeriesAllocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, parentShopID, seriesID)
|
||||
if err != nil {
|
||||
s.logger.Warn("上级店铺未分配该系列,停止链式计算",
|
||||
zap.Uint("parent_shop_id", parentShopID),
|
||||
zap.Uint("series_id", seriesID))
|
||||
break
|
||||
}
|
||||
|
||||
parentShop, err := s.shopStore.GetByID(ctx, parentShopID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取上级店铺失败", zap.Uint("shop_id", parentShopID), zap.Error(err))
|
||||
break
|
||||
}
|
||||
|
||||
childAmountGiven = myAmount
|
||||
currentShopID = parentShopID
|
||||
currentShop = parentShop
|
||||
currentSeriesAllocation = parentSeriesAllocation
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (s *Service) matchOneTimeCommissionTier(ctx context.Context, shopID uint, seriesID uint, allocationID uint, tiers []model.OneTimeCommissionTier) (int64, error) {
|
||||
if len(tiers) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
stats, err := s.commissionStatsService.GetCurrentStats(ctx, allocationID, "all_time")
|
||||
if err != nil {
|
||||
s.logger.Error("获取销售业绩统计失败", zap.Uint("allocation_id", allocationID), zap.Error(err))
|
||||
return 0, nil
|
||||
}
|
||||
now := time.Now()
|
||||
var matchedAmount int64 = 0
|
||||
|
||||
if stats == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var matchedTier *model.ShopSeriesOneTimeCommissionTier
|
||||
for _, tier := range tiers {
|
||||
var salesValue int64
|
||||
if tier.TierType == model.TierTypeSalesCount {
|
||||
salesValue = stats.TotalSalesCount
|
||||
} else if tier.TierType == model.TierTypeSalesAmount {
|
||||
salesValue = stats.TotalSalesAmount
|
||||
var salesCount, salesAmount int64
|
||||
var err error
|
||||
|
||||
if tier.StatScope == model.OneTimeCommissionStatScopeSelfAndSub {
|
||||
subordinateIDs, subErr := s.shopStore.GetSubordinateShopIDs(ctx, shopID)
|
||||
if subErr != nil {
|
||||
s.logger.Warn("获取下级店铺失败", zap.Uint("shop_id", shopID), zap.Error(subErr))
|
||||
subordinateIDs = []uint{shopID}
|
||||
}
|
||||
|
||||
allocationIDs, allocErr := s.shopSeriesAllocationStore.GetIDsByShopIDsAndSeries(ctx, subordinateIDs, seriesID)
|
||||
if allocErr != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, allocErr, "获取下级分配ID失败")
|
||||
}
|
||||
|
||||
salesCount, salesAmount, err = s.commissionStatsStore.GetAggregatedStats(ctx, allocationIDs, "monthly", now)
|
||||
} else {
|
||||
salesCount, salesAmount, err = s.commissionStatsStore.GetAggregatedStats(ctx, []uint{allocationID}, "monthly", now)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.Warn("获取销售统计失败", zap.Uint("allocation_id", allocationID), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
if salesValue >= tier.ThresholdValue {
|
||||
if matchedTier == nil || tier.ThresholdValue > matchedTier.ThresholdValue {
|
||||
matchedTier = tier
|
||||
}
|
||||
var currentValue int64
|
||||
if tier.Dimension == model.TierTypeSalesCount {
|
||||
currentValue = salesCount
|
||||
} else {
|
||||
currentValue = salesAmount
|
||||
}
|
||||
|
||||
if currentValue >= tier.Threshold && tier.Amount > matchedAmount {
|
||||
matchedAmount = tier.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if matchedTier == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if matchedTier.CommissionMode == model.CommissionModeFixed {
|
||||
return matchedTier.CommissionValue, nil
|
||||
} else if matchedTier.CommissionMode == model.CommissionModePercent {
|
||||
return orderAmount * matchedTier.CommissionValue / 1000, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
return matchedAmount, nil
|
||||
}
|
||||
|
||||
func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record *model.CommissionRecord) error {
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
package commission_calculation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestCalculateCommission_PurchaseOnBehalf(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
shopSeriesOneTimeCommissionTierStore := postgres.NewShopSeriesOneTimeCommissionTierStore(tx)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
deviceStore := postgres.NewDeviceStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
orderStore := postgres.NewOrderStore(tx, rdb)
|
||||
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
statsStore := postgres.NewShopSeriesCommissionStatsStore(tx)
|
||||
commissionStatsService := commission_stats.New(statsStore)
|
||||
|
||||
service := New(
|
||||
tx,
|
||||
commissionRecordStore,
|
||||
shopStore,
|
||||
shopSeriesAllocationStore,
|
||||
shopSeriesOneTimeCommissionTierStore,
|
||||
iotCardStore,
|
||||
deviceStore,
|
||||
walletStore,
|
||||
walletTransactionStore,
|
||||
orderStore,
|
||||
orderItemStore,
|
||||
packageStore,
|
||||
commissionStatsService,
|
||||
zap.NewNop(),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
shop := &model.Shop{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "TEST001",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, tx.Create(shop).Error)
|
||||
|
||||
wallet := &model.Wallet{
|
||||
ResourceType: "shop",
|
||||
ResourceID: shop.ID,
|
||||
WalletType: "commission",
|
||||
Balance: 0,
|
||||
Version: 1,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, tx.Create(wallet).Error)
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
ShopID: shop.ID,
|
||||
SeriesID: 1,
|
||||
AllocatorShopID: 1,
|
||||
BaseCommissionMode: model.CommissionModeFixed,
|
||||
BaseCommissionValue: 5000,
|
||||
EnableOneTimeCommission: true,
|
||||
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerAccumulatedRecharge,
|
||||
OneTimeCommissionThreshold: 10000,
|
||||
OneTimeCommissionType: model.OneTimeCommissionTypeFixed,
|
||||
OneTimeCommissionMode: model.CommissionModeFixed,
|
||||
OneTimeCommissionValue: 1000,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(allocation).Error)
|
||||
|
||||
card := &model.IotCard{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
ICCID: "89860000000000000001",
|
||||
ShopID: &shop.ID,
|
||||
SeriesID: &allocation.SeriesID,
|
||||
AccumulatedRecharge: 0,
|
||||
FirstCommissionPaid: false,
|
||||
}
|
||||
require.NoError(t, tx.Create(card).Error)
|
||||
|
||||
seriesID := allocation.SeriesID
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
isPurchaseOnBehalf bool
|
||||
expectedAccumulatedRecharge int64
|
||||
expectedCommissionRecords int
|
||||
expectedOneTimeCommission bool
|
||||
}{
|
||||
{
|
||||
name: "普通订单_触发累计充值和一次性佣金",
|
||||
isPurchaseOnBehalf: false,
|
||||
expectedAccumulatedRecharge: 15000,
|
||||
expectedCommissionRecords: 2,
|
||||
expectedOneTimeCommission: true,
|
||||
},
|
||||
{
|
||||
name: "代购订单_不触发累计充值和一次性佣金",
|
||||
isPurchaseOnBehalf: true,
|
||||
expectedAccumulatedRecharge: 0,
|
||||
expectedCommissionRecords: 1,
|
||||
expectedOneTimeCommission: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.NoError(t, tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Updates(map[string]interface{}{
|
||||
"accumulated_recharge": 0,
|
||||
"first_commission_paid": false,
|
||||
}).Error)
|
||||
|
||||
require.NoError(t, tx.Where("1=1").Delete(&model.CommissionRecord{}).Error)
|
||||
require.NoError(t, tx.Where("1=1").Delete(&model.Order{}).Error)
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
OrderNo: "ORD" + time.Now().Format("20060102150405"),
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &card.ID,
|
||||
BuyerType: model.BuyerTypeAgent,
|
||||
BuyerID: shop.ID,
|
||||
SellerShopID: &shop.ID,
|
||||
SeriesID: &seriesID,
|
||||
TotalAmount: 15000,
|
||||
SellerCostPrice: 5000,
|
||||
IsPurchaseOnBehalf: tt.isPurchaseOnBehalf,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
PaymentStatus: model.PaymentStatusPaid,
|
||||
}
|
||||
require.NoError(t, tx.Create(order).Error)
|
||||
|
||||
err := service.CalculateCommission(ctx, order.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var updatedCard model.IotCard
|
||||
require.NoError(t, tx.First(&updatedCard, card.ID).Error)
|
||||
assert.Equal(t, tt.expectedAccumulatedRecharge, updatedCard.AccumulatedRecharge, "累计充值金额不符合预期")
|
||||
|
||||
var records []model.CommissionRecord
|
||||
require.NoError(t, tx.Where("order_id = ?", order.ID).Find(&records).Error)
|
||||
assert.Equal(t, tt.expectedCommissionRecords, len(records), "佣金记录数量不符合预期")
|
||||
|
||||
hasOneTimeCommission := false
|
||||
for _, record := range records {
|
||||
if record.CommissionSource == model.CommissionSourceOneTime {
|
||||
hasOneTimeCommission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.expectedOneTimeCommission, hasOneTimeCommission, "一次性佣金触发状态不符合预期")
|
||||
|
||||
if tt.expectedOneTimeCommission {
|
||||
assert.True(t, updatedCard.FirstCommissionPaid, "首次佣金发放标记应为true")
|
||||
} else {
|
||||
assert.False(t, updatedCard.FirstCommissionPaid, "首次佣金发放标记应为false")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateCommission_Device_PurchaseOnBehalf(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
shopSeriesOneTimeCommissionTierStore := postgres.NewShopSeriesOneTimeCommissionTierStore(tx)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
deviceStore := postgres.NewDeviceStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
orderStore := postgres.NewOrderStore(tx, rdb)
|
||||
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
statsStore := postgres.NewShopSeriesCommissionStatsStore(tx)
|
||||
commissionStatsService := commission_stats.New(statsStore)
|
||||
|
||||
service := New(
|
||||
tx,
|
||||
commissionRecordStore,
|
||||
shopStore,
|
||||
shopSeriesAllocationStore,
|
||||
shopSeriesOneTimeCommissionTierStore,
|
||||
iotCardStore,
|
||||
deviceStore,
|
||||
walletStore,
|
||||
walletTransactionStore,
|
||||
orderStore,
|
||||
orderItemStore,
|
||||
packageStore,
|
||||
commissionStatsService,
|
||||
zap.NewNop(),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
shop := &model.Shop{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "TEST002",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000002",
|
||||
}
|
||||
require.NoError(t, tx.Create(shop).Error)
|
||||
|
||||
wallet := &model.Wallet{
|
||||
ResourceType: "shop",
|
||||
ResourceID: shop.ID,
|
||||
WalletType: "commission",
|
||||
Balance: 0,
|
||||
Version: 1,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, tx.Create(wallet).Error)
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
ShopID: shop.ID,
|
||||
SeriesID: 1,
|
||||
AllocatorShopID: 1,
|
||||
BaseCommissionMode: model.CommissionModeFixed,
|
||||
BaseCommissionValue: 5000,
|
||||
EnableOneTimeCommission: true,
|
||||
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerAccumulatedRecharge,
|
||||
OneTimeCommissionThreshold: 10000,
|
||||
OneTimeCommissionType: model.OneTimeCommissionTypeFixed,
|
||||
OneTimeCommissionMode: model.CommissionModeFixed,
|
||||
OneTimeCommissionValue: 1000,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(allocation).Error)
|
||||
|
||||
device := &model.Device{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
DeviceNo: "DEV001",
|
||||
ShopID: &shop.ID,
|
||||
SeriesID: &allocation.SeriesID,
|
||||
AccumulatedRecharge: 0,
|
||||
FirstCommissionPaid: false,
|
||||
}
|
||||
require.NoError(t, tx.Create(device).Error)
|
||||
|
||||
seriesID := allocation.SeriesID
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
isPurchaseOnBehalf bool
|
||||
expectedAccumulatedRecharge int64
|
||||
expectedCommissionRecords int
|
||||
expectedOneTimeCommission bool
|
||||
}{
|
||||
{
|
||||
name: "普通订单_触发累计充值和一次性佣金",
|
||||
isPurchaseOnBehalf: false,
|
||||
expectedAccumulatedRecharge: 15000,
|
||||
expectedCommissionRecords: 2,
|
||||
expectedOneTimeCommission: true,
|
||||
},
|
||||
{
|
||||
name: "代购订单_不触发累计充值和一次性佣金",
|
||||
isPurchaseOnBehalf: true,
|
||||
expectedAccumulatedRecharge: 0,
|
||||
expectedCommissionRecords: 1,
|
||||
expectedOneTimeCommission: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.NoError(t, tx.Model(&model.Device{}).Where("id = ?", device.ID).Updates(map[string]interface{}{
|
||||
"accumulated_recharge": 0,
|
||||
"first_commission_paid": false,
|
||||
}).Error)
|
||||
|
||||
require.NoError(t, tx.Where("1=1").Delete(&model.CommissionRecord{}).Error)
|
||||
require.NoError(t, tx.Where("1=1").Delete(&model.Order{}).Error)
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
OrderNo: "ORD" + time.Now().Format("20060102150405"),
|
||||
OrderType: model.OrderTypeDevice,
|
||||
DeviceID: &device.ID,
|
||||
BuyerType: model.BuyerTypeAgent,
|
||||
BuyerID: shop.ID,
|
||||
SellerShopID: &shop.ID,
|
||||
SeriesID: &seriesID,
|
||||
TotalAmount: 15000,
|
||||
SellerCostPrice: 5000,
|
||||
IsPurchaseOnBehalf: tt.isPurchaseOnBehalf,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
PaymentStatus: model.PaymentStatusPaid,
|
||||
}
|
||||
require.NoError(t, tx.Create(order).Error)
|
||||
|
||||
err := service.CalculateCommission(ctx, order.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var updatedDevice model.Device
|
||||
require.NoError(t, tx.First(&updatedDevice, device.ID).Error)
|
||||
assert.Equal(t, tt.expectedAccumulatedRecharge, updatedDevice.AccumulatedRecharge, "累计充值金额不符合预期")
|
||||
|
||||
var records []model.CommissionRecord
|
||||
require.NoError(t, tx.Where("order_id = ?", order.ID).Find(&records).Error)
|
||||
assert.Equal(t, tt.expectedCommissionRecords, len(records), "佣金记录数量不符合预期")
|
||||
|
||||
hasOneTimeCommission := false
|
||||
for _, record := range records {
|
||||
if record.CommissionSource == model.CommissionSourceOneTime {
|
||||
hasOneTimeCommission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.expectedOneTimeCommission, hasOneTimeCommission, "一次性佣金触发状态不符合预期")
|
||||
|
||||
if tt.expectedOneTimeCommission {
|
||||
assert.True(t, updatedDevice.FirstCommissionPaid, "首次佣金发放标记应为true")
|
||||
} else {
|
||||
assert.False(t, updatedDevice.FirstCommissionPaid, "首次佣金发放标记应为false")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,9 @@ type Service struct {
|
||||
iotCardStore *postgres.IotCardStore
|
||||
shopStore *postgres.ShopStore
|
||||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -31,7 +31,8 @@ func New(
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
@@ -41,9 +42,9 @@ func New(
|
||||
iotCardStore: iotCardStore,
|
||||
shopStore: shopStore,
|
||||
assetAllocationRecordStore: assetAllocationRecordStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
shopSeriesAllocationStore: seriesAllocationStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,20 +633,27 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetDe
|
||||
continue
|
||||
}
|
||||
|
||||
// 验证操作者权限(仅代理用户)
|
||||
// 验证操作者权限(仅代理用户)- 检查是否有该系列的套餐分配
|
||||
if operatorShopID != nil && req.SeriesID > 0 {
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *operatorShopID, req.SeriesID)
|
||||
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound || allocation.Status != 1 {
|
||||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||||
DeviceID: deviceID,
|
||||
DeviceNo: device.DeviceNo,
|
||||
Reason: "您没有权限分配该套餐系列",
|
||||
})
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
hasSeriesAllocation := false
|
||||
for _, alloc := range seriesAllocations {
|
||||
if alloc.SeriesID == req.SeriesID && alloc.Status == 1 {
|
||||
hasSeriesAllocation = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSeriesAllocation {
|
||||
failedItems = append(failedItems, dto.DeviceSeriesBindngFailedItem{
|
||||
DeviceID: deviceID,
|
||||
DeviceNo: device.DeviceNo,
|
||||
Reason: "您没有权限分配该套餐系列",
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 验证设备权限(基于 device.ShopID)
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func uniqueTestDeviceNoPrefix() string {
|
||||
return fmt.Sprintf("D%d", time.Now().UnixNano()%1000000000)
|
||||
}
|
||||
|
||||
func TestDeviceService_BatchSetSeriesBinding(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
deviceStore := postgres.NewDeviceStore(tx, rdb)
|
||||
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
assetAllocationRecordStore := postgres.NewAssetAllocationRecordStore(tx, rdb)
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
|
||||
svc := New(tx, deviceStore, deviceSimBindingStore, iotCardStore, shopStore, assetAllocationRecordStore, seriesAllocationStore, packageSeriesStore)
|
||||
ctx := context.Background()
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: fmt.Sprintf("SHOP%d", time.Now().UnixNano()%1000000),
|
||||
Level: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(shop).Error)
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("SERIES%d", time.Now().UnixNano()%1000000),
|
||||
SeriesName: "测试系列",
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(series).Error)
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(allocation).Error)
|
||||
|
||||
prefix := uniqueTestDeviceNoPrefix()
|
||||
devices := []*model.Device{
|
||||
{DeviceNo: prefix + "001", DeviceName: "测试设备1", Status: 1, ShopID: &shop.ID},
|
||||
{DeviceNo: prefix + "002", DeviceName: "测试设备2", Status: 1, ShopID: &shop.ID},
|
||||
{DeviceNo: prefix + "003", DeviceName: "测试设备3", Status: 1, ShopID: nil},
|
||||
}
|
||||
require.NoError(t, deviceStore.CreateBatch(ctx, devices))
|
||||
|
||||
t.Run("成功设置系列绑定", func(t *testing.T) {
|
||||
req := &dto.BatchSetDeviceSeriesBindngRequest{
|
||||
DeviceIDs: []uint{devices[0].ID, devices[1].ID},
|
||||
SeriesID: allocation.SeriesID,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, resp.SuccessCount)
|
||||
assert.Equal(t, 0, resp.FailCount)
|
||||
|
||||
var updatedDevices []*model.Device
|
||||
require.NoError(t, tx.Where("id IN ?", req.DeviceIDs).Find(&updatedDevices).Error)
|
||||
for _, device := range updatedDevices {
|
||||
require.NotNil(t, device.SeriesID)
|
||||
assert.Equal(t, allocation.SeriesID, *device.SeriesID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("设备不属于套餐系列分配的店铺", func(t *testing.T) {
|
||||
req := &dto.BatchSetDeviceSeriesBindngRequest{
|
||||
DeviceIDs: []uint{devices[2].ID},
|
||||
SeriesID: allocation.SeriesID,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, resp.SuccessCount)
|
||||
assert.Equal(t, 1, resp.FailCount)
|
||||
assert.Equal(t, "设备不属于套餐系列分配的店铺", resp.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("设备不存在", func(t *testing.T) {
|
||||
req := &dto.BatchSetDeviceSeriesBindngRequest{
|
||||
DeviceIDs: []uint{99999},
|
||||
SeriesID: allocation.SeriesID,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, resp.SuccessCount)
|
||||
assert.Equal(t, 1, resp.FailCount)
|
||||
assert.Equal(t, "设备不存在", resp.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("清除系列绑定", func(t *testing.T) {
|
||||
req := &dto.BatchSetDeviceSeriesBindngRequest{
|
||||
DeviceIDs: []uint{devices[0].ID},
|
||||
SeriesID: 0,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, resp.SuccessCount)
|
||||
|
||||
var updatedDevice model.Device
|
||||
require.NoError(t, tx.First(&updatedDevice, devices[0].ID).Error)
|
||||
assert.Nil(t, updatedDevice.SeriesID)
|
||||
})
|
||||
|
||||
t.Run("代理用户只能操作自己店铺的设备", func(t *testing.T) {
|
||||
otherShopID := uint(99999)
|
||||
req := &dto.BatchSetDeviceSeriesBindngRequest{
|
||||
DeviceIDs: []uint{devices[1].ID},
|
||||
SeriesID: 0,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, &otherShopID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, resp.SuccessCount)
|
||||
assert.Equal(t, 1, resp.FailCount)
|
||||
assert.Equal(t, "无权操作此设备", resp.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("套餐系列分配不存在", func(t *testing.T) {
|
||||
req := &dto.BatchSetDeviceSeriesBindngRequest{
|
||||
DeviceIDs: []uint{devices[1].ID},
|
||||
SeriesID: 99999,
|
||||
}
|
||||
|
||||
_, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -19,7 +19,8 @@ type Service struct {
|
||||
iotCardStore *postgres.IotCardStore
|
||||
shopStore *postgres.ShopStore
|
||||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
@@ -30,7 +31,8 @@ func New(
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
gatewayClient *gateway.Client,
|
||||
logger *zap.Logger,
|
||||
@@ -40,7 +42,8 @@ func New(
|
||||
iotCardStore: iotCardStore,
|
||||
shopStore: shopStore,
|
||||
assetAllocationRecordStore: assetAllocationRecordStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
@@ -603,17 +606,24 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
|
||||
|
||||
// 验证操作者权限(仅代理用户)
|
||||
if operatorShopID != nil && req.SeriesID > 0 {
|
||||
allocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, *operatorShopID, req.SeriesID)
|
||||
seriesAllocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, *operatorShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound || allocation.Status != 1 {
|
||||
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
||||
ICCID: iccid,
|
||||
Reason: "您没有权限分配该套餐系列",
|
||||
})
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
hasSeriesAllocation := false
|
||||
for _, alloc := range seriesAllocations {
|
||||
if alloc.SeriesID == req.SeriesID && alloc.Status == 1 {
|
||||
hasSeriesAllocation = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSeriesAllocation {
|
||||
failedItems = append(failedItems, dto.CardSeriesBindngFailedItem{
|
||||
ICCID: iccid,
|
||||
Reason: "您没有权限分配该套餐系列",
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 验证卡权限(基于 card.ShopID)
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package iot_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func uniqueTestICCIDPrefix() string {
|
||||
return fmt.Sprintf("T%d", time.Now().UnixNano()%1000000000)
|
||||
}
|
||||
|
||||
func TestIotCardService_BatchSetSeriesBinding(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
assetAllocationRecordStore := postgres.NewAssetAllocationRecordStore(tx, rdb)
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(tx, iotCardStore, shopStore, assetAllocationRecordStore, seriesAllocationStore, packageSeriesStore, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: fmt.Sprintf("SHOP%d", time.Now().UnixNano()%1000000),
|
||||
Level: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(shop).Error)
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("SERIES%d", time.Now().UnixNano()%1000000),
|
||||
SeriesName: "测试系列",
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(series).Error)
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, tx.Create(allocation).Error)
|
||||
|
||||
prefix := uniqueTestICCIDPrefix()
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: prefix + "001", CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
{ICCID: prefix + "002", CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
{ICCID: prefix + "003", CarrierID: 1, Status: 1, ShopID: nil},
|
||||
}
|
||||
require.NoError(t, iotCardStore.CreateBatch(ctx, cards))
|
||||
|
||||
t.Run("成功设置系列绑定", func(t *testing.T) {
|
||||
req := &dto.BatchSetCardSeriesBindngRequest{
|
||||
ICCIDs: []string{prefix + "001", prefix + "002"},
|
||||
SeriesID: allocation.SeriesID,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, resp.SuccessCount)
|
||||
assert.Equal(t, 0, resp.FailCount)
|
||||
|
||||
var updatedCards []*model.IotCard
|
||||
require.NoError(t, tx.Where("iccid IN ?", req.ICCIDs).Find(&updatedCards).Error)
|
||||
for _, card := range updatedCards {
|
||||
require.NotNil(t, card.SeriesID)
|
||||
assert.Equal(t, allocation.SeriesID, *card.SeriesID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("卡不属于套餐系列分配的店铺", func(t *testing.T) {
|
||||
req := &dto.BatchSetCardSeriesBindngRequest{
|
||||
ICCIDs: []string{prefix + "003"},
|
||||
SeriesID: allocation.SeriesID,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, resp.SuccessCount)
|
||||
assert.Equal(t, 1, resp.FailCount)
|
||||
assert.Equal(t, "卡不属于套餐系列分配的店铺", resp.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("卡不存在", func(t *testing.T) {
|
||||
req := &dto.BatchSetCardSeriesBindngRequest{
|
||||
ICCIDs: []string{"NOTEXIST001"},
|
||||
SeriesID: allocation.SeriesID,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, resp.SuccessCount)
|
||||
assert.Equal(t, 1, resp.FailCount)
|
||||
assert.Equal(t, "卡不存在", resp.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("清除系列绑定", func(t *testing.T) {
|
||||
req := &dto.BatchSetCardSeriesBindngRequest{
|
||||
ICCIDs: []string{prefix + "001"},
|
||||
SeriesID: 0,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, resp.SuccessCount)
|
||||
|
||||
var updatedCard model.IotCard
|
||||
require.NoError(t, tx.Where("iccid = ?", prefix+"001").First(&updatedCard).Error)
|
||||
assert.Nil(t, updatedCard.SeriesID)
|
||||
})
|
||||
|
||||
t.Run("代理用户只能操作自己店铺的卡", func(t *testing.T) {
|
||||
otherShopID := uint(99999)
|
||||
req := &dto.BatchSetCardSeriesBindngRequest{
|
||||
ICCIDs: []string{prefix + "002"},
|
||||
SeriesID: 0,
|
||||
}
|
||||
|
||||
resp, err := svc.BatchSetSeriesBinding(ctx, req, &otherShopID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, resp.SuccessCount)
|
||||
assert.Equal(t, 1, resp.FailCount)
|
||||
assert.Equal(t, "无权操作此卡", resp.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("套餐系列分配不存在", func(t *testing.T) {
|
||||
req := &dto.BatchSetCardSeriesBindngRequest{
|
||||
ICCIDs: []string{prefix + "002"},
|
||||
SeriesID: 99999,
|
||||
}
|
||||
|
||||
_, err := svc.BatchSetSeriesBinding(ctx, req, nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
@@ -21,18 +20,19 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
orderStore *postgres.OrderStore
|
||||
orderItemStore *postgres.OrderItemStore
|
||||
walletStore *postgres.WalletStore
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
wechatPayment wechat.PaymentServiceInterface
|
||||
queueClient *queue.Client
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
orderStore *postgres.OrderStore
|
||||
orderItemStore *postgres.OrderItemStore
|
||||
walletStore *postgres.WalletStore
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
wechatPayment wechat.PaymentServiceInterface
|
||||
queueClient *queue.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -41,27 +41,29 @@ func New(
|
||||
orderItemStore *postgres.OrderItemStore,
|
||||
walletStore *postgres.WalletStore,
|
||||
purchaseValidationService *purchase_validation.Service,
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
wechatPayment wechat.PaymentServiceInterface,
|
||||
queueClient *queue.Client,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
orderStore: orderStore,
|
||||
orderItemStore: orderItemStore,
|
||||
walletStore: walletStore,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
allocationConfigStore: allocationConfigStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
wechatPayment: wechatPayment,
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
db: db,
|
||||
orderStore: orderStore,
|
||||
orderItemStore: orderItemStore,
|
||||
walletStore: walletStore,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
wechatPayment: wechatPayment,
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +89,12 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(validationResult)
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
|
||||
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
|
||||
return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求")
|
||||
}
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
|
||||
|
||||
orderBuyerType := buyerType
|
||||
orderBuyerID := buyerID
|
||||
totalAmount := validationResult.TotalPrice
|
||||
@@ -107,9 +107,20 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
var sellerShopID *uint
|
||||
var sellerCostPrice int64
|
||||
|
||||
if validationResult.Allocation != nil {
|
||||
seriesID = &validationResult.Allocation.SeriesID
|
||||
sellerShopID = &validationResult.Allocation.ShopID
|
||||
if validationResult.Card != nil {
|
||||
seriesID = validationResult.Card.SeriesID
|
||||
sellerShopID = validationResult.Card.ShopID
|
||||
} else if validationResult.Device != nil {
|
||||
seriesID = validationResult.Device.SeriesID
|
||||
sellerShopID = validationResult.Device.ShopID
|
||||
}
|
||||
|
||||
if sellerShopID != nil && len(validationResult.Packages) > 0 {
|
||||
firstPackageID := validationResult.Packages[0].ID
|
||||
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, *sellerShopID, firstPackageID)
|
||||
if err == nil && allocation != nil {
|
||||
sellerCostPrice = allocation.CostPrice
|
||||
}
|
||||
}
|
||||
|
||||
if req.PaymentMethod == model.PaymentMethodOffline {
|
||||
@@ -125,8 +136,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
paidAt = purchasePaidAt
|
||||
isPurchaseOnBehalf = true
|
||||
sellerCostPrice = buyerCostPrice
|
||||
} else if validationResult.Allocation != nil {
|
||||
sellerCostPrice = utils.CalculateCostPrice(validationResult.Allocation, validationResult.TotalPrice)
|
||||
}
|
||||
|
||||
order := &model.Order{
|
||||
@@ -145,7 +154,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
PaymentStatus: paymentStatus,
|
||||
PaidAt: paidAt,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
CommissionConfigVersion: configVersion,
|
||||
CommissionConfigVersion: 0,
|
||||
SeriesID: seriesID,
|
||||
SellerShopID: sellerShopID,
|
||||
SellerCostPrice: sellerCostPrice,
|
||||
@@ -171,24 +180,36 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
|
||||
func (s *Service) resolvePurchaseOnBehalfInfo(ctx context.Context, result *purchase_validation.PurchaseValidationResult) (uint, int64, *time.Time, error) {
|
||||
var resourceShopID *uint
|
||||
var seriesID *uint
|
||||
|
||||
if result.Card != nil {
|
||||
resourceShopID = result.Card.ShopID
|
||||
seriesID = result.Card.SeriesID
|
||||
} else if result.Device != nil {
|
||||
resourceShopID = result.Device.ShopID
|
||||
seriesID = result.Device.SeriesID
|
||||
}
|
||||
|
||||
if resourceShopID == nil || *resourceShopID == 0 {
|
||||
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "资源未分配给代理商,无法代购")
|
||||
}
|
||||
|
||||
buyerAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, *resourceShopID, result.Allocation.SeriesID)
|
||||
if err != nil {
|
||||
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "买家没有该套餐系列的分配配置")
|
||||
if seriesID == nil || *seriesID == 0 {
|
||||
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "资源未关联套餐系列")
|
||||
}
|
||||
|
||||
if len(result.Packages) == 0 {
|
||||
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "订单中没有套餐")
|
||||
}
|
||||
|
||||
firstPackageID := result.Packages[0].ID
|
||||
buyerAllocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, *resourceShopID, firstPackageID)
|
||||
if err != nil {
|
||||
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "买家没有该套餐的分配配置")
|
||||
}
|
||||
|
||||
buyerCostPrice := utils.CalculateCostPrice(buyerAllocation, result.TotalPrice)
|
||||
now := time.Now()
|
||||
return *resourceShopID, buyerCostPrice, &now, nil
|
||||
return *resourceShopID, buyerAllocation.CostPrice, &now, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package) []*model.OrderItem {
|
||||
@@ -524,7 +545,7 @@ func (s *Service) activatePackage(ctx context.Context, tx *gorm.DB, order *model
|
||||
OrderID: order.ID,
|
||||
PackageID: item.PackageID,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.DataAmountMB,
|
||||
DataLimitMB: pkg.RealDataMB,
|
||||
ActivatedAt: now,
|
||||
ExpiresAt: now.AddDate(0, pkg.DurationMonths, 0),
|
||||
Status: 1,
|
||||
@@ -544,17 +565,6 @@ func (s *Service) activatePackage(ctx context.Context, tx *gorm.DB, order *model
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) snapshotCommissionConfig(ctx context.Context, allocationID uint) int {
|
||||
if s.allocationConfigStore == nil {
|
||||
return 0
|
||||
}
|
||||
config, err := s.allocationConfigStore.GetEffective(ctx, allocationID, time.Now())
|
||||
if err != nil || config == nil {
|
||||
return 0
|
||||
}
|
||||
return config.Version
|
||||
}
|
||||
|
||||
func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint) {
|
||||
if s.queueClient == nil {
|
||||
s.logger.Warn("队列客户端未初始化,跳过佣金计算任务入队", zap.Uint("order_id", orderID))
|
||||
@@ -752,39 +762,62 @@ type ForceRechargeRequirement struct {
|
||||
TriggerType string
|
||||
}
|
||||
|
||||
func (s *Service) checkForceRechargeRequirement(result *purchase_validation.PurchaseValidationResult) *ForceRechargeRequirement {
|
||||
if result.Allocation == nil {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
}
|
||||
|
||||
allocation := result.Allocation
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
}
|
||||
// checkForceRechargeRequirement 检查强充要求
|
||||
// 从 PackageSeries 获取一次性佣金配置,使用 per-series 追踪判断是否需要强充
|
||||
func (s *Service) checkForceRechargeRequirement(ctx context.Context, result *purchase_validation.PurchaseValidationResult) *ForceRechargeRequirement {
|
||||
defaultResult := &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
|
||||
// 1. 获取 seriesID
|
||||
var seriesID *uint
|
||||
var firstCommissionPaid bool
|
||||
|
||||
if result.Card != nil {
|
||||
firstCommissionPaid = result.Card.FirstCommissionPaid
|
||||
seriesID = result.Card.SeriesID
|
||||
if seriesID != nil {
|
||||
firstCommissionPaid = result.Card.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
} else if result.Device != nil {
|
||||
firstCommissionPaid = result.Device.FirstCommissionPaid
|
||||
}
|
||||
|
||||
if firstCommissionPaid {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
}
|
||||
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerSingleRecharge {
|
||||
return &ForceRechargeRequirement{
|
||||
NeedForceRecharge: true,
|
||||
ForceRechargeAmount: allocation.OneTimeCommissionThreshold,
|
||||
TriggerType: model.OneTimeCommissionTriggerSingleRecharge,
|
||||
seriesID = result.Device.SeriesID
|
||||
if seriesID != nil {
|
||||
firstCommissionPaid = result.Device.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
}
|
||||
|
||||
if allocation.EnableForceRecharge {
|
||||
forceAmount := allocation.ForceRechargeAmount
|
||||
if seriesID == nil {
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
// 2. 从 PackageSeries 获取一次性佣金配置
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
|
||||
if err != nil {
|
||||
s.logger.Warn("查询套餐系列失败", zap.Uint("series_id", *seriesID), zap.Error(err))
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
config, err := series.GetOneTimeCommissionConfig()
|
||||
if err != nil || config == nil || !config.Enable {
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
// 3. 如果该系列的一次性佣金已发放,无需强充
|
||||
if firstCommissionPaid {
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
// 4. 根据触发类型判断是否需要强充
|
||||
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
|
||||
return &ForceRechargeRequirement{
|
||||
NeedForceRecharge: true,
|
||||
ForceRechargeAmount: config.Threshold,
|
||||
TriggerType: model.OneTimeCommissionTriggerFirstRecharge,
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 累计充值模式,检查是否启用强充
|
||||
if config.EnableForceRecharge {
|
||||
forceAmount := config.ForceAmount
|
||||
if forceAmount == 0 {
|
||||
forceAmount = allocation.OneTimeCommissionThreshold
|
||||
forceAmount = config.Threshold
|
||||
}
|
||||
return &ForceRechargeRequirement{
|
||||
NeedForceRecharge: true,
|
||||
@@ -793,7 +826,7 @@ func (s *Service) checkForceRechargeRequirement(result *purchase_validation.Purc
|
||||
}
|
||||
}
|
||||
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRequest) (*dto.PurchaseCheckResponse, error) {
|
||||
@@ -812,7 +845,7 @@ func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(validationResult)
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
|
||||
|
||||
response := &dto.PurchaseCheckResponse{
|
||||
TotalPackageAmount: validationResult.TotalPrice,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ package packagepkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -20,20 +19,17 @@ type Service struct {
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
}
|
||||
|
||||
func New(
|
||||
packageStore *postgres.PackageStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +44,20 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
|
||||
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, "虚流量额度不能大于真流量额度")
|
||||
}
|
||||
}
|
||||
|
||||
var seriesName *string
|
||||
if req.SeriesID != nil && *req.SeriesID > 0 {
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
|
||||
@@ -61,32 +71,24 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
|
||||
}
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: req.PackageCode,
|
||||
PackageName: req.PackageName,
|
||||
PackageType: req.PackageType,
|
||||
DurationMonths: req.DurationMonths,
|
||||
Price: req.Price,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
PackageCode: req.PackageCode,
|
||||
PackageName: req.PackageName,
|
||||
PackageType: req.PackageType,
|
||||
DurationMonths: req.DurationMonths,
|
||||
CostPrice: req.CostPrice,
|
||||
EnableVirtualData: req.EnableVirtualData,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
}
|
||||
if req.SeriesID != nil {
|
||||
pkg.SeriesID = *req.SeriesID
|
||||
}
|
||||
if req.DataType != nil {
|
||||
pkg.DataType = *req.DataType
|
||||
}
|
||||
if req.RealDataMB != nil {
|
||||
pkg.RealDataMB = *req.RealDataMB
|
||||
}
|
||||
if req.VirtualDataMB != nil {
|
||||
pkg.VirtualDataMB = *req.VirtualDataMB
|
||||
}
|
||||
if req.DataAmountMB != nil {
|
||||
pkg.DataAmountMB = *req.DataAmountMB
|
||||
}
|
||||
if req.SuggestedCostPrice != nil {
|
||||
pkg.SuggestedCostPrice = *req.SuggestedCostPrice
|
||||
}
|
||||
if req.SuggestedRetailPrice != nil {
|
||||
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
|
||||
}
|
||||
@@ -147,7 +149,6 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
|
||||
pkg.SeriesID = *req.SeriesID
|
||||
seriesName = &series.SeriesName
|
||||
} else if pkg.SeriesID > 0 {
|
||||
// 如果没有更新 SeriesID,但现有套餐有 SeriesID,则查询当前的系列名称
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
if err == nil {
|
||||
seriesName = &series.SeriesName
|
||||
@@ -163,27 +164,32 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
|
||||
if req.DurationMonths != nil {
|
||||
pkg.DurationMonths = *req.DurationMonths
|
||||
}
|
||||
if req.DataType != nil {
|
||||
pkg.DataType = *req.DataType
|
||||
}
|
||||
if req.RealDataMB != nil {
|
||||
pkg.RealDataMB = *req.RealDataMB
|
||||
}
|
||||
if req.VirtualDataMB != nil {
|
||||
pkg.VirtualDataMB = *req.VirtualDataMB
|
||||
}
|
||||
if req.DataAmountMB != nil {
|
||||
pkg.DataAmountMB = *req.DataAmountMB
|
||||
if req.EnableVirtualData != nil {
|
||||
pkg.EnableVirtualData = *req.EnableVirtualData
|
||||
}
|
||||
if req.Price != nil {
|
||||
pkg.Price = *req.Price
|
||||
}
|
||||
if req.SuggestedCostPrice != nil {
|
||||
pkg.SuggestedCostPrice = *req.SuggestedCostPrice
|
||||
if req.CostPrice != nil {
|
||||
pkg.CostPrice = *req.CostPrice
|
||||
}
|
||||
if req.SuggestedRetailPrice != nil {
|
||||
pkg.SuggestedRetailPrice = *req.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
// 校验虚流量配置
|
||||
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 {
|
||||
@@ -246,9 +252,11 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询套餐列表失败")
|
||||
}
|
||||
|
||||
// 收集所有唯一的 series_id
|
||||
// 收集所有唯一的 series_id 和 package_id
|
||||
seriesIDMap := make(map[uint]bool)
|
||||
for _, pkg := range packages {
|
||||
packageIDs := make([]uint, len(packages))
|
||||
for i, pkg := range packages {
|
||||
packageIDs[i] = pkg.ID
|
||||
if pkg.SeriesID > 0 {
|
||||
seriesIDMap[pkg.SeriesID] = true
|
||||
}
|
||||
@@ -270,10 +278,16 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
}
|
||||
}
|
||||
|
||||
// 构建响应,填充系列名称
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
var allocationMap map[uint]*model.ShopPackageAllocation
|
||||
if userType == constants.UserTypeAgent && shopID > 0 && len(packageIDs) > 0 {
|
||||
allocationMap = s.batchGetAllocationsForShop(ctx, shopID, packageIDs)
|
||||
}
|
||||
|
||||
responses := make([]*dto.PackageResponse, len(packages))
|
||||
for i, pkg := range packages {
|
||||
resp := s.toResponse(ctx, pkg)
|
||||
resp := s.toResponseWithAllocation(pkg, allocationMap)
|
||||
if pkg.SeriesID > 0 {
|
||||
if seriesName, ok := seriesMap[pkg.SeriesID]; ok {
|
||||
resp.SeriesName = &seriesName
|
||||
@@ -354,12 +368,10 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
DataType: pkg.DataType,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
DataAmountMB: pkg.DataAmountMB,
|
||||
Price: pkg.Price,
|
||||
SuggestedCostPrice: pkg.SuggestedCostPrice,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
@@ -372,34 +384,61 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
|
||||
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.CostPrice = allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
|
||||
commissionInfo := s.getCommissionInfo(ctx, allocation.AllocationID)
|
||||
if commissionInfo != nil {
|
||||
resp.CurrentCommissionRate = commissionInfo.CurrentRate
|
||||
resp.TierInfo = commissionInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Service) getCommissionInfo(ctx context.Context, allocationID uint) *dto.CommissionTierInfo {
|
||||
seriesAllocation, err := s.seriesAllocationStore.GetByID(ctx, allocationID)
|
||||
if err != nil {
|
||||
return nil
|
||||
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
|
||||
}
|
||||
|
||||
info := &dto.CommissionTierInfo{}
|
||||
|
||||
if seriesAllocation.BaseCommissionMode == constants.CommissionModeFixed {
|
||||
info.CurrentRate = fmt.Sprintf("%.2f元/单", float64(seriesAllocation.BaseCommissionValue)/100)
|
||||
} else {
|
||||
info.CurrentRate = fmt.Sprintf("%.1f%%", float64(seriesAllocation.BaseCommissionValue)/10)
|
||||
for _, alloc := range allocations {
|
||||
allocationMap[alloc.PackageID] = alloc
|
||||
}
|
||||
|
||||
return info
|
||||
return allocationMap
|
||||
}
|
||||
|
||||
func (s *Service) toResponseWithAllocation(pkg *model.Package, allocationMap map[uint]*model.ShopPackageAllocation) *dto.PackageResponse {
|
||||
var seriesID *uint
|
||||
if pkg.SeriesID > 0 {
|
||||
seriesID = &pkg.SeriesID
|
||||
}
|
||||
|
||||
resp := &dto.PackageResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
SeriesID: seriesID,
|
||||
PackageType: pkg.PackageType,
|
||||
DurationMonths: pkg.DurationMonths,
|
||||
RealDataMB: pkg.RealDataMB,
|
||||
VirtualDataMB: pkg.VirtualDataMB,
|
||||
EnableVirtualData: pkg.EnableVirtualData,
|
||||
CostPrice: pkg.CostPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestPackageService_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -38,7 +38,6 @@ func TestPackageService_Create(t *testing.T) {
|
||||
PackageName: "创建测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
@@ -57,7 +56,6 @@ func TestPackageService_Create(t *testing.T) {
|
||||
PackageName: "第一个套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
_, err := svc.Create(ctx, req1)
|
||||
require.NoError(t, err)
|
||||
@@ -67,7 +65,6 @@ func TestPackageService_Create(t *testing.T) {
|
||||
PackageName: "第二个套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
_, err = svc.Create(ctx, req2)
|
||||
require.Error(t, err)
|
||||
@@ -82,7 +79,6 @@ func TestPackageService_Create(t *testing.T) {
|
||||
PackageName: "系列测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
SeriesID: func() *uint { id := uint(99999); return &id }(),
|
||||
}
|
||||
|
||||
@@ -98,7 +94,7 @@ func TestPackageService_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -110,7 +106,6 @@ func TestPackageService_UpdateStatus(t *testing.T) {
|
||||
PackageName: "状态测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -138,7 +133,6 @@ func TestPackageService_UpdateStatus(t *testing.T) {
|
||||
PackageName: "启用测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created2, err := svc.Create(ctx, req2)
|
||||
require.NoError(t, err)
|
||||
@@ -168,7 +162,7 @@ func TestPackageService_UpdateShelfStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -181,7 +175,6 @@ func TestPackageService_UpdateShelfStatus(t *testing.T) {
|
||||
PackageName: "上架测试-启用",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -205,7 +198,6 @@ func TestPackageService_UpdateShelfStatus(t *testing.T) {
|
||||
PackageName: "上架测试-禁用",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -230,7 +222,6 @@ func TestPackageService_UpdateShelfStatus(t *testing.T) {
|
||||
PackageName: "下架测试",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -255,7 +246,7 @@ func TestPackageService_Get(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -267,7 +258,6 @@ func TestPackageService_Get(t *testing.T) {
|
||||
PackageName: "查询测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -293,7 +283,7 @@ func TestPackageService_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -305,23 +295,19 @@ func TestPackageService_Update(t *testing.T) {
|
||||
PackageName: "更新测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("更新成功", func(t *testing.T) {
|
||||
newName := "更新后的套餐名称"
|
||||
newPrice := int64(19900)
|
||||
updateReq := &dto.UpdatePackageRequest{
|
||||
PackageName: &newName,
|
||||
Price: &newPrice,
|
||||
}
|
||||
|
||||
resp, err := svc.Update(ctx, created.ID, updateReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newName, resp.PackageName)
|
||||
assert.Equal(t, newPrice, resp.Price)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的套餐", func(t *testing.T) {
|
||||
@@ -342,7 +328,7 @@ func TestPackageService_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -354,7 +340,6 @@ func TestPackageService_Delete(t *testing.T) {
|
||||
PackageName: "删除测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -377,7 +362,7 @@ func TestPackageService_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -390,21 +375,18 @@ func TestPackageService_List(t *testing.T) {
|
||||
PackageName: "列表测试套餐1",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
},
|
||||
{
|
||||
PackageCode: generateUniquePackageCode("PKG_LIST_002"),
|
||||
PackageName: "列表测试套餐2",
|
||||
PackageType: "addon",
|
||||
DurationMonths: 1,
|
||||
Price: 4900,
|
||||
},
|
||||
{
|
||||
PackageCode: generateUniquePackageCode("PKG_LIST_003"),
|
||||
PackageName: "列表测试套餐3",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Price: 99900,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -456,11 +438,118 @@ func TestPackageService_List(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageService_VirtualDataValidation(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
t.Run("启用虚流量时虚流量必须大于0", func(t *testing.T) {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_VDATA_1"),
|
||||
PackageName: "虚流量测试-零值",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
EnableVirtualData: true,
|
||||
RealDataMB: func() *int64 { v := int64(1000); return &v }(),
|
||||
VirtualDataMB: func() *int64 { v := int64(0); return &v }(),
|
||||
}
|
||||
|
||||
_, err := svc.Create(ctx, req)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
assert.Contains(t, appErr.Message, "虚流量额度必须大于0")
|
||||
})
|
||||
|
||||
t.Run("启用虚流量时虚流量不能超过真流量", func(t *testing.T) {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_VDATA_2"),
|
||||
PackageName: "虚流量测试-超过",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
EnableVirtualData: true,
|
||||
RealDataMB: func() *int64 { v := int64(1000); return &v }(),
|
||||
VirtualDataMB: func() *int64 { v := int64(2000); return &v }(),
|
||||
}
|
||||
|
||||
_, err := svc.Create(ctx, req)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
assert.Contains(t, appErr.Message, "虚流量额度不能大于真流量额度")
|
||||
})
|
||||
|
||||
t.Run("启用虚流量时配置正确则创建成功", func(t *testing.T) {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_VDATA_3"),
|
||||
PackageName: "虚流量测试-正确",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
EnableVirtualData: true,
|
||||
RealDataMB: func() *int64 { v := int64(1000); return &v }(),
|
||||
VirtualDataMB: func() *int64 { v := int64(500); return &v }(),
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, resp.EnableVirtualData)
|
||||
assert.Equal(t, int64(500), resp.VirtualDataMB)
|
||||
})
|
||||
|
||||
t.Run("不启用虚流量时可以不填虚流量值", func(t *testing.T) {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_VDATA_4"),
|
||||
PackageName: "虚流量测试-不启用",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
EnableVirtualData: false,
|
||||
RealDataMB: func() *int64 { v := int64(1000); return &v }(),
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, resp.EnableVirtualData)
|
||||
})
|
||||
|
||||
t.Run("更新时校验虚流量配置", func(t *testing.T) {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_VDATA_5"),
|
||||
PackageName: "虚流量测试-更新",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
EnableVirtualData: false,
|
||||
RealDataMB: func() *int64 { v := int64(1000); return &v }(),
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
enableVD := true
|
||||
virtualDataMB := int64(2000)
|
||||
updateReq := &dto.UpdatePackageRequest{
|
||||
EnableVirtualData: &enableVD,
|
||||
VirtualDataMB: &virtualDataMB,
|
||||
}
|
||||
_, err = svc.Update(ctx, created.ID, updateReq)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -485,7 +574,6 @@ func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
@@ -502,7 +590,6 @@ func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -522,7 +609,6 @@ func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -547,7 +633,6 @@ func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
_, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
@@ -578,7 +663,6 @@ func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
PackageName: "无系列套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
|
||||
@@ -35,13 +35,21 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageSeriesReques
|
||||
}
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: req.SeriesCode,
|
||||
SeriesName: req.SeriesName,
|
||||
Description: req.Description,
|
||||
Status: constants.StatusEnabled,
|
||||
SeriesCode: req.SeriesCode,
|
||||
SeriesName: req.SeriesName,
|
||||
Description: req.Description,
|
||||
Status: constants.StatusEnabled,
|
||||
OneTimeCommissionConfigJSON: "{}",
|
||||
}
|
||||
series.Creator = currentUserID
|
||||
|
||||
if req.OneTimeCommissionConfig != nil {
|
||||
config := s.dtoToModelConfig(req.OneTimeCommissionConfig)
|
||||
if err := series.SetOneTimeCommissionConfig(config); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "设置一次性佣金配置失败")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.packageSeriesStore.Create(ctx, series); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建套餐系列失败")
|
||||
}
|
||||
@@ -80,6 +88,12 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageSer
|
||||
if req.Description != nil {
|
||||
series.Description = *req.Description
|
||||
}
|
||||
if req.OneTimeCommissionConfig != nil {
|
||||
config := s.dtoToModelConfig(req.OneTimeCommissionConfig)
|
||||
if err := series.SetOneTimeCommissionConfig(config); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "设置一次性佣金配置失败")
|
||||
}
|
||||
}
|
||||
series.Updater = currentUserID
|
||||
|
||||
if err := s.packageSeriesStore.Update(ctx, series); err != nil {
|
||||
@@ -125,6 +139,9 @@ func (s *Service) List(ctx context.Context, req *dto.PackageSeriesListRequest) (
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
if req.EnableOneTimeCommission != nil {
|
||||
filters["enable_one_time_commission"] = *req.EnableOneTimeCommission
|
||||
}
|
||||
|
||||
seriesList, total, err := s.packageSeriesStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
@@ -164,13 +181,86 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
}
|
||||
|
||||
func (s *Service) toResponse(series *model.PackageSeries) *dto.PackageSeriesResponse {
|
||||
return &dto.PackageSeriesResponse{
|
||||
ID: series.ID,
|
||||
SeriesCode: series.SeriesCode,
|
||||
SeriesName: series.SeriesName,
|
||||
Description: series.Description,
|
||||
Status: series.Status,
|
||||
CreatedAt: series.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: series.UpdatedAt.Format(time.RFC3339),
|
||||
resp := &dto.PackageSeriesResponse{
|
||||
ID: series.ID,
|
||||
SeriesCode: series.SeriesCode,
|
||||
SeriesName: series.SeriesName,
|
||||
Description: series.Description,
|
||||
EnableOneTimeCommission: series.EnableOneTimeCommission,
|
||||
Status: series.Status,
|
||||
CreatedAt: series.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: series.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if config, err := series.GetOneTimeCommissionConfig(); err == nil && config != nil {
|
||||
resp.OneTimeCommissionConfig = s.modelToDTO(config)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Service) dtoToModelConfig(dtoConfig *dto.SeriesOneTimeCommissionConfigDTO) *model.OneTimeCommissionConfig {
|
||||
if dtoConfig == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tiers []model.OneTimeCommissionTier
|
||||
if len(dtoConfig.Tiers) > 0 {
|
||||
tiers = make([]model.OneTimeCommissionTier, len(dtoConfig.Tiers))
|
||||
for i, tier := range dtoConfig.Tiers {
|
||||
tiers[i] = model.OneTimeCommissionTier{
|
||||
Dimension: tier.Dimension,
|
||||
StatScope: tier.StatScope,
|
||||
Threshold: tier.Threshold,
|
||||
Amount: tier.Amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &model.OneTimeCommissionConfig{
|
||||
Enable: dtoConfig.Enable,
|
||||
TriggerType: dtoConfig.TriggerType,
|
||||
Threshold: dtoConfig.Threshold,
|
||||
CommissionType: dtoConfig.CommissionType,
|
||||
CommissionAmount: dtoConfig.CommissionAmount,
|
||||
Tiers: tiers,
|
||||
ValidityType: dtoConfig.ValidityType,
|
||||
ValidityValue: dtoConfig.ValidityValue,
|
||||
EnableForceRecharge: dtoConfig.EnableForceRecharge,
|
||||
ForceCalcType: dtoConfig.ForceCalcType,
|
||||
ForceAmount: dtoConfig.ForceAmount,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) modelToDTO(config *model.OneTimeCommissionConfig) *dto.SeriesOneTimeCommissionConfigDTO {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var tiers []dto.OneTimeCommissionTierDTO
|
||||
if len(config.Tiers) > 0 {
|
||||
tiers = make([]dto.OneTimeCommissionTierDTO, len(config.Tiers))
|
||||
for i, tier := range config.Tiers {
|
||||
tiers[i] = dto.OneTimeCommissionTierDTO{
|
||||
Dimension: tier.Dimension,
|
||||
StatScope: tier.StatScope,
|
||||
Threshold: tier.Threshold,
|
||||
Amount: tier.Amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.SeriesOneTimeCommissionConfigDTO{
|
||||
Enable: config.Enable,
|
||||
TriggerType: config.TriggerType,
|
||||
Threshold: config.Threshold,
|
||||
CommissionType: config.CommissionType,
|
||||
CommissionAmount: config.CommissionAmount,
|
||||
Tiers: tiers,
|
||||
ValidityType: config.ValidityType,
|
||||
ValidityValue: config.ValidityValue,
|
||||
EnableForceRecharge: config.EnableForceRecharge,
|
||||
ForceCalcType: config.ForceCalcType,
|
||||
ForceAmount: config.ForceAmount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
packageStore *postgres.PackageStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
packageStore *postgres.PackageStore
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -23,14 +22,12 @@ func New(
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
packageStore: packageStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
packageStore: packageStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +36,6 @@ type PurchaseValidationResult struct {
|
||||
Device *model.Device
|
||||
Packages []*model.Package
|
||||
TotalPrice int64
|
||||
Allocation *model.ShopSeriesAllocation
|
||||
}
|
||||
|
||||
func (s *Service) ValidateCardPurchase(ctx context.Context, cardID uint, packageIDs []uint) (*PurchaseValidationResult, error) {
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
package purchase_validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestData(t *testing.T) (context.Context, *Service, *model.IotCard, *model.Device, *model.Package, *model.ShopSeriesAllocation) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
deviceStore := postgres.NewDeviceStore(tx, rdb)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
carrierStore := postgres.NewCarrierStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "TEST_CARRIER_PV",
|
||||
CarrierName: "测试运营商",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, carrierStore.Create(ctx, carrier))
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺PV",
|
||||
ShopCode: "TEST_SHOP_PV",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_PV",
|
||||
SeriesName: "测试套餐系列",
|
||||
Description: "测试用",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_PV",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: series.ID,
|
||||
SuggestedRetailPrice: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.ShelfStatusOn,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
shopIDPtr := &shop.ID
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860000000000000001",
|
||||
ShopID: shopIDPtr,
|
||||
CarrierID: carrier.ID,
|
||||
SeriesID: &series.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, iotCardStore.Create(ctx, card))
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "DEV_TEST_PV_001",
|
||||
ShopID: shopIDPtr,
|
||||
SeriesID: &series.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, deviceStore.Create(ctx, device))
|
||||
|
||||
svc := New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
|
||||
|
||||
return ctx, svc, card, device, pkg, allocation
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_ValidateCardPurchase(t *testing.T) {
|
||||
ctx, svc, card, _, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("验证成功", func(t *testing.T) {
|
||||
result, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{pkg.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result.Card)
|
||||
assert.Equal(t, card.ID, result.Card.ID)
|
||||
assert.Len(t, result.Packages, 1)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, result.TotalPrice)
|
||||
})
|
||||
|
||||
t.Run("卡不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, 99999, []uint{pkg.ID})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeIotCardNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐列表为空", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{99999})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_ValidateDevicePurchase(t *testing.T) {
|
||||
ctx, svc, _, device, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("验证成功", func(t *testing.T) {
|
||||
result, err := svc.ValidateDevicePurchase(ctx, device.ID, []uint{pkg.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result.Device)
|
||||
assert.Equal(t, device.ID, result.Device.ID)
|
||||
assert.Len(t, result.Packages, 1)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, result.TotalPrice)
|
||||
})
|
||||
|
||||
t.Run("设备不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateDevicePurchase(ctx, 99999, []uint{pkg.ID})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐列表为空", func(t *testing.T) {
|
||||
_, err := svc.ValidateDevicePurchase(ctx, device.ID, []uint{})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_GetPurchasePrice(t *testing.T) {
|
||||
ctx, svc, _, _, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("获取个人客户价格", func(t *testing.T) {
|
||||
price := svc.GetPurchasePrice(ctx, pkg, model.BuyerTypePersonal)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, price)
|
||||
})
|
||||
|
||||
t.Run("获取代理商价格", func(t *testing.T) {
|
||||
price := svc.GetPurchasePrice(ctx, pkg, model.BuyerTypeAgent)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, price)
|
||||
})
|
||||
}
|
||||
@@ -38,6 +38,7 @@ type Service struct {
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
commissionRecordStore *postgres.CommissionRecordStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
@@ -51,6 +52,7 @@ func New(
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
commissionRecordStore *postgres.CommissionRecordStore,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
@@ -62,6 +64,7 @@ func New(
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
commissionRecordStore: commissionRecordStore,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -379,7 +382,6 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
|
||||
var accumulatedRecharge int64
|
||||
var firstCommissionPaid bool
|
||||
|
||||
// 1. 查询资源信息
|
||||
if resourceType == "iot_card" {
|
||||
card, err := s.iotCardStore.GetByID(ctx, resourceID)
|
||||
if err != nil {
|
||||
@@ -390,8 +392,10 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
|
||||
}
|
||||
seriesID = card.SeriesID
|
||||
shopID = card.ShopID
|
||||
accumulatedRecharge = card.AccumulatedRecharge
|
||||
firstCommissionPaid = card.FirstCommissionPaid
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
} else if resourceType == "device" {
|
||||
device, err := s.deviceStore.GetByID(ctx, resourceID)
|
||||
if err != nil {
|
||||
@@ -402,80 +406,101 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
|
||||
}
|
||||
seriesID = device.SeriesID
|
||||
shopID = device.ShopID
|
||||
accumulatedRecharge = device.AccumulatedRecharge
|
||||
firstCommissionPaid = device.FirstCommissionPaid
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
}
|
||||
|
||||
result.CurrentAccumulated = accumulatedRecharge
|
||||
result.FirstCommissionPaid = firstCommissionPaid
|
||||
|
||||
// 2. 如果没有系列ID或店铺ID,无强充要求
|
||||
if seriesID == nil || shopID == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 3. 查询系列分配配置
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID)
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return result, nil
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败")
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
|
||||
}
|
||||
|
||||
// 4. 如果未启用一次性佣金,无强充要求
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
config, err := series.GetOneTimeCommissionConfig()
|
||||
if err != nil || config == nil || !config.Enable {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.Threshold = allocation.OneTimeCommissionThreshold
|
||||
result.TriggerType = allocation.OneTimeCommissionTrigger
|
||||
result.Threshold = config.Threshold
|
||||
result.TriggerType = config.TriggerType
|
||||
|
||||
// 5. 如果一次性佣金已发放,无强充要求
|
||||
if firstCommissionPaid {
|
||||
result.Message = "一次性佣金已发放,无强充要求"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 6. 根据触发类型判断强充要求
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerSingleRecharge {
|
||||
// 首次充值触发:必须充值阈值金额
|
||||
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
|
||||
result.NeedForceRecharge = true
|
||||
result.ForceRechargeAmount = allocation.OneTimeCommissionThreshold
|
||||
result.Message = fmt.Sprintf("首次充值必须充值%d分", allocation.OneTimeCommissionThreshold)
|
||||
} else if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||
// 累计充值触发:检查是否启用强充
|
||||
if allocation.EnableForceRecharge {
|
||||
result.NeedForceRecharge = true
|
||||
// 强充金额优先使用配置值,否则使用阈值
|
||||
if allocation.ForceRechargeAmount > 0 {
|
||||
result.ForceRechargeAmount = allocation.ForceRechargeAmount
|
||||
} else {
|
||||
result.ForceRechargeAmount = allocation.OneTimeCommissionThreshold
|
||||
}
|
||||
result.Message = fmt.Sprintf("每次充值必须充值%d分", result.ForceRechargeAmount)
|
||||
result.ForceRechargeAmount = config.Threshold
|
||||
result.Message = fmt.Sprintf("首次充值必须充值%d分", config.Threshold)
|
||||
} else if config.EnableForceRecharge {
|
||||
result.NeedForceRecharge = true
|
||||
if config.ForceAmount > 0 {
|
||||
result.ForceRechargeAmount = config.ForceAmount
|
||||
} else {
|
||||
result.Message = "累计充值模式,可自由充值"
|
||||
result.ForceRechargeAmount = config.Threshold
|
||||
}
|
||||
result.Message = fmt.Sprintf("每次充值必须充值%d分", result.ForceRechargeAmount)
|
||||
} else {
|
||||
result.Message = "累计充值模式,可自由充值"
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// updateAccumulatedRechargeInTx 更新累计充值(事务内使用)
|
||||
// 原子操作更新卡或设备的累计充值金额
|
||||
// 同时更新旧的 accumulated_recharge 字段和新的 accumulated_recharge_by_series JSON 字段
|
||||
func (s *Service) updateAccumulatedRechargeInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, amount int64) error {
|
||||
if resourceType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
|
||||
if card.SeriesID != nil {
|
||||
if err := card.AddAccumulatedRechargeBySeries(*card.SeriesID, amount); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新卡按系列累计充值失败")
|
||||
}
|
||||
}
|
||||
|
||||
result := tx.Model(&model.IotCard{}).
|
||||
Where("id = ?", resourceID).
|
||||
Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", amount))
|
||||
Updates(map[string]any{
|
||||
"accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount),
|
||||
"accumulated_recharge_by_series": card.AccumulatedRechargeBySeriesJSON,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡累计充值失败")
|
||||
}
|
||||
} else if resourceType == "device" {
|
||||
var device model.Device
|
||||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
|
||||
if device.SeriesID != nil {
|
||||
if err := device.AddAccumulatedRechargeBySeries(*device.SeriesID, amount); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新设备按系列累计充值失败")
|
||||
}
|
||||
}
|
||||
|
||||
result := tx.Model(&model.Device{}).
|
||||
Where("id = ?", resourceID).
|
||||
Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", amount))
|
||||
Updates(map[string]any{
|
||||
"accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount),
|
||||
"accumulated_recharge_by_series": device.AccumulatedRechargeBySeriesJSON,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新设备累计充值失败")
|
||||
}
|
||||
@@ -491,33 +516,34 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
var firstCommissionPaid bool
|
||||
var shopID *uint
|
||||
|
||||
// 1. 查询资源当前状态(需要从数据库重新查询以获取更新后的累计充值)
|
||||
if resourceType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
seriesID = card.SeriesID
|
||||
accumulatedRecharge = card.AccumulatedRecharge
|
||||
firstCommissionPaid = card.FirstCommissionPaid
|
||||
shopID = card.ShopID
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
} else if resourceType == "device" {
|
||||
var device model.Device
|
||||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
seriesID = device.SeriesID
|
||||
accumulatedRecharge = device.AccumulatedRecharge
|
||||
firstCommissionPaid = device.FirstCommissionPaid
|
||||
shopID = device.ShopID
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果没有系列ID或已发放佣金,跳过
|
||||
if seriesID == nil || firstCommissionPaid {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. 如果没有归属店铺,无法发放佣金
|
||||
if shopID == nil {
|
||||
s.logger.Warn("资源未归属店铺,无法发放一次性佣金",
|
||||
zap.String("resource_type", resourceType),
|
||||
@@ -526,7 +552,19 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 查询系列分配配置
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
|
||||
}
|
||||
|
||||
config, cfgErr := series.GetOneTimeCommissionConfig()
|
||||
if cfgErr != nil || config == nil || !config.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -535,34 +573,23 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败")
|
||||
}
|
||||
|
||||
// 5. 如果未启用一次性佣金,跳过
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. 根据触发类型判断是否满足条件
|
||||
var rechargeAmountToCheck int64
|
||||
switch allocation.OneTimeCommissionTrigger {
|
||||
case model.OneTimeCommissionTriggerSingleRecharge:
|
||||
switch config.TriggerType {
|
||||
case model.OneTimeCommissionTriggerFirstRecharge:
|
||||
rechargeAmountToCheck = rechargeAmount
|
||||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||||
rechargeAmountToCheck = accumulatedRecharge
|
||||
default:
|
||||
rechargeAmountToCheck = accumulatedRecharge
|
||||
}
|
||||
|
||||
if rechargeAmountToCheck < config.Threshold {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 7. 检查是否达到阈值
|
||||
if rechargeAmountToCheck < allocation.OneTimeCommissionThreshold {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 8. 计算佣金金额
|
||||
commissionAmount := s.calculateOneTimeCommission(allocation, rechargeAmount)
|
||||
commissionAmount := allocation.OneTimeCommissionAmount
|
||||
if commissionAmount <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 9. 查询店铺的佣金钱包
|
||||
var commissionWallet model.Wallet
|
||||
if err := tx.Where("resource_type = ? AND resource_id = ? AND wallet_type = ?", "shop", *shopID, "commission").
|
||||
First(&commissionWallet).Error; err != nil {
|
||||
@@ -575,7 +602,6 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺佣金钱包失败")
|
||||
}
|
||||
|
||||
// 10. 创建佣金记录
|
||||
var iotCardID, deviceID *uint
|
||||
if resourceType == "iot_card" {
|
||||
iotCardID = &resourceID
|
||||
@@ -647,13 +673,33 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
|
||||
// 14. 标记一次性佣金已发放
|
||||
if resourceType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
if err := card.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "设置卡佣金发放状态失败")
|
||||
}
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", resourceID).
|
||||
Update("first_commission_paid", true).Error; err != nil {
|
||||
Updates(map[string]any{
|
||||
"first_commission_paid": true,
|
||||
"first_recharge_triggered_by_series": card.FirstRechargeTriggeredBySeriesJSON,
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
|
||||
}
|
||||
} else {
|
||||
var device model.Device
|
||||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
if err := device.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "设置设备佣金发放状态失败")
|
||||
}
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", resourceID).
|
||||
Update("first_commission_paid", true).Error; err != nil {
|
||||
Updates(map[string]any{
|
||||
"first_commission_paid": true,
|
||||
"first_recharge_triggered_by_series": device.FirstRechargeTriggeredBySeriesJSON,
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败")
|
||||
}
|
||||
}
|
||||
@@ -668,21 +714,6 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateOneTimeCommission 计算一次性佣金金额
|
||||
func (s *Service) calculateOneTimeCommission(allocation *model.ShopSeriesAllocation, orderAmount int64) int64 {
|
||||
if allocation.OneTimeCommissionType == model.OneTimeCommissionTypeFixed {
|
||||
// 固定佣金
|
||||
if allocation.OneTimeCommissionMode == model.CommissionModeFixed {
|
||||
return allocation.OneTimeCommissionValue
|
||||
} else if allocation.OneTimeCommissionMode == model.CommissionModePercent {
|
||||
// 百分比佣金(千分比)
|
||||
return orderAmount * allocation.OneTimeCommissionValue / 1000
|
||||
}
|
||||
}
|
||||
// 梯度佣金在此不处理,由 commission_calculation 服务处理
|
||||
return 0
|
||||
}
|
||||
|
||||
// generateRechargeNo 生成充值订单号
|
||||
// 格式: RCH + 14位时间戳 + 6位随机数
|
||||
func (s *Service) generateRechargeNo() string {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ type Service struct {
|
||||
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore
|
||||
shopStore *postgres.ShopStore
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -28,6 +29,7 @@ func New(
|
||||
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
@@ -35,6 +37,7 @@ func New(
|
||||
priceHistoryStore: priceHistoryStore,
|
||||
shopStore: shopStore,
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,25 +76,26 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopPackageAllocati
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
|
||||
}
|
||||
|
||||
existing, _ := s.packageAllocationStore.GetByShopAndPackage(ctx, req.ShopID, req.PackageID)
|
||||
if existing != nil {
|
||||
return nil, errors.New(errors.CodeConflict, "该店铺已有此套餐的分配配置")
|
||||
}
|
||||
|
||||
seriesAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, req.ShopID, pkg.SeriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "该套餐的系列未分配给此店铺")
|
||||
return nil, errors.New(errors.CodeInvalidParam, "请先分配该套餐所属的系列")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取系列分配失败")
|
||||
}
|
||||
|
||||
existing, _ := s.packageAllocationStore.GetByShopAndPackage(ctx, req.ShopID, req.PackageID)
|
||||
if existing != nil {
|
||||
return nil, errors.New(errors.CodeConflict, "该店铺已有此套餐的覆盖配置")
|
||||
}
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: req.ShopID,
|
||||
PackageID: req.PackageID,
|
||||
AllocationID: seriesAllocation.ID,
|
||||
CostPrice: req.CostPrice,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: req.ShopID,
|
||||
PackageID: req.PackageID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: req.CostPrice,
|
||||
SeriesAllocationID: &seriesAllocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation.Creator = currentUserID
|
||||
|
||||
@@ -204,6 +208,12 @@ func (s *Service) List(ctx context.Context, req *dto.ShopPackageAllocationListRe
|
||||
if req.PackageID != nil {
|
||||
filters["package_id"] = *req.PackageID
|
||||
}
|
||||
if req.SeriesAllocationID != nil {
|
||||
filters["series_allocation_id"] = *req.SeriesAllocationID
|
||||
}
|
||||
if req.AllocatorShopID != nil {
|
||||
filters["allocator_shop_id"] = *req.AllocatorShopID
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
@@ -258,19 +268,44 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
}
|
||||
|
||||
func (s *Service) buildResponse(ctx context.Context, a *model.ShopPackageAllocation, shopName, packageName, packageCode string) (*dto.ShopPackageAllocationResponse, error) {
|
||||
var seriesID uint
|
||||
seriesName := ""
|
||||
|
||||
pkg, _ := s.packageStore.GetByID(ctx, a.PackageID)
|
||||
if pkg != nil {
|
||||
seriesID = pkg.SeriesID
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
}
|
||||
|
||||
allocatorShopName := ""
|
||||
if a.AllocatorShopID > 0 {
|
||||
allocatorShop, _ := s.shopStore.GetByID(ctx, a.AllocatorShopID)
|
||||
if allocatorShop != nil {
|
||||
allocatorShopName = allocatorShop.ShopName
|
||||
}
|
||||
} else {
|
||||
allocatorShopName = "平台"
|
||||
}
|
||||
|
||||
return &dto.ShopPackageAllocationResponse{
|
||||
ID: a.ID,
|
||||
ShopID: a.ShopID,
|
||||
ShopName: shopName,
|
||||
PackageID: a.PackageID,
|
||||
PackageName: packageName,
|
||||
PackageCode: packageCode,
|
||||
AllocationID: a.AllocationID,
|
||||
CostPrice: a.CostPrice,
|
||||
CalculatedCostPrice: 0,
|
||||
Status: a.Status,
|
||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||
ID: a.ID,
|
||||
ShopID: a.ShopID,
|
||||
ShopName: shopName,
|
||||
PackageID: a.PackageID,
|
||||
PackageName: packageName,
|
||||
PackageCode: packageCode,
|
||||
SeriesID: seriesID,
|
||||
SeriesName: seriesName,
|
||||
SeriesAllocationID: a.SeriesAllocationID,
|
||||
AllocatorShopID: a.AllocatorShopID,
|
||||
AllocatorShopName: allocatorShopName,
|
||||
CostPrice: a.CostPrice,
|
||||
Status: a.Status,
|
||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package shop_package_batch_allocation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
@@ -16,29 +15,23 @@ import (
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
packageStore *postgres.PackageStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore
|
||||
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
shopStore *postgres.ShopStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
packageStore *postgres.PackageStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
packageStore: packageStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
configStore: configStore,
|
||||
commissionStatsStore: commissionStatsStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
shopStore: shopStore,
|
||||
}
|
||||
}
|
||||
@@ -79,48 +72,31 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
|
||||
return 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 errors.Wrap(errors.CodeInternalError, err, "查询系列分配失败")
|
||||
}
|
||||
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
BaseModel: model.BaseModel{Creator: currentUserID, Updater: currentUserID},
|
||||
ShopID: req.ShopID,
|
||||
SeriesID: req.SeriesID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
BaseCommissionMode: req.BaseCommission.Mode,
|
||||
BaseCommissionValue: req.BaseCommission.Value,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
if err := tx.Create(seriesAllocation).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建系列分配失败")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
config := &model.ShopSeriesAllocationConfig{
|
||||
AllocationID: seriesAllocation.ID,
|
||||
Version: 1,
|
||||
BaseCommissionMode: req.BaseCommission.Mode,
|
||||
BaseCommissionValue: req.BaseCommission.Value,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
|
||||
if err := tx.Create(config).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建配置版本失败")
|
||||
}
|
||||
|
||||
packageAllocations := make([]*model.ShopPackageAllocation, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
costPrice := pkg.SuggestedCostPrice
|
||||
costPrice := pkg.CostPrice
|
||||
if req.PriceAdjustment != nil {
|
||||
costPrice = s.calculateAdjustedPrice(pkg.SuggestedCostPrice, req.PriceAdjustment)
|
||||
costPrice = s.calculateAdjustedPrice(pkg.CostPrice, req.PriceAdjustment)
|
||||
}
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
BaseModel: model.BaseModel{Creator: currentUserID, Updater: currentUserID},
|
||||
ShopID: req.ShopID,
|
||||
PackageID: pkg.ID,
|
||||
AllocationID: seriesAllocation.ID,
|
||||
CostPrice: costPrice,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: currentUserID, Updater: currentUserID},
|
||||
ShopID: req.ShopID,
|
||||
PackageID: pkg.ID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: costPrice,
|
||||
SeriesAllocationID: &seriesAllocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
packageAllocations = append(packageAllocations, allocation)
|
||||
}
|
||||
|
||||
@@ -10,34 +10,29 @@ import (
|
||||
"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"
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
allocationStore *postgres.ShopSeriesAllocationStore
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore
|
||||
oneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore
|
||||
shopStore *postgres.ShopStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageStore *postgres.PackageStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopStore *postgres.ShopStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
}
|
||||
|
||||
func New(
|
||||
allocationStore *postgres.ShopSeriesAllocationStore,
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
oneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
allocationStore: allocationStore,
|
||||
configStore: configStore,
|
||||
oneTimeCommissionTierStore: oneTimeCommissionTierStore,
|
||||
shopStore: shopStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageStore: packageStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
shopStore: shopStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,16 +57,9 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取店铺失败")
|
||||
}
|
||||
|
||||
isPlatformUser := userType == constants.UserTypeSuperAdmin || userType == constants.UserTypePlatform
|
||||
isFirstLevelShop := targetShop.ParentID == nil
|
||||
|
||||
if isPlatformUser {
|
||||
if !isFirstLevelShop {
|
||||
return nil, errors.New(errors.CodeForbidden, "平台只能为一级店铺分配套餐")
|
||||
}
|
||||
} else {
|
||||
if isFirstLevelShop || *targetShop.ParentID != allocatorShopID {
|
||||
return nil, errors.New(errors.CodeForbidden, "只能为直属下级分配套餐")
|
||||
if userType == constants.UserTypeAgent {
|
||||
if targetShop.ParentID == nil || *targetShop.ParentID != allocatorShopID {
|
||||
return nil, errors.New(errors.CodeForbidden, "只能为直属下级分配套餐系列")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,49 +71,54 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐系列失败")
|
||||
}
|
||||
|
||||
if userType == constants.UserTypeAgent {
|
||||
myAllocation, err := s.allocationStore.GetByShopAndSeries(ctx, allocatorShopID, req.SeriesID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "检查分配权限失败")
|
||||
}
|
||||
if myAllocation == nil || myAllocation.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeForbidden, "您没有该套餐系列的分配权限")
|
||||
}
|
||||
// 检查是否已存在分配(跳过数据权限过滤,避免误判)
|
||||
skipCtx := pkggorm.SkipDataPermission(ctx)
|
||||
exists, err := s.seriesAllocationStore.ExistsByShopAndSeries(skipCtx, req.ShopID, req.SeriesID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "检查分配记录失败")
|
||||
}
|
||||
|
||||
existing, _ := s.allocationStore.GetByShopAndSeries(ctx, req.ShopID, req.SeriesID)
|
||||
if existing != nil {
|
||||
if exists {
|
||||
return nil, errors.New(errors.CodeConflict, "该店铺已分配此套餐系列")
|
||||
}
|
||||
|
||||
if err := s.validateOneTimeCommissionConfig(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: req.ShopID,
|
||||
SeriesID: req.SeriesID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
BaseCommissionMode: req.BaseCommission.Mode,
|
||||
BaseCommissionValue: req.BaseCommission.Value,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
// 处理一次性佣金配置
|
||||
allocation.EnableOneTimeCommission = req.EnableOneTimeCommission
|
||||
if req.EnableOneTimeCommission && req.OneTimeCommissionConfig != nil {
|
||||
cfg := req.OneTimeCommissionConfig
|
||||
allocation.OneTimeCommissionType = cfg.Type
|
||||
allocation.OneTimeCommissionTrigger = cfg.Trigger
|
||||
allocation.OneTimeCommissionThreshold = cfg.Threshold
|
||||
// fixed 类型需要保存 mode 和 value
|
||||
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||
allocation.OneTimeCommissionMode = cfg.Mode
|
||||
allocation.OneTimeCommissionValue = cfg.Value
|
||||
// 代理用户:检查自己是否有该系列的分配权限,且金额不能超过上级给的上限
|
||||
// 平台用户:无上限限制,可自由设定金额
|
||||
if userType == constants.UserTypeAgent {
|
||||
allocatorAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(skipCtx, allocatorShopID, req.SeriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "您没有该套餐系列的分配权限")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取分配权限失败")
|
||||
}
|
||||
if req.OneTimeCommissionAmount > allocatorAllocation.OneTimeCommissionAmount {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "一次性佣金金额不能超过您的分配上限")
|
||||
}
|
||||
}
|
||||
|
||||
// 处理强充配置
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: req.ShopID,
|
||||
SeriesID: req.SeriesID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
OneTimeCommissionAmount: req.OneTimeCommissionAmount,
|
||||
EnableOneTimeCommission: false,
|
||||
OneTimeCommissionTrigger: "",
|
||||
OneTimeCommissionThreshold: 0,
|
||||
EnableForceRecharge: false,
|
||||
ForceRechargeAmount: 0,
|
||||
ForceRechargeTriggerType: 2,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
if req.EnableOneTimeCommission != nil {
|
||||
allocation.EnableOneTimeCommission = *req.EnableOneTimeCommission
|
||||
}
|
||||
if req.OneTimeCommissionTrigger != "" {
|
||||
allocation.OneTimeCommissionTrigger = req.OneTimeCommissionTrigger
|
||||
}
|
||||
if req.OneTimeCommissionThreshold != nil {
|
||||
allocation.OneTimeCommissionThreshold = *req.OneTimeCommissionThreshold
|
||||
}
|
||||
if req.EnableForceRecharge != nil {
|
||||
allocation.EnableForceRecharge = *req.EnableForceRecharge
|
||||
}
|
||||
@@ -138,23 +131,15 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
|
||||
|
||||
allocation.Creator = currentUserID
|
||||
|
||||
if err := s.allocationStore.Create(ctx, allocation); err != nil {
|
||||
if err := s.seriesAllocationStore.Create(ctx, allocation); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建分配失败")
|
||||
}
|
||||
|
||||
// 如果是梯度类型,保存梯度配置
|
||||
if req.EnableOneTimeCommission && req.OneTimeCommissionConfig != nil &&
|
||||
req.OneTimeCommissionConfig.Type == model.OneTimeCommissionTypeTiered {
|
||||
if err := s.saveOneTimeCommissionTiers(ctx, allocation.ID, req.OneTimeCommissionConfig.Tiers, currentUserID); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建一次性佣金梯度配置失败")
|
||||
}
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, targetShop.ShopName, series.SeriesName)
|
||||
return s.buildResponse(ctx, allocation, targetShop.ShopName, series.SeriesName, series.SeriesCode)
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uint) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
allocation, err := s.allocationStore.GetByID(ctx, id)
|
||||
allocation, err := s.seriesAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
@@ -167,14 +152,16 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.ShopSeriesAllocationRe
|
||||
|
||||
shopName := ""
|
||||
seriesName := ""
|
||||
seriesCode := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
seriesCode = series.SeriesCode
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, shopName, seriesName)
|
||||
return s.buildResponse(ctx, allocation, shopName, seriesName, seriesCode)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeriesAllocationRequest) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
@@ -183,7 +170,10 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
allocation, err := s.allocationStore.GetByID(ctx, id)
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
allocatorShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
allocation, err := s.seriesAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
@@ -191,52 +181,27 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
|
||||
}
|
||||
|
||||
configChanged := false
|
||||
if req.BaseCommission != nil {
|
||||
if allocation.BaseCommissionMode != req.BaseCommission.Mode ||
|
||||
allocation.BaseCommissionValue != req.BaseCommission.Value {
|
||||
configChanged = true
|
||||
if req.OneTimeCommissionAmount != nil {
|
||||
newAmount := *req.OneTimeCommissionAmount
|
||||
if userType == constants.UserTypeAgent {
|
||||
allocatorAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, allocatorShopID, allocation.SeriesID)
|
||||
if err == nil && allocatorAllocation != nil {
|
||||
if newAmount > allocatorAllocation.OneTimeCommissionAmount {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "一次性佣金金额不能超过您的分配上限")
|
||||
}
|
||||
}
|
||||
}
|
||||
allocation.BaseCommissionMode = req.BaseCommission.Mode
|
||||
allocation.BaseCommissionValue = req.BaseCommission.Value
|
||||
allocation.OneTimeCommissionAmount = newAmount
|
||||
}
|
||||
|
||||
enableOneTimeCommission := allocation.EnableOneTimeCommission
|
||||
if req.EnableOneTimeCommission != nil {
|
||||
enableOneTimeCommission = *req.EnableOneTimeCommission
|
||||
}
|
||||
if err := s.validateOneTimeCommissionConfigForUpdate(enableOneTimeCommission, req.OneTimeCommissionConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oneTimeCommissionChanged := false
|
||||
if req.EnableOneTimeCommission != nil {
|
||||
if allocation.EnableOneTimeCommission != *req.EnableOneTimeCommission {
|
||||
oneTimeCommissionChanged = true
|
||||
}
|
||||
allocation.EnableOneTimeCommission = *req.EnableOneTimeCommission
|
||||
}
|
||||
if req.OneTimeCommissionConfig != nil && allocation.EnableOneTimeCommission {
|
||||
cfg := req.OneTimeCommissionConfig
|
||||
if allocation.OneTimeCommissionType != cfg.Type ||
|
||||
allocation.OneTimeCommissionTrigger != cfg.Trigger ||
|
||||
allocation.OneTimeCommissionThreshold != cfg.Threshold ||
|
||||
allocation.OneTimeCommissionMode != cfg.Mode ||
|
||||
allocation.OneTimeCommissionValue != cfg.Value {
|
||||
oneTimeCommissionChanged = true
|
||||
}
|
||||
allocation.OneTimeCommissionType = cfg.Type
|
||||
allocation.OneTimeCommissionTrigger = cfg.Trigger
|
||||
allocation.OneTimeCommissionThreshold = cfg.Threshold
|
||||
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||
allocation.OneTimeCommissionMode = cfg.Mode
|
||||
allocation.OneTimeCommissionValue = cfg.Value
|
||||
} else {
|
||||
allocation.OneTimeCommissionMode = ""
|
||||
allocation.OneTimeCommissionValue = 0
|
||||
}
|
||||
if req.OneTimeCommissionTrigger != nil {
|
||||
allocation.OneTimeCommissionTrigger = *req.OneTimeCommissionTrigger
|
||||
}
|
||||
if req.OneTimeCommissionThreshold != nil {
|
||||
allocation.OneTimeCommissionThreshold = *req.OneTimeCommissionThreshold
|
||||
}
|
||||
|
||||
if req.EnableForceRecharge != nil {
|
||||
allocation.EnableForceRecharge = *req.EnableForceRecharge
|
||||
}
|
||||
@@ -246,46 +211,36 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
|
||||
if req.ForceRechargeTriggerType != nil {
|
||||
allocation.ForceRechargeTriggerType = *req.ForceRechargeTriggerType
|
||||
}
|
||||
if req.Status != nil {
|
||||
allocation.Status = *req.Status
|
||||
}
|
||||
|
||||
allocation.Updater = currentUserID
|
||||
|
||||
if configChanged {
|
||||
if err := s.createNewConfigVersion(ctx, allocation); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建配置版本失败")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.allocationStore.Update(ctx, allocation); err != nil {
|
||||
if err := s.seriesAllocationStore.Update(ctx, allocation); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "更新分配失败")
|
||||
}
|
||||
|
||||
if oneTimeCommissionChanged && req.OneTimeCommissionConfig != nil &&
|
||||
req.OneTimeCommissionConfig.Type == model.OneTimeCommissionTypeTiered {
|
||||
if err := s.oneTimeCommissionTierStore.DeleteByAllocationID(ctx, allocation.ID); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "清理旧梯度配置失败")
|
||||
}
|
||||
if err := s.saveOneTimeCommissionTiers(ctx, allocation.ID, req.OneTimeCommissionConfig.Tiers, currentUserID); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "更新一次性佣金梯度配置失败")
|
||||
}
|
||||
}
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
|
||||
|
||||
shopName := ""
|
||||
seriesName := ""
|
||||
seriesCode := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
seriesCode = series.SeriesCode
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, shopName, seriesName)
|
||||
return s.buildResponse(ctx, allocation, shopName, seriesName, seriesCode)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
allocation, err := s.allocationStore.GetByID(ctx, id)
|
||||
skipCtx := pkggorm.SkipDataPermission(ctx)
|
||||
_, err := s.seriesAllocationStore.GetByID(skipCtx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
@@ -293,15 +248,15 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
|
||||
}
|
||||
|
||||
hasDependent, err := s.allocationStore.HasDependentAllocations(ctx, allocation.ShopID, allocation.SeriesID)
|
||||
count, err := s.packageAllocationStore.CountBySeriesAllocationID(skipCtx, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "检查依赖关系失败")
|
||||
return errors.Wrap(errors.CodeInternalError, err, "检查关联套餐分配失败")
|
||||
}
|
||||
if hasDependent {
|
||||
return errors.New(errors.CodeConflict, "存在下级依赖,无法删除")
|
||||
if count > 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "存在关联的套餐分配,无法删除")
|
||||
}
|
||||
|
||||
if err := s.allocationStore.Delete(ctx, id); err != nil {
|
||||
if err := s.seriesAllocationStore.Delete(skipCtx, id); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "删除分配失败")
|
||||
}
|
||||
|
||||
@@ -309,9 +264,6 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *dto.ShopSeriesAllocationListRequest) ([]*dto.ShopSeriesAllocationResponse, int64, error) {
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
@@ -331,14 +283,14 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesAllocationListReq
|
||||
if req.SeriesID != nil {
|
||||
filters["series_id"] = *req.SeriesID
|
||||
}
|
||||
if req.AllocatorShopID != nil {
|
||||
filters["allocator_shop_id"] = *req.AllocatorShopID
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
if shopID > 0 && userType == constants.UserTypeAgent {
|
||||
filters["allocator_shop_id"] = shopID
|
||||
}
|
||||
|
||||
allocations, total, err := s.allocationStore.List(ctx, opts, filters)
|
||||
allocations, total, err := s.seriesAllocationStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(errors.CodeInternalError, err, "查询分配列表失败")
|
||||
}
|
||||
@@ -350,233 +302,55 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesAllocationListReq
|
||||
|
||||
shopName := ""
|
||||
seriesName := ""
|
||||
seriesCode := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
seriesCode = series.SeriesCode
|
||||
}
|
||||
|
||||
resp, _ := s.buildResponse(ctx, a, shopName, seriesName)
|
||||
resp, _ := s.buildResponse(ctx, a, shopName, seriesName, seriesCode)
|
||||
responses[i] = resp
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.allocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
|
||||
}
|
||||
|
||||
if err := s.allocationStore.UpdateStatus(ctx, id, status, currentUserID); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新状态失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetParentCostPrice(ctx context.Context, shopID, packageID uint) (int64, error) {
|
||||
pkg, err := s.packageStore.GetByID(ctx, packageID)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
|
||||
}
|
||||
|
||||
shop, err := s.shopStore.GetByID(ctx, shopID)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(errors.CodeInternalError, err, "获取店铺失败")
|
||||
}
|
||||
|
||||
if shop.ParentID == nil || *shop.ParentID == 0 {
|
||||
return pkg.SuggestedCostPrice, nil
|
||||
}
|
||||
|
||||
return 0, errors.New(errors.CodeInvalidParam, "自动计算成本价功能已移除,请手动设置成本价")
|
||||
}
|
||||
|
||||
func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocation, shopName, seriesName string) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
allocatorShop, _ := s.shopStore.GetByID(ctx, a.AllocatorShopID)
|
||||
func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocation, shopName, seriesName, seriesCode string) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
allocatorShopName := ""
|
||||
if allocatorShop != nil {
|
||||
allocatorShopName = allocatorShop.ShopName
|
||||
}
|
||||
|
||||
resp := &dto.ShopSeriesAllocationResponse{
|
||||
ID: a.ID,
|
||||
ShopID: a.ShopID,
|
||||
ShopName: shopName,
|
||||
SeriesID: a.SeriesID,
|
||||
SeriesName: seriesName,
|
||||
AllocatorShopID: a.AllocatorShopID,
|
||||
AllocatorShopName: allocatorShopName,
|
||||
BaseCommission: dto.BaseCommissionConfig{
|
||||
Mode: a.BaseCommissionMode,
|
||||
Value: a.BaseCommissionValue,
|
||||
},
|
||||
EnableOneTimeCommission: a.EnableOneTimeCommission,
|
||||
EnableForceRecharge: a.EnableForceRecharge,
|
||||
ForceRechargeAmount: a.ForceRechargeAmount,
|
||||
ForceRechargeTriggerType: a.ForceRechargeTriggerType,
|
||||
Status: a.Status,
|
||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if a.EnableOneTimeCommission {
|
||||
cfg := &dto.OneTimeCommissionConfig{
|
||||
Type: a.OneTimeCommissionType,
|
||||
Trigger: a.OneTimeCommissionTrigger,
|
||||
Threshold: a.OneTimeCommissionThreshold,
|
||||
Mode: a.OneTimeCommissionMode,
|
||||
Value: a.OneTimeCommissionValue,
|
||||
if a.AllocatorShopID > 0 {
|
||||
allocatorShop, _ := s.shopStore.GetByID(ctx, a.AllocatorShopID)
|
||||
if allocatorShop != nil {
|
||||
allocatorShopName = allocatorShop.ShopName
|
||||
}
|
||||
if a.OneTimeCommissionType == model.OneTimeCommissionTypeTiered {
|
||||
tiers, err := s.oneTimeCommissionTierStore.ListByAllocationID(ctx, a.ID)
|
||||
if err == nil && len(tiers) > 0 {
|
||||
cfg.Tiers = make([]dto.OneTimeCommissionTierEntry, len(tiers))
|
||||
for i, t := range tiers {
|
||||
cfg.Tiers[i] = dto.OneTimeCommissionTierEntry{
|
||||
TierType: t.TierType,
|
||||
Threshold: t.ThresholdValue,
|
||||
Mode: t.CommissionMode,
|
||||
Value: t.CommissionValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.OneTimeCommissionConfig = cfg
|
||||
} else {
|
||||
allocatorShopName = "平台"
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return &dto.ShopSeriesAllocationResponse{
|
||||
ID: a.ID,
|
||||
ShopID: a.ShopID,
|
||||
ShopName: shopName,
|
||||
SeriesID: a.SeriesID,
|
||||
SeriesName: seriesName,
|
||||
SeriesCode: seriesCode,
|
||||
AllocatorShopID: a.AllocatorShopID,
|
||||
AllocatorShopName: allocatorShopName,
|
||||
OneTimeCommissionAmount: a.OneTimeCommissionAmount,
|
||||
EnableOneTimeCommission: a.EnableOneTimeCommission,
|
||||
OneTimeCommissionTrigger: a.OneTimeCommissionTrigger,
|
||||
OneTimeCommissionThreshold: a.OneTimeCommissionThreshold,
|
||||
EnableForceRecharge: a.EnableForceRecharge,
|
||||
ForceRechargeAmount: a.ForceRechargeAmount,
|
||||
ForceRechargeTriggerType: a.ForceRechargeTriggerType,
|
||||
Status: a.Status,
|
||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) createNewConfigVersion(ctx context.Context, allocation *model.ShopSeriesAllocation) error {
|
||||
now := time.Now()
|
||||
|
||||
if err := s.configStore.InvalidateCurrent(ctx, allocation.ID, now); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "失效当前配置版本失败")
|
||||
}
|
||||
|
||||
latestVersion, err := s.configStore.GetLatestVersion(ctx, allocation.ID)
|
||||
newVersion := 1
|
||||
if err == nil && latestVersion != nil {
|
||||
newVersion = latestVersion.Version + 1
|
||||
}
|
||||
|
||||
newConfig := &model.ShopSeriesAllocationConfig{
|
||||
AllocationID: allocation.ID,
|
||||
Version: newVersion,
|
||||
BaseCommissionMode: allocation.BaseCommissionMode,
|
||||
BaseCommissionValue: allocation.BaseCommissionValue,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
|
||||
if err := s.configStore.Create(ctx, newConfig); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建新配置版本失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) validateOneTimeCommissionConfig(req *dto.CreateShopSeriesAllocationRequest) error {
|
||||
if !req.EnableOneTimeCommission {
|
||||
return nil
|
||||
}
|
||||
if req.OneTimeCommissionConfig == nil {
|
||||
return errors.New(errors.CodeInvalidParam, "启用一次性佣金时必须提供配置")
|
||||
}
|
||||
cfg := req.OneTimeCommissionConfig
|
||||
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||
if cfg.Mode == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣模式")
|
||||
}
|
||||
if cfg.Value <= 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣金额")
|
||||
}
|
||||
} else if cfg.Type == model.OneTimeCommissionTypeTiered {
|
||||
if len(cfg.Tiers) == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "梯度类型一次性佣金必须提供梯度档位")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) validateOneTimeCommissionConfigForUpdate(enableOneTimeCommission bool, cfg *dto.OneTimeCommissionConfig) error {
|
||||
if !enableOneTimeCommission {
|
||||
return nil
|
||||
}
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||
if cfg.Mode == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣模式")
|
||||
}
|
||||
if cfg.Value <= 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣金额")
|
||||
}
|
||||
} else if cfg.Type == model.OneTimeCommissionTypeTiered {
|
||||
if len(cfg.Tiers) == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "梯度类型一次性佣金必须提供梯度档位")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) saveOneTimeCommissionTiers(ctx context.Context, allocationID uint, tiers []dto.OneTimeCommissionTierEntry, userID uint) error {
|
||||
if len(tiers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tierModels := make([]*model.ShopSeriesOneTimeCommissionTier, len(tiers))
|
||||
for i, t := range tiers {
|
||||
tierModels[i] = &model.ShopSeriesOneTimeCommissionTier{
|
||||
AllocationID: allocationID,
|
||||
TierType: t.TierType,
|
||||
ThresholdValue: t.Threshold,
|
||||
CommissionMode: t.Mode,
|
||||
CommissionValue: t.Value,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
tierModels[i].Creator = userID
|
||||
}
|
||||
|
||||
return s.oneTimeCommissionTierStore.BatchCreate(ctx, tierModels)
|
||||
}
|
||||
|
||||
func (s *Service) GetEffectiveConfig(ctx context.Context, allocationID uint, at time.Time) (*model.ShopSeriesAllocationConfig, error) {
|
||||
config, err := s.configStore.GetEffective(ctx, allocationID, at)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "未找到生效的配置版本")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取生效配置失败")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListConfigVersions(ctx context.Context, allocationID uint) ([]*model.ShopSeriesAllocationConfig, error) {
|
||||
_, err := s.allocationStore.GetByID(ctx, allocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
|
||||
}
|
||||
|
||||
configs, err := s.configStore.List(ctx, allocationID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取配置版本列表失败")
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
func (s *Service) GetByShopAndSeries(ctx context.Context, shopID, seriesID uint) (*model.ShopSeriesAllocation, error) {
|
||||
return s.seriesAllocationStore.GetByShopAndSeries(ctx, shopID, seriesID)
|
||||
}
|
||||
|
||||
@@ -203,3 +203,12 @@ func (s *DeviceStore) ListBySeriesID(ctx context.Context, seriesID uint) ([]*mod
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (s *DeviceStore) UpdateRechargeTrackingFields(ctx context.Context, deviceID uint, accumulatedJSON, triggeredJSON string) error {
|
||||
return s.db.WithContext(ctx).Model(&model.Device{}).
|
||||
Where("id = ?", deviceID).
|
||||
Updates(map[string]interface{}{
|
||||
"accumulated_recharge_by_series": accumulatedJSON,
|
||||
"first_recharge_triggered_by_series": triggeredJSON,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -401,3 +401,12 @@ func (s *IotCardStore) ListBySeriesID(ctx context.Context, seriesID uint) ([]*mo
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) UpdateRechargeTrackingFields(ctx context.Context, cardID uint, accumulatedJSON, triggeredJSON string) error {
|
||||
return s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("id = ?", cardID).
|
||||
Updates(map[string]interface{}{
|
||||
"accumulated_recharge_by_series": accumulatedJSON,
|
||||
"first_recharge_triggered_by_series": triggeredJSON,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ func (s *PackageSeriesStore) List(ctx context.Context, opts *store.QueryOptions,
|
||||
if status, ok := filters["status"]; ok {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if enableOneTime, ok := filters["enable_one_time_commission"].(bool); ok {
|
||||
query = query.Where("enable_one_time_commission = ?", enableOneTime)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
|
||||
@@ -18,17 +18,16 @@ func TestPackageStore_Create(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "PKG_TEST_001",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
PackageCode: "PKG_TEST_001",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
RealDataMB: 1024,
|
||||
CostPrice: 9900,
|
||||
SuggestedRetailPrice: 12800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, pkg)
|
||||
@@ -47,12 +46,12 @@ func TestPackageStore_GetByID(t *testing.T) {
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 2048,
|
||||
DataAmountMB: 2048,
|
||||
Price: 19900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 2048,
|
||||
CostPrice: 19900,
|
||||
SuggestedRetailPrice: 25800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
@@ -79,12 +78,12 @@ func TestPackageStore_GetByCode(t *testing.T) {
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 3072,
|
||||
DataAmountMB: 3072,
|
||||
Price: 29900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 3072,
|
||||
CostPrice: 29900,
|
||||
SuggestedRetailPrice: 39800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
@@ -111,24 +110,24 @@ func TestPackageStore_Update(t *testing.T) {
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 4096,
|
||||
DataAmountMB: 4096,
|
||||
Price: 39900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 4096,
|
||||
CostPrice: 39900,
|
||||
SuggestedRetailPrice: 49800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
pkg.PackageName = "测试套餐4-更新"
|
||||
pkg.Price = 49900
|
||||
pkg.CostPrice = 49900
|
||||
err := s.Update(ctx, pkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "测试套餐4-更新", updated.PackageName)
|
||||
assert.Equal(t, int64(49900), updated.Price)
|
||||
assert.Equal(t, int64(49900), updated.CostPrice)
|
||||
}
|
||||
|
||||
func TestPackageStore_Delete(t *testing.T) {
|
||||
@@ -142,12 +141,12 @@ func TestPackageStore_Delete(t *testing.T) {
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 1024,
|
||||
CostPrice: 9900,
|
||||
SuggestedRetailPrice: 12800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
@@ -170,12 +169,12 @@ func TestPackageStore_List(t *testing.T) {
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 1024,
|
||||
CostPrice: 9900,
|
||||
SuggestedRetailPrice: 12800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
},
|
||||
{
|
||||
PackageCode: "LIST_P_002",
|
||||
@@ -183,12 +182,12 @@ func TestPackageStore_List(t *testing.T) {
|
||||
SeriesID: 2,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
DataType: "real",
|
||||
RealDataMB: 10240,
|
||||
DataAmountMB: 10240,
|
||||
Price: 99900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 10240,
|
||||
CostPrice: 99900,
|
||||
SuggestedRetailPrice: 129800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
},
|
||||
{
|
||||
PackageCode: "LIST_P_003",
|
||||
@@ -196,12 +195,12 @@ func TestPackageStore_List(t *testing.T) {
|
||||
SeriesID: 3,
|
||||
PackageType: "addon",
|
||||
DurationMonths: 1,
|
||||
DataType: "virtual",
|
||||
VirtualDataMB: 5120,
|
||||
DataAmountMB: 5120,
|
||||
Price: 4900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
|
||||
VirtualDataMB: 5120,
|
||||
CostPrice: 4900,
|
||||
SuggestedRetailPrice: 6800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
},
|
||||
}
|
||||
for _, pkg := range pkgList {
|
||||
@@ -286,12 +285,12 @@ func TestPackageStore_UpdateStatus(t *testing.T) {
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 1024,
|
||||
CostPrice: 9900,
|
||||
SuggestedRetailPrice: 12800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
@@ -314,12 +313,12 @@ func TestPackageStore_UpdateShelfStatus(t *testing.T) {
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
|
||||
RealDataMB: 1024,
|
||||
CostPrice: 9900,
|
||||
SuggestedRetailPrice: 12800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
|
||||
@@ -56,8 +56,11 @@ func (s *ShopPackageAllocationStore) List(ctx context.Context, opts *store.Query
|
||||
if packageID, ok := filters["package_id"].(uint); ok && packageID > 0 {
|
||||
query = query.Where("package_id = ?", packageID)
|
||||
}
|
||||
if allocationID, ok := filters["allocation_id"].(uint); ok && allocationID > 0 {
|
||||
query = query.Where("allocation_id = ?", allocationID)
|
||||
if seriesAllocationID, ok := filters["series_allocation_id"].(uint); ok && seriesAllocationID > 0 {
|
||||
query = query.Where("series_allocation_id = ?", seriesAllocationID)
|
||||
}
|
||||
if allocatorShopID, ok := filters["allocator_shop_id"].(uint); ok {
|
||||
query = query.Where("allocator_shop_id = ?", allocatorShopID)
|
||||
}
|
||||
if status, ok := filters["status"].(int); ok && status > 0 {
|
||||
query = query.Where("status = ?", status)
|
||||
@@ -102,8 +105,33 @@ func (s *ShopPackageAllocationStore) GetByShopID(ctx context.Context, shopID uin
|
||||
return allocations, nil
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationStore) DeleteByAllocationID(ctx context.Context, allocationID uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Delete(&model.ShopPackageAllocation{}).Error
|
||||
func (s *ShopPackageAllocationStore) GetByShopAndPackages(ctx context.Context, shopID uint, packageIDs []uint) ([]*model.ShopPackageAllocation, error) {
|
||||
var allocations []*model.ShopPackageAllocation
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("shop_id = ? AND package_id IN ? AND status = 1", shopID, packageIDs).
|
||||
Find(&allocations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return allocations, nil
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationStore) GetBySeriesAllocationID(ctx context.Context, seriesAllocationID uint) ([]*model.ShopPackageAllocation, error) {
|
||||
var allocations []*model.ShopPackageAllocation
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("series_allocation_id = ? AND status = 1", seriesAllocationID).
|
||||
Find(&allocations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return allocations, nil
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationStore) CountBySeriesAllocationID(ctx context.Context, seriesAllocationID uint) (int64, error) {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.ShopPackageAllocation{}).
|
||||
Where("series_allocation_id = ?", seriesAllocationID).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ func TestShopPackageAllocationStore_Create(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: 1,
|
||||
PackageID: 1,
|
||||
AllocationID: 1,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: 1,
|
||||
PackageID: 1,
|
||||
AllocatorShopID: 0,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, allocation)
|
||||
@@ -36,11 +36,11 @@ func TestShopPackageAllocationStore_GetByID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: 2,
|
||||
PackageID: 2,
|
||||
AllocationID: 1,
|
||||
CostPrice: 6000,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: 2,
|
||||
PackageID: 2,
|
||||
AllocatorShopID: 0,
|
||||
CostPrice: 6000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
@@ -64,11 +64,11 @@ func TestShopPackageAllocationStore_GetByShopAndPackage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: 3,
|
||||
PackageID: 3,
|
||||
AllocationID: 1,
|
||||
CostPrice: 7000,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: 3,
|
||||
PackageID: 3,
|
||||
AllocatorShopID: 0,
|
||||
CostPrice: 7000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
@@ -92,11 +92,11 @@ func TestShopPackageAllocationStore_Update(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: 4,
|
||||
PackageID: 4,
|
||||
AllocationID: 1,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: 4,
|
||||
PackageID: 4,
|
||||
AllocatorShopID: 0,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
@@ -115,11 +115,11 @@ func TestShopPackageAllocationStore_Delete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: 5,
|
||||
PackageID: 5,
|
||||
AllocationID: 1,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: 5,
|
||||
PackageID: 5,
|
||||
AllocatorShopID: 0,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
@@ -136,9 +136,9 @@ func TestShopPackageAllocationStore_List(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
allocations := []*model.ShopPackageAllocation{
|
||||
{ShopID: 10, PackageID: 10, AllocationID: 1, CostPrice: 5000, Status: constants.StatusEnabled},
|
||||
{ShopID: 11, PackageID: 11, AllocationID: 1, CostPrice: 6000, Status: constants.StatusEnabled},
|
||||
{ShopID: 12, PackageID: 12, AllocationID: 2, CostPrice: 7000, Status: constants.StatusEnabled},
|
||||
{ShopID: 10, PackageID: 10, AllocatorShopID: 0, CostPrice: 5000, Status: constants.StatusEnabled},
|
||||
{ShopID: 11, PackageID: 11, AllocatorShopID: 0, CostPrice: 6000, Status: constants.StatusEnabled},
|
||||
{ShopID: 12, PackageID: 12, AllocatorShopID: 10, CostPrice: 7000, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, a := range allocations {
|
||||
require.NoError(t, s.Create(ctx, a))
|
||||
@@ -173,16 +173,6 @@ func TestShopPackageAllocationStore_List(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按分配ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"allocation_id": uint(1)}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(2))
|
||||
for _, a := range result {
|
||||
assert.Equal(t, uint(1), a.AllocationID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按状态过滤-启用状态值为1", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"status": 1}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
@@ -223,11 +213,11 @@ func TestShopPackageAllocationStore_UpdateStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: 20,
|
||||
PackageID: 20,
|
||||
AllocationID: 1,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: 20,
|
||||
PackageID: 20,
|
||||
AllocatorShopID: 0,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ShopSeriesAllocationConfigStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewShopSeriesAllocationConfigStore(db *gorm.DB) *ShopSeriesAllocationConfigStore {
|
||||
return &ShopSeriesAllocationConfigStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationConfigStore) Create(ctx context.Context, config *model.ShopSeriesAllocationConfig) error {
|
||||
return s.db.WithContext(ctx).Create(config).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationConfigStore) GetEffective(ctx context.Context, allocationID uint, at time.Time) (*model.ShopSeriesAllocationConfig, error) {
|
||||
var config model.ShopSeriesAllocationConfig
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Where("effective_from <= ?", at).
|
||||
Where("effective_to IS NULL OR effective_to > ?", at).
|
||||
First(&config).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationConfigStore) GetLatestVersion(ctx context.Context, allocationID uint) (*model.ShopSeriesAllocationConfig, error) {
|
||||
var config model.ShopSeriesAllocationConfig
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Order("version DESC").
|
||||
First(&config).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationConfigStore) InvalidateCurrent(ctx context.Context, allocationID uint, effectiveTo time.Time) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.ShopSeriesAllocationConfig{}).
|
||||
Where("allocation_id = ? AND effective_to IS NULL", allocationID).
|
||||
Update("effective_to", effectiveTo).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationConfigStore) List(ctx context.Context, allocationID uint) ([]*model.ShopSeriesAllocationConfig, error) {
|
||||
var configs []*model.ShopSeriesAllocationConfig
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Order("version DESC").
|
||||
Find(&configs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
@@ -30,7 +30,9 @@ func (s *ShopSeriesAllocationStore) GetByID(ctx context.Context, id uint) (*mode
|
||||
|
||||
func (s *ShopSeriesAllocationStore) GetByShopAndSeries(ctx context.Context, shopID, seriesID uint) (*model.ShopSeriesAllocation, error) {
|
||||
var allocation model.ShopSeriesAllocation
|
||||
if err := s.db.WithContext(ctx).Where("shop_id = ? AND series_id = ?", shopID, seriesID).First(&allocation).Error; err != nil {
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("shop_id = ? AND series_id = ?", shopID, seriesID).
|
||||
First(&allocation).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &allocation, nil
|
||||
@@ -56,7 +58,7 @@ func (s *ShopSeriesAllocationStore) List(ctx context.Context, opts *store.QueryO
|
||||
if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
|
||||
query = query.Where("series_id = ?", seriesID)
|
||||
}
|
||||
if allocatorShopID, ok := filters["allocator_shop_id"].(uint); ok && allocatorShopID > 0 {
|
||||
if allocatorShopID, ok := filters["allocator_shop_id"].(uint); ok {
|
||||
query = query.Where("allocator_shop_id = ?", allocatorShopID)
|
||||
}
|
||||
if status, ok := filters["status"].(int); ok && status > 0 {
|
||||
@@ -75,6 +77,8 @@ func (s *ShopSeriesAllocationStore) List(ctx context.Context, opts *store.QueryO
|
||||
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
} else {
|
||||
query = query.Order("id DESC")
|
||||
}
|
||||
|
||||
if err := query.Find(&allocations).Error; err != nil {
|
||||
@@ -94,31 +98,58 @@ func (s *ShopSeriesAllocationStore) UpdateStatus(ctx context.Context, id uint, s
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationStore) HasDependentAllocations(ctx context.Context, allocatorShopID, seriesID uint) (bool, error) {
|
||||
func (s *ShopSeriesAllocationStore) GetByShopID(ctx context.Context, shopID uint) ([]*model.ShopSeriesAllocation, error) {
|
||||
var allocations []*model.ShopSeriesAllocation
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("shop_id = ? AND status = 1", shopID).
|
||||
Find(&allocations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return allocations, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationStore) CountBySeriesID(ctx context.Context, seriesID uint) (int64, error) {
|
||||
var count int64
|
||||
err := s.db.WithContext(ctx).
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.ShopSeriesAllocation{}).
|
||||
Where("allocator_shop_id IN (SELECT id FROM tb_shop WHERE parent_id = ?)", allocatorShopID).
|
||||
Where("series_id = ?", seriesID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationStore) ExistsByShopAndSeries(ctx context.Context, shopID, seriesID uint) (bool, error) {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.ShopSeriesAllocation{}).
|
||||
Where("shop_id = ? AND series_id = ?", shopID, seriesID).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationStore) GetByShopID(ctx context.Context, shopID uint) ([]*model.ShopSeriesAllocation, error) {
|
||||
func (s *ShopSeriesAllocationStore) GetByAllocatorShopID(ctx context.Context, allocatorShopID uint) ([]*model.ShopSeriesAllocation, error) {
|
||||
var allocations []*model.ShopSeriesAllocation
|
||||
if err := s.db.WithContext(ctx).Where("shop_id = ? AND status = 1", shopID).Find(&allocations).Error; err != nil {
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("allocator_shop_id = ? AND status = 1", allocatorShopID).
|
||||
Find(&allocations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return allocations, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesAllocationStore) GetByAllocatorShopID(ctx context.Context, allocatorShopID uint) ([]*model.ShopSeriesAllocation, error) {
|
||||
var allocations []*model.ShopSeriesAllocation
|
||||
if err := s.db.WithContext(ctx).Where("allocator_shop_id = ?", allocatorShopID).Find(&allocations).Error; err != nil {
|
||||
func (s *ShopSeriesAllocationStore) GetIDsByShopIDsAndSeries(ctx context.Context, shopIDs []uint, seriesID uint) ([]uint, error) {
|
||||
if len(shopIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var ids []uint
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.ShopSeriesAllocation{}).
|
||||
Where("shop_id IN ? AND series_id = ? AND status = 1", shopIDs, seriesID).
|
||||
Pluck("id", &ids).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return allocations, nil
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestShopSeriesAllocationStore_GetByShopAndSeries(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
|
||||
// 创建测试数据
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 1,
|
||||
SeriesID: 100,
|
||||
AllocatorShopID: 0,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
t.Run("查询存在的分配", func(t *testing.T) {
|
||||
result, err := s.GetByShopAndSeries(ctx, 1, 100)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, uint(1), result.ShopID)
|
||||
assert.Equal(t, uint(100), result.SeriesID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的分配", func(t *testing.T) {
|
||||
result, err := s.GetByShopAndSeries(ctx, 999, 999)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 1,
|
||||
SeriesID: 100,
|
||||
AllocatorShopID: 0,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, allocation)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, allocation.ID)
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 1,
|
||||
SeriesID: 100,
|
||||
AllocatorShopID: 0,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
result, err := s.GetByID(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, allocation.ID, result.ID)
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
|
||||
// 创建测试数据
|
||||
allocations := []*model.ShopSeriesAllocation{
|
||||
{ShopID: 1, SeriesID: 100, AllocatorShopID: 0, Status: 1},
|
||||
{ShopID: 1, SeriesID: 101, AllocatorShopID: 0, Status: 1},
|
||||
{ShopID: 2, SeriesID: 100, AllocatorShopID: 0, Status: 1},
|
||||
}
|
||||
for _, a := range allocations {
|
||||
require.NoError(t, s.Create(ctx, a))
|
||||
}
|
||||
|
||||
t.Run("按店铺ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"shop_id": uint(1)}
|
||||
result, total, err := s.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, result, 2)
|
||||
})
|
||||
|
||||
t.Run("按系列ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"series_id": uint(100)}
|
||||
result, total, err := s.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, result, 2)
|
||||
})
|
||||
}
|
||||
@@ -68,3 +68,29 @@ func (s *ShopSeriesCommissionStatsStore) ListExpired(ctx context.Context, before
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesCommissionStatsStore) GetAggregatedStats(ctx context.Context, allocationIDs []uint, periodType string, now time.Time) (int64, int64, error) {
|
||||
if len(allocationIDs) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
var result struct {
|
||||
TotalSalesCount int64
|
||||
TotalSalesAmount int64
|
||||
}
|
||||
|
||||
err := s.db.WithContext(ctx).
|
||||
Model(&model.ShopSeriesCommissionStats{}).
|
||||
Select("COALESCE(SUM(total_sales_count), 0) as total_sales_count, COALESCE(SUM(total_sales_amount), 0) as total_sales_amount").
|
||||
Where("allocation_id IN ?", allocationIDs).
|
||||
Where("period_type = ?", periodType).
|
||||
Where("period_start <= ? AND period_end >= ?", now, now).
|
||||
Where("status = ?", model.StatsStatusActive).
|
||||
Scan(&result).Error
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return result.TotalSalesCount, result.TotalSalesAmount, nil
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ShopSeriesOneTimeCommissionTierStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewShopSeriesOneTimeCommissionTierStore(db *gorm.DB) *ShopSeriesOneTimeCommissionTierStore {
|
||||
return &ShopSeriesOneTimeCommissionTierStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ShopSeriesOneTimeCommissionTierStore) Create(ctx context.Context, tier *model.ShopSeriesOneTimeCommissionTier) error {
|
||||
return s.db.WithContext(ctx).Create(tier).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesOneTimeCommissionTierStore) BatchCreate(ctx context.Context, tiers []*model.ShopSeriesOneTimeCommissionTier) error {
|
||||
if len(tiers) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.db.WithContext(ctx).Create(&tiers).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesOneTimeCommissionTierStore) GetByID(ctx context.Context, id uint) (*model.ShopSeriesOneTimeCommissionTier, error) {
|
||||
var tier model.ShopSeriesOneTimeCommissionTier
|
||||
if err := s.db.WithContext(ctx).First(&tier, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tier, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesOneTimeCommissionTierStore) Update(ctx context.Context, tier *model.ShopSeriesOneTimeCommissionTier) error {
|
||||
return s.db.WithContext(ctx).Save(tier).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesOneTimeCommissionTierStore) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).Delete(&model.ShopSeriesOneTimeCommissionTier{}, id).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesOneTimeCommissionTierStore) ListByAllocationID(ctx context.Context, allocationID uint) ([]*model.ShopSeriesOneTimeCommissionTier, error) {
|
||||
var tiers []*model.ShopSeriesOneTimeCommissionTier
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Where("status = ?", 1).
|
||||
Order("threshold_value ASC").
|
||||
Find(&tiers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tiers, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesOneTimeCommissionTierStore) DeleteByAllocationID(ctx context.Context, allocationID uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Delete(&model.ShopSeriesOneTimeCommissionTier{}).Error
|
||||
}
|
||||
@@ -23,14 +23,14 @@ type CommissionStatsUpdatePayload struct {
|
||||
type CommissionStatsUpdateHandler struct {
|
||||
redis *redis.Client
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
allocationStore *postgres.ShopSeriesAllocationStore
|
||||
allocationStore *postgres.ShopPackageAllocationStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewCommissionStatsUpdateHandler(
|
||||
redis *redis.Client,
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore,
|
||||
allocationStore *postgres.ShopSeriesAllocationStore,
|
||||
allocationStore *postgres.ShopPackageAllocationStore,
|
||||
logger *zap.Logger,
|
||||
) *CommissionStatsUpdateHandler {
|
||||
return &CommissionStatsUpdateHandler{
|
||||
|
||||
Reference in New Issue
Block a user