refactor: 一次性佣金配置从套餐级别提升到系列级别
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:
2026-02-04 14:28:44 +08:00
parent fba8e9e76b
commit b18ecfeb55
106 changed files with 9899 additions and 6608 deletions

View File

@@ -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),
}
}

View File

@@ -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),

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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 更新套餐聚合参数

View File

@@ -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 更新套餐系列聚合参数

View File

@@ -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:"分配者店铺ID0表示平台分配"`
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

View File

@@ -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:"跳过数量(已存在)"`

View File

@@ -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:"分配者店铺ID0表示平台分配"`
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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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:分配者店铺ID0表示平台分配" 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 指定表名

View File

@@ -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:分配者店铺ID0表示平台分配" 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"
)

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,
})
}

View File

@@ -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 {

View File

@@ -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")
}
})
}
}

View File

@@ -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

View File

@@ -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)
})
}

View File

@@ -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

View File

@@ -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)
})
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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,
}
}

View File

@@ -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) {

View File

@@ -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)
})
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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{