重构: 店铺套餐分配系统从加价模式改为返佣模式
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m18s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m18s
主要变更: - 重构分配模型:从加价模式(pricing_mode/pricing_value)改为返佣模式(base_commission + tier_commission) - 删除独立的 my_package 接口,统一到 /api/admin/packages(通过数据权限自动过滤) - 新增批量分配和批量调价功能,支持事务和性能优化 - 新增配置版本管理,订单创建时锁定返佣配置 - 新增成本价历史记录,支持审计和纠纷处理 - 新增统计缓存系统(Redis + 异步任务),优化梯度返佣计算性能 - 删除冗余的梯度佣金独立 CRUD 接口(合并到分配配置中) - 归档 3 个已完成的 OpenSpec changes 并同步 8 个新 capabilities 到 main specs 技术细节: - 数据库迁移:000026_refactor_shop_package_allocation - 新增 Store:AllocationConfigStore, PriceHistoryStore, CommissionStatsStore - 新增 Service:BatchAllocationService, BatchPricingService, CommissionStatsService - 新增异步任务:统计更新、定时同步、周期归档 - 测试覆盖:批量操作集成测试、梯度佣金 CRUD 清理验证 影响: - API 变更:删除 4 个梯度 CRUD 接口(POST/GET/PUT/DELETE /:id/tiers) - API 新增:批量分配、批量调价接口 - 数据模型:重构 shop_series_allocation 表结构 - 性能优化:批量操作使用 CreateInBatches,统计使用 Redis 缓存 相关文档: - openspec/changes/archive/2026-01-28-refactor-shop-package-allocation/ - openspec/specs/agent-available-packages/ - openspec/specs/allocation-config-versioning/ - 等 8 个新 capability specs
This commit is contained in:
@@ -38,6 +38,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
Package: admin.NewPackageHandler(svc.Package),
|
||||
ShopSeriesAllocation: admin.NewShopSeriesAllocationHandler(svc.ShopSeriesAllocation),
|
||||
ShopPackageAllocation: admin.NewShopPackageAllocationHandler(svc.ShopPackageAllocation),
|
||||
MyPackage: admin.NewMyPackageHandler(svc.MyPackage),
|
||||
ShopPackageBatchAllocation: admin.NewShopPackageBatchAllocationHandler(svc.ShopPackageBatchAllocation),
|
||||
ShopPackageBatchPricing: admin.NewShopPackageBatchPricingHandler(svc.ShopPackageBatchPricing),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record"
|
||||
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
||||
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
|
||||
commissionStatsSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
|
||||
commissionWithdrawalSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal"
|
||||
commissionWithdrawalSettingSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal_setting"
|
||||
customerAccountSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_account"
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
|
||||
iotCardImportSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card_import"
|
||||
myCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/my_commission"
|
||||
myPackageSvc "github.com/break/junhong_cmp_fiber/internal/service/my_package"
|
||||
packageSvc "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
packageSeriesSvc "github.com/break/junhong_cmp_fiber/internal/service/package_series"
|
||||
permissionSvc "github.com/break/junhong_cmp_fiber/internal/service/permission"
|
||||
@@ -25,6 +25,8 @@ import (
|
||||
shopAccountSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_account"
|
||||
shopCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_commission"
|
||||
shopPackageAllocationSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_package_allocation"
|
||||
shopPackageBatchAllocationSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_allocation"
|
||||
shopPackageBatchPricingSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_pricing"
|
||||
shopSeriesAllocationSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_series_allocation"
|
||||
)
|
||||
|
||||
@@ -54,7 +56,9 @@ type services struct {
|
||||
Package *packageSvc.Service
|
||||
ShopSeriesAllocation *shopSeriesAllocationSvc.Service
|
||||
ShopPackageAllocation *shopPackageAllocationSvc.Service
|
||||
MyPackage *myPackageSvc.Service
|
||||
ShopPackageBatchAllocation *shopPackageBatchAllocationSvc.Service
|
||||
ShopPackageBatchPricing *shopPackageBatchPricingSvc.Service
|
||||
CommissionStats *commissionStatsSvc.Service
|
||||
}
|
||||
|
||||
func initServices(s *stores, deps *Dependencies) *services {
|
||||
@@ -81,9 +85,11 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
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),
|
||||
ShopSeriesAllocation: shopSeriesAllocationSvc.New(s.ShopSeriesAllocation, s.ShopSeriesCommissionTier, s.Shop, s.PackageSeries, s.Package),
|
||||
ShopPackageAllocation: shopPackageAllocationSvc.New(s.ShopPackageAllocation, s.ShopSeriesAllocation, s.Shop, s.Package),
|
||||
MyPackage: myPackageSvc.New(s.ShopSeriesAllocation, s.ShopPackageAllocation, s.PackageSeries, s.Package, s.Shop),
|
||||
Package: packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.ShopSeriesCommissionTier),
|
||||
ShopSeriesAllocation: shopSeriesAllocationSvc.New(s.ShopSeriesAllocation, s.ShopSeriesCommissionTier, s.ShopSeriesAllocationConfig, 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.ShopSeriesCommissionTier, s.ShopSeriesCommissionStats, s.Shop),
|
||||
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
|
||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,63 +5,69 @@ import (
|
||||
)
|
||||
|
||||
type stores struct {
|
||||
Account *postgres.AccountStore
|
||||
Shop *postgres.ShopStore
|
||||
Role *postgres.RoleStore
|
||||
Permission *postgres.PermissionStore
|
||||
AccountRole *postgres.AccountRoleStore
|
||||
RolePermission *postgres.RolePermissionStore
|
||||
PersonalCustomer *postgres.PersonalCustomerStore
|
||||
PersonalCustomerPhone *postgres.PersonalCustomerPhoneStore
|
||||
Wallet *postgres.WalletStore
|
||||
CommissionWithdrawalRequest *postgres.CommissionWithdrawalRequestStore
|
||||
CommissionRecord *postgres.CommissionRecordStore
|
||||
WalletTransaction *postgres.WalletTransactionStore
|
||||
CommissionWithdrawalSetting *postgres.CommissionWithdrawalSettingStore
|
||||
Enterprise *postgres.EnterpriseStore
|
||||
EnterpriseCardAuthorization *postgres.EnterpriseCardAuthorizationStore
|
||||
IotCard *postgres.IotCardStore
|
||||
IotCardImportTask *postgres.IotCardImportTaskStore
|
||||
Device *postgres.DeviceStore
|
||||
DeviceSimBinding *postgres.DeviceSimBindingStore
|
||||
DeviceImportTask *postgres.DeviceImportTaskStore
|
||||
AssetAllocationRecord *postgres.AssetAllocationRecordStore
|
||||
Carrier *postgres.CarrierStore
|
||||
PackageSeries *postgres.PackageSeriesStore
|
||||
Package *postgres.PackageStore
|
||||
ShopSeriesAllocation *postgres.ShopSeriesAllocationStore
|
||||
ShopSeriesCommissionTier *postgres.ShopSeriesCommissionTierStore
|
||||
ShopPackageAllocation *postgres.ShopPackageAllocationStore
|
||||
Account *postgres.AccountStore
|
||||
Shop *postgres.ShopStore
|
||||
Role *postgres.RoleStore
|
||||
Permission *postgres.PermissionStore
|
||||
AccountRole *postgres.AccountRoleStore
|
||||
RolePermission *postgres.RolePermissionStore
|
||||
PersonalCustomer *postgres.PersonalCustomerStore
|
||||
PersonalCustomerPhone *postgres.PersonalCustomerPhoneStore
|
||||
Wallet *postgres.WalletStore
|
||||
CommissionWithdrawalRequest *postgres.CommissionWithdrawalRequestStore
|
||||
CommissionRecord *postgres.CommissionRecordStore
|
||||
WalletTransaction *postgres.WalletTransactionStore
|
||||
CommissionWithdrawalSetting *postgres.CommissionWithdrawalSettingStore
|
||||
Enterprise *postgres.EnterpriseStore
|
||||
EnterpriseCardAuthorization *postgres.EnterpriseCardAuthorizationStore
|
||||
IotCard *postgres.IotCardStore
|
||||
IotCardImportTask *postgres.IotCardImportTaskStore
|
||||
Device *postgres.DeviceStore
|
||||
DeviceSimBinding *postgres.DeviceSimBindingStore
|
||||
DeviceImportTask *postgres.DeviceImportTaskStore
|
||||
AssetAllocationRecord *postgres.AssetAllocationRecordStore
|
||||
Carrier *postgres.CarrierStore
|
||||
PackageSeries *postgres.PackageSeriesStore
|
||||
Package *postgres.PackageStore
|
||||
ShopSeriesAllocation *postgres.ShopSeriesAllocationStore
|
||||
ShopSeriesCommissionTier *postgres.ShopSeriesCommissionTierStore
|
||||
ShopSeriesAllocationConfig *postgres.ShopSeriesAllocationConfigStore
|
||||
ShopPackageAllocation *postgres.ShopPackageAllocationStore
|
||||
ShopPackageAllocationPriceHistory *postgres.ShopPackageAllocationPriceHistoryStore
|
||||
ShopSeriesCommissionStats *postgres.ShopSeriesCommissionStatsStore
|
||||
}
|
||||
|
||||
func initStores(deps *Dependencies) *stores {
|
||||
return &stores{
|
||||
Account: postgres.NewAccountStore(deps.DB, deps.Redis),
|
||||
Shop: postgres.NewShopStore(deps.DB, deps.Redis),
|
||||
Role: postgres.NewRoleStore(deps.DB),
|
||||
Permission: postgres.NewPermissionStore(deps.DB),
|
||||
AccountRole: postgres.NewAccountRoleStore(deps.DB, deps.Redis),
|
||||
RolePermission: postgres.NewRolePermissionStore(deps.DB, deps.Redis),
|
||||
PersonalCustomer: postgres.NewPersonalCustomerStore(deps.DB, deps.Redis),
|
||||
PersonalCustomerPhone: postgres.NewPersonalCustomerPhoneStore(deps.DB),
|
||||
Wallet: postgres.NewWalletStore(deps.DB, deps.Redis),
|
||||
CommissionWithdrawalRequest: postgres.NewCommissionWithdrawalRequestStore(deps.DB, deps.Redis),
|
||||
CommissionRecord: postgres.NewCommissionRecordStore(deps.DB, deps.Redis),
|
||||
WalletTransaction: postgres.NewWalletTransactionStore(deps.DB, deps.Redis),
|
||||
CommissionWithdrawalSetting: postgres.NewCommissionWithdrawalSettingStore(deps.DB, deps.Redis),
|
||||
Enterprise: postgres.NewEnterpriseStore(deps.DB, deps.Redis),
|
||||
EnterpriseCardAuthorization: postgres.NewEnterpriseCardAuthorizationStore(deps.DB, deps.Redis),
|
||||
IotCard: postgres.NewIotCardStore(deps.DB, deps.Redis),
|
||||
IotCardImportTask: postgres.NewIotCardImportTaskStore(deps.DB, deps.Redis),
|
||||
Device: postgres.NewDeviceStore(deps.DB, deps.Redis),
|
||||
DeviceSimBinding: postgres.NewDeviceSimBindingStore(deps.DB, deps.Redis),
|
||||
DeviceImportTask: postgres.NewDeviceImportTaskStore(deps.DB, deps.Redis),
|
||||
AssetAllocationRecord: postgres.NewAssetAllocationRecordStore(deps.DB, deps.Redis),
|
||||
Carrier: postgres.NewCarrierStore(deps.DB),
|
||||
PackageSeries: postgres.NewPackageSeriesStore(deps.DB),
|
||||
Package: postgres.NewPackageStore(deps.DB),
|
||||
ShopSeriesAllocation: postgres.NewShopSeriesAllocationStore(deps.DB),
|
||||
ShopSeriesCommissionTier: postgres.NewShopSeriesCommissionTierStore(deps.DB),
|
||||
ShopPackageAllocation: postgres.NewShopPackageAllocationStore(deps.DB),
|
||||
Account: postgres.NewAccountStore(deps.DB, deps.Redis),
|
||||
Shop: postgres.NewShopStore(deps.DB, deps.Redis),
|
||||
Role: postgres.NewRoleStore(deps.DB),
|
||||
Permission: postgres.NewPermissionStore(deps.DB),
|
||||
AccountRole: postgres.NewAccountRoleStore(deps.DB, deps.Redis),
|
||||
RolePermission: postgres.NewRolePermissionStore(deps.DB, deps.Redis),
|
||||
PersonalCustomer: postgres.NewPersonalCustomerStore(deps.DB, deps.Redis),
|
||||
PersonalCustomerPhone: postgres.NewPersonalCustomerPhoneStore(deps.DB),
|
||||
Wallet: postgres.NewWalletStore(deps.DB, deps.Redis),
|
||||
CommissionWithdrawalRequest: postgres.NewCommissionWithdrawalRequestStore(deps.DB, deps.Redis),
|
||||
CommissionRecord: postgres.NewCommissionRecordStore(deps.DB, deps.Redis),
|
||||
WalletTransaction: postgres.NewWalletTransactionStore(deps.DB, deps.Redis),
|
||||
CommissionWithdrawalSetting: postgres.NewCommissionWithdrawalSettingStore(deps.DB, deps.Redis),
|
||||
Enterprise: postgres.NewEnterpriseStore(deps.DB, deps.Redis),
|
||||
EnterpriseCardAuthorization: postgres.NewEnterpriseCardAuthorizationStore(deps.DB, deps.Redis),
|
||||
IotCard: postgres.NewIotCardStore(deps.DB, deps.Redis),
|
||||
IotCardImportTask: postgres.NewIotCardImportTaskStore(deps.DB, deps.Redis),
|
||||
Device: postgres.NewDeviceStore(deps.DB, deps.Redis),
|
||||
DeviceSimBinding: postgres.NewDeviceSimBindingStore(deps.DB, deps.Redis),
|
||||
DeviceImportTask: postgres.NewDeviceImportTaskStore(deps.DB, deps.Redis),
|
||||
AssetAllocationRecord: postgres.NewAssetAllocationRecordStore(deps.DB, deps.Redis),
|
||||
Carrier: postgres.NewCarrierStore(deps.DB),
|
||||
PackageSeries: postgres.NewPackageSeriesStore(deps.DB),
|
||||
Package: postgres.NewPackageStore(deps.DB),
|
||||
ShopSeriesAllocation: postgres.NewShopSeriesAllocationStore(deps.DB),
|
||||
ShopSeriesCommissionTier: postgres.NewShopSeriesCommissionTierStore(deps.DB),
|
||||
ShopSeriesAllocationConfig: postgres.NewShopSeriesAllocationConfigStore(deps.DB),
|
||||
ShopPackageAllocation: postgres.NewShopPackageAllocationStore(deps.DB),
|
||||
ShopPackageAllocationPriceHistory: postgres.NewShopPackageAllocationPriceHistoryStore(deps.DB),
|
||||
ShopSeriesCommissionStats: postgres.NewShopSeriesCommissionStatsStore(deps.DB),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ type Handlers struct {
|
||||
Package *admin.PackageHandler
|
||||
ShopSeriesAllocation *admin.ShopSeriesAllocationHandler
|
||||
ShopPackageAllocation *admin.ShopPackageAllocationHandler
|
||||
MyPackage *admin.MyPackageHandler
|
||||
ShopPackageBatchAllocation *admin.ShopPackageBatchAllocationHandler
|
||||
ShopPackageBatchPricing *admin.ShopPackageBatchPricingHandler
|
||||
}
|
||||
|
||||
// Middlewares 封装所有中间件
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
myPackageService "github.com/break/junhong_cmp_fiber/internal/service/my_package"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
type MyPackageHandler struct {
|
||||
service *myPackageService.Service
|
||||
}
|
||||
|
||||
func NewMyPackageHandler(service *myPackageService.Service) *MyPackageHandler {
|
||||
return &MyPackageHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *MyPackageHandler) ListMyPackages(c *fiber.Ctx) error {
|
||||
var req dto.MyPackageListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
packages, total, err := h.service.ListMyPackages(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, packages, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
func (h *MyPackageHandler) GetMyPackage(c *fiber.Ctx) error {
|
||||
var req dto.IDReq
|
||||
if err := c.ParamsParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的套餐 ID")
|
||||
}
|
||||
|
||||
pkg, err := h.service.GetMyPackage(c.UserContext(), req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, pkg)
|
||||
}
|
||||
|
||||
func (h *MyPackageHandler) ListMySeriesAllocations(c *fiber.Ctx) error {
|
||||
var req dto.MySeriesAllocationListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
allocations, total, err := h.service.ListMySeriesAllocations(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, allocations, total, req.Page, req.PageSize)
|
||||
}
|
||||
@@ -110,3 +110,28 @@ func (h *ShopPackageAllocationHandler) UpdateStatus(c *fiber.Ctx) error {
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
// UpdateCostPrice 更新成本价
|
||||
func (h *ShopPackageAllocationHandler) UpdateCostPrice(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺套餐分配 ID")
|
||||
}
|
||||
|
||||
type UpdateCostPriceRequest struct {
|
||||
NewCostPrice int64 `json:"new_cost_price" validate:"required,min=0"`
|
||||
ChangeReason string `json:"change_reason" validate:"omitempty,max=255"`
|
||||
}
|
||||
|
||||
var req UpdateCostPriceRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
result, err := h.service.UpdateCostPrice(c.UserContext(), uint(id), req.NewCostPrice, req.ChangeReason)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
32
internal/handler/admin/shop_package_batch_allocation.go
Normal file
32
internal/handler/admin/shop_package_batch_allocation.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
batchAllocationService "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_allocation"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
type ShopPackageBatchAllocationHandler struct {
|
||||
service *batchAllocationService.Service
|
||||
}
|
||||
|
||||
func NewShopPackageBatchAllocationHandler(service *batchAllocationService.Service) *ShopPackageBatchAllocationHandler {
|
||||
return &ShopPackageBatchAllocationHandler{service: service}
|
||||
}
|
||||
|
||||
// BatchAllocate 批量分配套餐
|
||||
func (h *ShopPackageBatchAllocationHandler) BatchAllocate(c *fiber.Ctx) error {
|
||||
var req dto.BatchAllocatePackagesRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.service.BatchAllocate(c.UserContext(), &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
33
internal/handler/admin/shop_package_batch_pricing.go
Normal file
33
internal/handler/admin/shop_package_batch_pricing.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
batchPricingService "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_pricing"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
type ShopPackageBatchPricingHandler struct {
|
||||
service *batchPricingService.Service
|
||||
}
|
||||
|
||||
func NewShopPackageBatchPricingHandler(service *batchPricingService.Service) *ShopPackageBatchPricingHandler {
|
||||
return &ShopPackageBatchPricingHandler{service: service}
|
||||
}
|
||||
|
||||
// BatchUpdatePricing 批量调价
|
||||
func (h *ShopPackageBatchPricingHandler) BatchUpdatePricing(c *fiber.Ctx) error {
|
||||
var req dto.BatchUpdateCostPriceRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
result, err := h.service.BatchUpdatePricing(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, result)
|
||||
}
|
||||
@@ -110,78 +110,3 @@ func (h *ShopSeriesAllocationHandler) UpdateStatus(c *fiber.Ctx) error {
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *ShopSeriesAllocationHandler) AddTier(c *fiber.Ctx) error {
|
||||
allocationID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
}
|
||||
|
||||
var req dto.CreateCommissionTierRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
tier, err := h.service.AddTier(c.UserContext(), uint(allocationID), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, tier)
|
||||
}
|
||||
|
||||
func (h *ShopSeriesAllocationHandler) UpdateTier(c *fiber.Ctx) error {
|
||||
allocationID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
}
|
||||
|
||||
tierId, err := strconv.ParseUint(c.Params("tier_id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的佣金等级 ID")
|
||||
}
|
||||
|
||||
var req dto.UpdateCommissionTierRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
tier, err := h.service.UpdateTier(c.UserContext(), uint(allocationID), uint(tierId), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, tier)
|
||||
}
|
||||
|
||||
func (h *ShopSeriesAllocationHandler) DeleteTier(c *fiber.Ctx) error {
|
||||
allocationID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
}
|
||||
|
||||
tierId, err := strconv.ParseUint(c.Params("tier_id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的佣金等级 ID")
|
||||
}
|
||||
|
||||
if err := h.service.DeleteTier(c.UserContext(), uint(allocationID), uint(tierId)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *ShopSeriesAllocationHandler) ListTiers(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的店铺系列分配 ID")
|
||||
}
|
||||
|
||||
tiers, err := h.service.ListTiers(c.UserContext(), uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, tiers)
|
||||
}
|
||||
|
||||
19
internal/model/dto/allocation_config_dto.go
Normal file
19
internal/model/dto/allocation_config_dto.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package dto
|
||||
|
||||
// AllocationConfigResponse 配置版本响应
|
||||
type AllocationConfigResponse struct {
|
||||
ID uint `json:"id" description:"配置版本ID"`
|
||||
AllocationID uint `json:"allocation_id" description:"关联的分配ID"`
|
||||
Version int `json:"version" description:"配置版本号"`
|
||||
BaseCommissionMode string `json:"base_commission_mode" description:"基础返佣模式 (fixed:固定金额, percent:百分比)"`
|
||||
BaseCommissionValue int64 `json:"base_commission_value" description:"基础返佣值(分或千分比)"`
|
||||
EnableTierCommission bool `json:"enable_tier_commission" description:"是否启用梯度返佣"`
|
||||
EffectiveFrom string `json:"effective_from" description:"生效开始时间"`
|
||||
EffectiveTo string `json:"effective_to,omitempty" description:"生效结束时间(NULL表示当前生效)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
}
|
||||
|
||||
// AllocationConfigListResponse 配置版本列表响应
|
||||
type AllocationConfigListResponse struct {
|
||||
List []*AllocationConfigResponse `json:"list" description:"配置版本列表"`
|
||||
}
|
||||
30
internal/model/dto/allocation_price_history_dto.go
Normal file
30
internal/model/dto/allocation_price_history_dto.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package dto
|
||||
|
||||
// PriceHistoryResponse 成本价历史响应
|
||||
type PriceHistoryResponse struct {
|
||||
ID uint `json:"id" description:"历史记录ID"`
|
||||
AllocationID uint `json:"allocation_id" description:"关联的套餐分配ID"`
|
||||
OldCostPrice int64 `json:"old_cost_price" description:"原成本价(分)"`
|
||||
NewCostPrice int64 `json:"new_cost_price" description:"新成本价(分)"`
|
||||
ChangeReason string `json:"change_reason" description:"变更原因"`
|
||||
ChangedBy uint `json:"changed_by" description:"变更人ID"`
|
||||
ChangedByName string `json:"changed_by_name" description:"变更人姓名"`
|
||||
EffectiveFrom string `json:"effective_from" description:"生效时间"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
}
|
||||
|
||||
// PriceHistoryListRequest 成本价历史列表请求
|
||||
type PriceHistoryListRequest 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:"每页数量"`
|
||||
AllocationID *uint `json:"allocation_id" query:"allocation_id" validate:"omitempty" description:"套餐分配ID"`
|
||||
}
|
||||
|
||||
// PriceHistoryPageResult 成本价历史分页结果
|
||||
type PriceHistoryPageResult struct {
|
||||
List []*PriceHistoryResponse `json:"list" description:"历史记录列表"`
|
||||
Total int64 `json:"total" description:"总数"`
|
||||
Page int `json:"page" description:"当前页"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
TotalPages int `json:"total_pages" description:"总页数"`
|
||||
}
|
||||
@@ -62,8 +62,8 @@ type MySeriesAllocationResponse struct {
|
||||
SeriesID uint `json:"series_id" description:"套餐系列ID"`
|
||||
SeriesCode string `json:"series_code" description:"系列编码"`
|
||||
SeriesName string `json:"series_name" description:"系列名称"`
|
||||
PricingMode string `json:"pricing_mode" description:"加价模式 (fixed:固定金额, percent:百分比)"`
|
||||
PricingValue int64 `json:"pricing_value" description:"加价值"`
|
||||
BaseCommissionMode string `json:"base_commission_mode" description:"基础佣金模式 (fixed:固定金额, percent:百分比)"`
|
||||
BaseCommissionValue int64 `json:"base_commission_value" description:"基础佣金值"`
|
||||
AvailablePackageCount int `json:"available_package_count" description:"可售套餐数量"`
|
||||
AllocatorShopName string `json:"allocator_shop_name" description:"分配者店铺名称"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
|
||||
@@ -52,25 +52,37 @@ type UpdatePackageShelfStatusRequest struct {
|
||||
ShelfStatus int `json:"shelf_status" validate:"required,oneof=1 2" required:"true" description:"上架状态 (1:上架, 2:下架)"`
|
||||
}
|
||||
|
||||
// CommissionTierInfo 返佣梯度信息
|
||||
type CommissionTierInfo struct {
|
||||
CurrentRate string `json:"current_rate" description:"当前返佣比例"`
|
||||
NextThreshold *int64 `json:"next_threshold,omitempty" description:"下一档位阈值"`
|
||||
NextRate string `json:"next_rate,omitempty" description:"下一档位返佣比例"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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:"更新时间"`
|
||||
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:"梯度返佣信息(仅代理用户可见)"`
|
||||
}
|
||||
|
||||
// UpdatePackageParams 更新套餐聚合参数
|
||||
|
||||
26
internal/model/dto/shop_package_batch_allocation_dto.go
Normal file
26
internal/model/dto/shop_package_batch_allocation_dto.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package dto
|
||||
|
||||
// PriceAdjustment 价格调整配置
|
||||
type PriceAdjustment struct {
|
||||
Type string `json:"type" validate:"required,oneof=fixed percent" required:"true" description:"调整类型 (fixed:固定金额, percent:百分比)"`
|
||||
Value int64 `json:"value" validate:"required" required:"true" description:"调整值(分或千分比)"`
|
||||
}
|
||||
|
||||
// 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:"基础返佣配置"`
|
||||
EnableTierCommission bool `json:"enable_tier_commission" description:"是否启用梯度返佣"`
|
||||
TierConfig *TierCommissionConfig `json:"tier_config" validate:"omitempty" 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:"跳过数量(已存在)"`
|
||||
PackageIDs []uint `json:"package_ids" description:"分配的套餐ID列表"`
|
||||
}
|
||||
15
internal/model/dto/shop_package_batch_pricing_dto.go
Normal file
15
internal/model/dto/shop_package_batch_pricing_dto.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package dto
|
||||
|
||||
// BatchUpdateCostPriceRequest 批量调价请求
|
||||
type BatchUpdateCostPriceRequest struct {
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"店铺ID"`
|
||||
SeriesID *uint `json:"series_id" validate:"omitempty" description:"套餐系列ID(可选,不填则调整所有)"`
|
||||
PriceAdjustment PriceAdjustment `json:"price_adjustment" validate:"required" required:"true" description:"价格调整配置"`
|
||||
ChangeReason string `json:"change_reason" validate:"omitempty,max=255" maxLength:"255" description:"变更原因"`
|
||||
}
|
||||
|
||||
// BatchUpdateCostPriceResponse 批量调价响应
|
||||
type BatchUpdateCostPriceResponse struct {
|
||||
UpdatedCount int `json:"updated_count" description:"更新数量"`
|
||||
AffectedIDs []uint `json:"affected_ids" description:"受影响的分配ID列表"`
|
||||
}
|
||||
@@ -1,23 +1,39 @@
|
||||
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%)"`
|
||||
}
|
||||
|
||||
// TierCommissionConfig 梯度返佣配置
|
||||
type TierCommissionConfig struct {
|
||||
PeriodType string `json:"period_type" validate:"required,oneof=monthly quarterly yearly" required:"true" description:"周期类型 (monthly:月度, quarterly:季度, yearly:年度)"`
|
||||
TierType string `json:"tier_type" validate:"required,oneof=sales_count sales_amount" required:"true" description:"梯度类型 (sales_count:销量, sales_amount:销售额)"`
|
||||
Tiers []TierEntry `json:"tiers" validate:"required,min=1,dive" required:"true" description:"梯度档位列表"`
|
||||
}
|
||||
|
||||
// TierEntry 梯度档位条目
|
||||
type TierEntry struct {
|
||||
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"`
|
||||
PricingMode string `json:"pricing_mode" validate:"required,oneof=fixed percent" required:"true" description:"加价模式 (fixed:固定金额, percent:百分比)"`
|
||||
PricingValue int64 `json:"pricing_value" validate:"required,min=0" required:"true" minimum:"0" description:"加价值(分或千分比,如100=10%)"`
|
||||
OneTimeCommissionTrigger string `json:"one_time_commission_trigger" validate:"omitempty,oneof=one_time_recharge accumulated_recharge" description:"一次性佣金触发类型 (one_time_recharge:单次充值, accumulated_recharge:累计充值)"`
|
||||
OneTimeCommissionThreshold int64 `json:"one_time_commission_threshold" validate:"omitempty,min=0" minimum:"0" description:"一次性佣金触发阈值(分)"`
|
||||
OneTimeCommissionAmount int64 `json:"one_time_commission_amount" validate:"omitempty,min=0" minimum:"0" description:"一次性佣金金额(分)"`
|
||||
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:"基础返佣配置"`
|
||||
EnableTierCommission bool `json:"enable_tier_commission" description:"是否启用梯度返佣"`
|
||||
TierConfig *TierCommissionConfig `json:"tier_config" validate:"omitempty" description:"梯度返佣配置(启用梯度返佣时必填)"`
|
||||
}
|
||||
|
||||
// UpdateShopSeriesAllocationRequest 更新套餐系列分配请求
|
||||
type UpdateShopSeriesAllocationRequest struct {
|
||||
PricingMode *string `json:"pricing_mode" validate:"omitempty,oneof=fixed percent" description:"加价模式 (fixed:固定金额, percent:百分比)"`
|
||||
PricingValue *int64 `json:"pricing_value" validate:"omitempty,min=0" minimum:"0" description:"加价值(分或千分比)"`
|
||||
OneTimeCommissionTrigger *string `json:"one_time_commission_trigger" validate:"omitempty,oneof=one_time_recharge accumulated_recharge" description:"一次性佣金触发类型"`
|
||||
OneTimeCommissionThreshold *int64 `json:"one_time_commission_threshold" validate:"omitempty,min=0" minimum:"0" description:"一次性佣金触发阈值(分)"`
|
||||
OneTimeCommissionAmount *int64 `json:"one_time_commission_amount" validate:"omitempty,min=0" minimum:"0" description:"一次性佣金金额(分)"`
|
||||
BaseCommission *BaseCommissionConfig `json:"base_commission" validate:"omitempty" description:"基础返佣配置"`
|
||||
EnableTierCommission *bool `json:"enable_tier_commission" description:"是否启用梯度返佣"`
|
||||
TierConfig *TierCommissionConfig `json:"tier_config" validate:"omitempty" description:"梯度返佣配置"`
|
||||
}
|
||||
|
||||
// ShopSeriesAllocationListRequest 套餐系列分配列表请求
|
||||
@@ -36,22 +52,18 @@ type UpdateShopSeriesAllocationStatusRequest struct {
|
||||
|
||||
// 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:"分配者店铺名称"`
|
||||
PricingMode string `json:"pricing_mode" description:"加价模式 (fixed:固定金额, percent:百分比)"`
|
||||
PricingValue int64 `json:"pricing_value" description:"加价值(分或千分比)"`
|
||||
CalculatedCostPrice int64 `json:"calculated_cost_price" description:"计算后的成本价(分)"`
|
||||
OneTimeCommissionTrigger string `json:"one_time_commission_trigger" description:"一次性佣金触发类型"`
|
||||
OneTimeCommissionThreshold int64 `json:"one_time_commission_threshold" description:"一次性佣金触发阈值(分)"`
|
||||
OneTimeCommissionAmount int64 `json:"one_time_commission_amount" 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:"被分配的店铺名称"`
|
||||
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:"基础返佣配置"`
|
||||
EnableTierCommission bool `json:"enable_tier_commission" description:"是否启用梯度返佣"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 2:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// ShopSeriesAllocationPageResult 套餐系列分配分页结果
|
||||
@@ -74,77 +86,3 @@ type UpdateShopSeriesAllocationStatusParams struct {
|
||||
IDReq
|
||||
UpdateShopSeriesAllocationStatusRequest
|
||||
}
|
||||
|
||||
// CreateCommissionTierRequest 创建梯度佣金请求
|
||||
type CreateCommissionTierRequest struct {
|
||||
TierType string `json:"tier_type" validate:"required,oneof=sales_count sales_amount" required:"true" description:"梯度类型 (sales_count:销量, sales_amount:销售额)"`
|
||||
PeriodType string `json:"period_type" validate:"required,oneof=monthly quarterly yearly custom" required:"true" description:"周期类型 (monthly:月度, quarterly:季度, yearly:年度, custom:自定义)"`
|
||||
PeriodStartDate *string `json:"period_start_date" validate:"omitempty" description:"自定义周期开始日期(YYYY-MM-DD),当周期类型为custom时必填"`
|
||||
PeriodEndDate *string `json:"period_end_date" validate:"omitempty" description:"自定义周期结束日期(YYYY-MM-DD),当周期类型为custom时必填"`
|
||||
ThresholdValue int64 `json:"threshold_value" validate:"required,min=1" required:"true" minimum:"1" description:"阈值(销量或金额分)"`
|
||||
CommissionAmount int64 `json:"commission_amount" validate:"required,min=1" required:"true" minimum:"1" description:"佣金金额(分)"`
|
||||
}
|
||||
|
||||
// UpdateCommissionTierRequest 更新梯度佣金请求
|
||||
type UpdateCommissionTierRequest struct {
|
||||
TierType *string `json:"tier_type" validate:"omitempty,oneof=sales_count sales_amount" description:"梯度类型"`
|
||||
PeriodType *string `json:"period_type" validate:"omitempty,oneof=monthly quarterly yearly custom" description:"周期类型"`
|
||||
PeriodStartDate *string `json:"period_start_date" validate:"omitempty" description:"自定义周期开始日期"`
|
||||
PeriodEndDate *string `json:"period_end_date" validate:"omitempty" description:"自定义周期结束日期"`
|
||||
ThresholdValue *int64 `json:"threshold_value" validate:"omitempty,min=1" minimum:"1" description:"阈值"`
|
||||
CommissionAmount *int64 `json:"commission_amount" validate:"omitempty,min=1" minimum:"1" description:"佣金金额(分)"`
|
||||
}
|
||||
|
||||
// CommissionTierResponse 梯度佣金响应
|
||||
type CommissionTierResponse struct {
|
||||
ID uint `json:"id" description:"梯度ID"`
|
||||
AllocationID uint `json:"allocation_id" description:"关联的分配ID"`
|
||||
TierType string `json:"tier_type" description:"梯度类型 (sales_count:销量, sales_amount:销售额)"`
|
||||
PeriodType string `json:"period_type" description:"周期类型 (monthly:月度, quarterly:季度, yearly:年度, custom:自定义)"`
|
||||
PeriodStartDate string `json:"period_start_date,omitempty" description:"自定义周期开始日期"`
|
||||
PeriodEndDate string `json:"period_end_date,omitempty" description:"自定义周期结束日期"`
|
||||
ThresholdValue int64 `json:"threshold_value" description:"阈值"`
|
||||
CommissionAmount int64 `json:"commission_amount" description:"佣金金额(分)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
// CreateCommissionTierParams 创建梯度佣金聚合参数
|
||||
type CreateCommissionTierParams struct {
|
||||
IDReq
|
||||
CreateCommissionTierRequest
|
||||
}
|
||||
|
||||
// UpdateCommissionTierParams 更新梯度佣金聚合参数
|
||||
type UpdateCommissionTierParams struct {
|
||||
AllocationIDReq
|
||||
TierIDReq
|
||||
UpdateCommissionTierRequest
|
||||
}
|
||||
|
||||
// DeleteCommissionTierParams 删除梯度佣金聚合参数
|
||||
type DeleteCommissionTierParams struct {
|
||||
AllocationIDReq
|
||||
TierIDReq
|
||||
}
|
||||
|
||||
// AllocationIDReq 分配ID路径参数
|
||||
type AllocationIDReq struct {
|
||||
ID uint `path:"id" description:"分配ID" required:"true"`
|
||||
}
|
||||
|
||||
// TierIDReq 梯度ID路径参数
|
||||
type TierIDReq struct {
|
||||
TierID uint `path:"tier_id" description:"梯度ID" required:"true"`
|
||||
}
|
||||
|
||||
// CommissionTierListResult 梯度佣金列表结果
|
||||
type CommissionTierListResult struct {
|
||||
List []*CommissionTierResponse `json:"list" description:"梯度佣金列表"`
|
||||
}
|
||||
|
||||
// TierIDParams 梯度ID路径参数组合
|
||||
type TierIDParams struct {
|
||||
AllocationIDReq
|
||||
TierIDReq
|
||||
}
|
||||
|
||||
25
internal/model/shop_package_allocation_price_history.go
Normal file
25
internal/model/shop_package_allocation_price_history.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShopPackageAllocationPriceHistory 套餐成本价变更历史模型
|
||||
// 记录成本价调整历史,支持审计和纠纷处理
|
||||
// 每次成本价变更都会自动创建历史记录
|
||||
type ShopPackageAllocationPriceHistory struct {
|
||||
gorm.Model
|
||||
AllocationID uint `gorm:"column:allocation_id;index;not null;comment:关联的套餐分配ID(tb_shop_package_allocation.id)" json:"allocation_id"`
|
||||
OldCostPrice int64 `gorm:"column:old_cost_price;type:bigint;not null;comment:原成本价(分)" json:"old_cost_price"`
|
||||
NewCostPrice int64 `gorm:"column:new_cost_price;type:bigint;not null;comment:新成本价(分)" json:"new_cost_price"`
|
||||
ChangeReason string `gorm:"column:change_reason;type:varchar(255);comment:变更原因" json:"change_reason"`
|
||||
ChangedBy uint `gorm:"column:changed_by;type:bigint;not null;comment:变更人ID" json:"changed_by"`
|
||||
EffectiveFrom time.Time `gorm:"column:effective_from;type:timestamptz;not null;comment:生效时间" json:"effective_from"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (ShopPackageAllocationPriceHistory) TableName() string {
|
||||
return "tb_shop_package_allocation_price_history"
|
||||
}
|
||||
@@ -5,20 +5,18 @@ import (
|
||||
)
|
||||
|
||||
// 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"`
|
||||
PricingMode string `gorm:"column:pricing_mode;type:varchar(20);not null;comment:加价模式 fixed-固定金额 percent-百分比" json:"pricing_mode"`
|
||||
PricingValue int64 `gorm:"column:pricing_value;type:bigint;not null;comment:加价值(分或千分比,如100=10%)" json:"pricing_value"`
|
||||
OneTimeCommissionTrigger string `gorm:"column:one_time_commission_trigger;type:varchar(30);comment:一次性佣金触发类型 one_time_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"`
|
||||
OneTimeCommissionAmount int64 `gorm:"column:one_time_commission_amount;type:bigint;default:0;comment:一次性佣金金额(分)" json:"one_time_commission_amount"`
|
||||
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;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"`
|
||||
EnableTierCommission bool `gorm:"column:enable_tier_commission;type:boolean;not null;default:false;comment:是否启用梯度返佣" json:"enable_tier_commission"`
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
@@ -26,12 +24,12 @@ func (ShopSeriesAllocation) TableName() string {
|
||||
return "tb_shop_series_allocation"
|
||||
}
|
||||
|
||||
// 加价模式常量
|
||||
// 返佣模式常量
|
||||
const (
|
||||
// PricingModeFixed 固定金额加价
|
||||
PricingModeFixed = "fixed"
|
||||
// PricingModePercent 百分比加价(千分比)
|
||||
PricingModePercent = "percent"
|
||||
// CommissionModeFixed 固定金额返佣
|
||||
CommissionModeFixed = "fixed"
|
||||
// CommissionModePercent 百分比返佣(千分比)
|
||||
CommissionModePercent = "percent"
|
||||
)
|
||||
|
||||
// 一次性佣金触发类型常量
|
||||
|
||||
26
internal/model/shop_series_allocation_config.go
Normal file
26
internal/model/shop_series_allocation_config.go
Normal file
@@ -0,0 +1,26 @@
|
||||
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"`
|
||||
EnableTierCommission bool `gorm:"column:enable_tier_commission;type:boolean;not null;comment:是否启用梯度返佣(配置快照)" json:"enable_tier_commission"`
|
||||
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"
|
||||
}
|
||||
39
internal/model/shop_series_commission_stats.go
Normal file
39
internal/model/shop_series_commission_stats.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShopSeriesCommissionStats 梯度佣金统计缓存模型
|
||||
// 缓存梯度返佣的统计数据(销量/销售额),避免实时计算性能问题
|
||||
// 通过 Redis + 异步任务更新,支持乐观锁防止并发冲突
|
||||
type ShopSeriesCommissionStats struct {
|
||||
gorm.Model
|
||||
AllocationID uint `gorm:"column:allocation_id;index;not null;comment:关联的分配ID" json:"allocation_id"`
|
||||
PeriodType string `gorm:"column:period_type;type:varchar(20);not null;comment:周期类型 monthly-月度 quarterly-季度 yearly-年度" json:"period_type"`
|
||||
PeriodStart time.Time `gorm:"column:period_start;type:timestamptz;not null;comment:周期开始时间" json:"period_start"`
|
||||
PeriodEnd time.Time `gorm:"column:period_end;type:timestamptz;not null;comment:周期结束时间" json:"period_end"`
|
||||
TotalSalesCount int64 `gorm:"column:total_sales_count;type:bigint;not null;default:0;comment:总销售数量" json:"total_sales_count"`
|
||||
TotalSalesAmount int64 `gorm:"column:total_sales_amount;type:bigint;not null;default:0;comment:总销售金额(分)" json:"total_sales_amount"`
|
||||
CurrentTierID *uint `gorm:"column:current_tier_id;type:bigint;comment:当前匹配的梯度ID" json:"current_tier_id"`
|
||||
LastUpdatedAt time.Time `gorm:"column:last_updated_at;type:timestamptz;not null;comment:最后更新时间" json:"last_updated_at"`
|
||||
Version int `gorm:"column:version;type:int;not null;default:0;comment:版本号(乐观锁)" json:"version"`
|
||||
Status string `gorm:"column:status;type:varchar(20);not null;default:active;comment:状态 active-活跃 completed-已完成 cancelled-已取消" json:"status"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (ShopSeriesCommissionStats) TableName() string {
|
||||
return "tb_shop_series_commission_stats"
|
||||
}
|
||||
|
||||
// 统计状态常量
|
||||
const (
|
||||
// StatsStatusActive 活跃
|
||||
StatsStatusActive = "active"
|
||||
// StatsStatusCompleted 已完成
|
||||
StatsStatusCompleted = "completed"
|
||||
// StatsStatusCancelled 已取消
|
||||
StatsStatusCancelled = "cancelled"
|
||||
)
|
||||
@@ -7,18 +7,19 @@ import (
|
||||
)
|
||||
|
||||
// ShopSeriesCommissionTier 梯度佣金配置模型
|
||||
// 基于销量或销售额配置不同档位的一次性佣金奖励
|
||||
// 基于销量或销售额配置不同档位的返佣比例提升
|
||||
// 支持月度、季度、年度、自定义周期的统计
|
||||
type ShopSeriesCommissionTier 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"`
|
||||
PeriodType string `gorm:"column:period_type;type:varchar(20);not null;comment:周期类型 monthly-月度 quarterly-季度 yearly-年度 custom-自定义" json:"period_type"`
|
||||
PeriodStartDate *time.Time `gorm:"column:period_start_date;comment:自定义周期开始日期" json:"period_start_date"`
|
||||
PeriodEndDate *time.Time `gorm:"column:period_end_date;comment:自定义周期结束日期" json:"period_end_date"`
|
||||
ThresholdValue int64 `gorm:"column:threshold_value;type:bigint;not null;comment:阈值(销量或金额分)" json:"threshold_value"`
|
||||
CommissionAmount int64 `gorm:"column:commission_amount;type:bigint;not null;comment:佣金金额(分)" json:"commission_amount"`
|
||||
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"`
|
||||
PeriodType string `gorm:"column:period_type;type:varchar(20);not null;comment:周期类型 monthly-月度 quarterly-季度 yearly-年度 custom-自定义" json:"period_type"`
|
||||
PeriodStartDate *time.Time `gorm:"column:period_start_date;comment:自定义周期开始日期" json:"period_start_date"`
|
||||
PeriodEndDate *time.Time `gorm:"column:period_end_date;comment:自定义周期结束日期" json:"period_end_date"`
|
||||
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:percent;comment:达标后返佣模式 fixed-固定金额 percent-百分比" json:"commission_mode"`
|
||||
CommissionValue int64 `gorm:"column:commission_value;type:bigint;not null;comment:达标后返佣值(分或千分比)" json:"commission_value"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -82,8 +82,11 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
|
||||
if handlers.ShopPackageAllocation != nil {
|
||||
registerShopPackageAllocationRoutes(authGroup, handlers.ShopPackageAllocation, doc, basePath)
|
||||
}
|
||||
if handlers.MyPackage != nil {
|
||||
registerMyPackageRoutes(authGroup, handlers.MyPackage, doc, basePath)
|
||||
if handlers.ShopPackageBatchAllocation != nil {
|
||||
registerShopPackageBatchAllocationRoutes(authGroup, handlers.ShopPackageBatchAllocation, doc, basePath)
|
||||
}
|
||||
if handlers.ShopPackageBatchPricing != nil {
|
||||
registerShopPackageBatchPricingRoutes(authGroup, handlers.ShopPackageBatchPricing, doc, basePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
func registerMyPackageRoutes(router fiber.Router, handler *admin.MyPackageHandler, doc *openapi.Generator, basePath string) {
|
||||
Register(router, doc, basePath, "GET", "/my-packages", handler.ListMyPackages, RouteSpec{
|
||||
Summary: "我的可售套餐列表",
|
||||
Tags: []string{"代理可售套餐"},
|
||||
Input: new(dto.MyPackageListRequest),
|
||||
Output: new(dto.MyPackagePageResult),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "GET", "/my-packages/:id", handler.GetMyPackage, RouteSpec{
|
||||
Summary: "获取可售套餐详情",
|
||||
Tags: []string{"代理可售套餐"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.MyPackageDetailResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(router, doc, basePath, "GET", "/my-series-allocations", handler.ListMySeriesAllocations, RouteSpec{
|
||||
Summary: "我的被分配系列列表",
|
||||
Tags: []string{"代理可售套餐"},
|
||||
Input: new(dto.MySeriesAllocationListRequest),
|
||||
Output: new(dto.MySeriesAllocationPageResult),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
@@ -59,4 +59,12 @@ func registerShopPackageAllocationRoutes(router fiber.Router, handler *admin.Sho
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "PUT", "/:id/cost-price", handler.UpdateCostPrice, RouteSpec{
|
||||
Summary: "更新单套餐分配成本价",
|
||||
Tags: []string{"单套餐分配"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.ShopPackageAllocationResponse),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
22
internal/routes/shop_package_batch_allocation.go
Normal file
22
internal/routes/shop_package_batch_allocation.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
func registerShopPackageBatchAllocationRoutes(router fiber.Router, handler *admin.ShopPackageBatchAllocationHandler, doc *openapi.Generator, basePath string) {
|
||||
batchAllocations := router.Group("/shop-package-batch-allocations")
|
||||
groupPath := basePath + "/shop-package-batch-allocations"
|
||||
|
||||
Register(batchAllocations, doc, groupPath, "POST", "", handler.BatchAllocate, RouteSpec{
|
||||
Summary: "批量分配套餐",
|
||||
Tags: []string{"批量套餐分配"},
|
||||
Input: new(dto.BatchAllocatePackagesRequest),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
22
internal/routes/shop_package_batch_pricing.go
Normal file
22
internal/routes/shop_package_batch_pricing.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
func registerShopPackageBatchPricingRoutes(router fiber.Router, handler *admin.ShopPackageBatchPricingHandler, doc *openapi.Generator, basePath string) {
|
||||
batchPricing := router.Group("/shop-package-batch-pricing")
|
||||
groupPath := basePath + "/shop-package-batch-pricing"
|
||||
|
||||
Register(batchPricing, doc, groupPath, "POST", "", handler.BatchUpdatePricing, RouteSpec{
|
||||
Summary: "批量调价",
|
||||
Tags: []string{"批量套餐调价"},
|
||||
Input: new(dto.BatchUpdateCostPriceRequest),
|
||||
Output: new(dto.BatchUpdateCostPriceResponse),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
@@ -60,36 +60,4 @@ func registerShopSeriesAllocationRoutes(router fiber.Router, handler *admin.Shop
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "GET", "/:id/tiers", handler.ListTiers, RouteSpec{
|
||||
Summary: "获取梯度佣金列表",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.CommissionTierListResult),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "POST", "/:id/tiers", handler.AddTier, RouteSpec{
|
||||
Summary: "添加梯度佣金配置",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Input: new(dto.CreateCommissionTierParams),
|
||||
Output: new(dto.CommissionTierResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "PUT", "/:id/tiers/:tier_id", handler.UpdateTier, RouteSpec{
|
||||
Summary: "更新梯度佣金配置",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Input: new(dto.UpdateCommissionTierParams),
|
||||
Output: new(dto.CommissionTierResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(allocations, doc, groupPath, "DELETE", "/:id/tiers/:tier_id", handler.DeleteTier, RouteSpec{
|
||||
Summary: "删除梯度佣金配置",
|
||||
Tags: []string{"套餐系列分配"},
|
||||
Input: new(dto.TierIDParams),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
98
internal/service/commission_stats/service.go
Normal file
98
internal/service/commission_stats/service.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package commission_stats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
}
|
||||
|
||||
func New(statsStore *postgres.ShopSeriesCommissionStatsStore) *Service {
|
||||
return &Service{
|
||||
statsStore: statsStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetCurrentStats(ctx context.Context, allocationID uint, periodType string) (*model.ShopSeriesCommissionStats, error) {
|
||||
now := time.Now()
|
||||
|
||||
stats, err := s.statsStore.GetCurrent(ctx, allocationID, periodType, now)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "统计数据不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取统计数据失败: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStats(ctx context.Context, allocationID uint, periodType string, salesCount int64, salesAmount int64) error {
|
||||
now := time.Now()
|
||||
periodStart, periodEnd := calculatePeriod(now, periodType)
|
||||
|
||||
stats, err := s.statsStore.GetCurrent(ctx, allocationID, periodType, now)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("查询统计数据失败: %w", err)
|
||||
}
|
||||
|
||||
if stats == nil {
|
||||
stats = &model.ShopSeriesCommissionStats{
|
||||
AllocationID: allocationID,
|
||||
PeriodType: periodType,
|
||||
PeriodStart: periodStart,
|
||||
PeriodEnd: periodEnd,
|
||||
TotalSalesCount: salesCount,
|
||||
TotalSalesAmount: salesAmount,
|
||||
Status: "active",
|
||||
LastUpdatedAt: now,
|
||||
Version: 1,
|
||||
}
|
||||
return s.statsStore.Create(ctx, stats)
|
||||
}
|
||||
|
||||
return s.statsStore.IncrementSales(ctx, stats.ID, salesCount, salesAmount, stats.Version)
|
||||
}
|
||||
|
||||
func (s *Service) ArchiveCompletedPeriod(ctx context.Context, allocationID uint, periodType string) error {
|
||||
now := time.Now()
|
||||
stats, err := s.statsStore.GetCurrent(ctx, allocationID, periodType, now)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("查询统计数据失败: %w", err)
|
||||
}
|
||||
|
||||
return s.statsStore.CompletePeriod(ctx, stats.ID)
|
||||
}
|
||||
|
||||
func calculatePeriod(now time.Time, periodType string) (time.Time, time.Time) {
|
||||
var periodStart, periodEnd time.Time
|
||||
|
||||
switch periodType {
|
||||
case "monthly":
|
||||
periodStart = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
periodEnd = periodStart.AddDate(0, 1, 0).Add(-time.Second)
|
||||
case "quarterly":
|
||||
quarter := (int(now.Month()) - 1) / 3
|
||||
periodStart = time.Date(now.Year(), time.Month(quarter*3+1), 1, 0, 0, 0, 0, now.Location())
|
||||
periodEnd = periodStart.AddDate(0, 3, 0).Add(-time.Second)
|
||||
case "yearly":
|
||||
periodStart = time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location())
|
||||
periodEnd = periodStart.AddDate(1, 0, 0).Add(-time.Second)
|
||||
default:
|
||||
periodStart = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
periodEnd = periodStart.AddDate(0, 1, 0).Add(-time.Second)
|
||||
}
|
||||
|
||||
return periodStart, periodEnd
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
package my_package
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageStore *postgres.PackageStore
|
||||
shopStore *postgres.ShopStore
|
||||
}
|
||||
|
||||
func New(
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageStore: packageStore,
|
||||
shopStore: shopStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListMyPackages(ctx context.Context, req *dto.MyPackageListRequest) ([]*dto.MyPackageResponse, int64, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, 0, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
seriesAllocations, err := s.seriesAllocationStore.GetByShopID(ctx, shopID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("获取系列分配失败: %w", err)
|
||||
}
|
||||
|
||||
if len(seriesAllocations) == 0 {
|
||||
return []*dto.MyPackageResponse{}, 0, nil
|
||||
}
|
||||
|
||||
seriesIDs := make([]uint, 0, len(seriesAllocations))
|
||||
for _, sa := range seriesAllocations {
|
||||
seriesIDs = append(seriesIDs, sa.SeriesID)
|
||||
}
|
||||
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "id DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
filters["series_ids"] = seriesIDs
|
||||
filters["status"] = constants.StatusEnabled
|
||||
filters["shelf_status"] = 1
|
||||
|
||||
if req.SeriesID != nil {
|
||||
found := false
|
||||
for _, sid := range seriesIDs {
|
||||
if sid == *req.SeriesID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return []*dto.MyPackageResponse{}, 0, nil
|
||||
}
|
||||
filters["series_id"] = *req.SeriesID
|
||||
}
|
||||
if req.PackageType != nil {
|
||||
filters["package_type"] = *req.PackageType
|
||||
}
|
||||
|
||||
packages, total, err := s.packageStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询套餐列表失败: %w", err)
|
||||
}
|
||||
|
||||
packageOverrides, _ := s.packageAllocationStore.GetByShopID(ctx, shopID)
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
for _, po := range packageOverrides {
|
||||
overrideMap[po.PackageID] = po
|
||||
}
|
||||
|
||||
allocationMap := make(map[uint]*model.ShopSeriesAllocation)
|
||||
for _, sa := range seriesAllocations {
|
||||
allocationMap[sa.SeriesID] = sa
|
||||
}
|
||||
|
||||
responses := make([]*dto.MyPackageResponse, len(packages))
|
||||
for i, pkg := range packages {
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
seriesName := ""
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
costPrice, priceSource := s.GetCostPrice(ctx, shopID, pkg, allocationMap, overrideMap)
|
||||
|
||||
responses[i] = &dto.MyPackageResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: pkg.PackageType,
|
||||
SeriesID: pkg.SeriesID,
|
||||
SeriesName: seriesName,
|
||||
CostPrice: costPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
ProfitMargin: pkg.SuggestedRetailPrice - costPrice,
|
||||
PriceSource: priceSource,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
}
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetMyPackage(ctx context.Context, packageID uint) (*dto.MyPackageDetailResponse, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
pkg, err := s.packageStore.GetByID(ctx, packageID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
|
||||
}
|
||||
|
||||
seriesAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, shopID, pkg.SeriesID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeForbidden, "您没有该套餐的销售权限")
|
||||
}
|
||||
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
seriesName := ""
|
||||
if series != nil {
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{pkg.SeriesID: seriesAllocation}
|
||||
|
||||
packageOverride, _ := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
if packageOverride != nil {
|
||||
overrideMap[packageID] = packageOverride
|
||||
}
|
||||
|
||||
costPrice, priceSource := s.GetCostPrice(ctx, shopID, pkg, allocationMap, overrideMap)
|
||||
|
||||
return &dto.MyPackageDetailResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: pkg.PackageType,
|
||||
Description: "",
|
||||
SeriesID: pkg.SeriesID,
|
||||
SeriesName: seriesName,
|
||||
CostPrice: costPrice,
|
||||
SuggestedRetailPrice: pkg.SuggestedRetailPrice,
|
||||
ProfitMargin: pkg.SuggestedRetailPrice - costPrice,
|
||||
PriceSource: priceSource,
|
||||
Status: pkg.Status,
|
||||
ShelfStatus: pkg.ShelfStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListMySeriesAllocations(ctx context.Context, req *dto.MySeriesAllocationListRequest) ([]*dto.MySeriesAllocationResponse, int64, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, 0, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
allocations, err := s.seriesAllocationStore.GetByShopID(ctx, shopID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("获取系列分配失败: %w", err)
|
||||
}
|
||||
|
||||
total := int64(len(allocations))
|
||||
|
||||
page := req.Page
|
||||
pageSize := req.PageSize
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize == 0 {
|
||||
pageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if start >= int(total) {
|
||||
return []*dto.MySeriesAllocationResponse{}, total, nil
|
||||
}
|
||||
if end > int(total) {
|
||||
end = int(total)
|
||||
}
|
||||
|
||||
allocations = allocations[start:end]
|
||||
|
||||
responses := make([]*dto.MySeriesAllocationResponse, len(allocations))
|
||||
for i, a := range allocations {
|
||||
series, _ := s.packageSeriesStore.GetByID(ctx, a.SeriesID)
|
||||
seriesCode := ""
|
||||
seriesName := ""
|
||||
if series != nil {
|
||||
seriesCode = series.SeriesCode
|
||||
seriesName = series.SeriesName
|
||||
}
|
||||
|
||||
allocatorShop, _ := s.shopStore.GetByID(ctx, a.AllocatorShopID)
|
||||
allocatorShopName := ""
|
||||
if allocatorShop != nil {
|
||||
allocatorShopName = allocatorShop.ShopName
|
||||
}
|
||||
|
||||
availableCount := 0
|
||||
filters := map[string]interface{}{
|
||||
"series_id": a.SeriesID,
|
||||
"status": constants.StatusEnabled,
|
||||
"shelf_status": 1,
|
||||
}
|
||||
packages, _, _ := s.packageStore.List(ctx, &store.QueryOptions{Page: 1, PageSize: 1000}, filters)
|
||||
availableCount = len(packages)
|
||||
|
||||
responses[i] = &dto.MySeriesAllocationResponse{
|
||||
ID: a.ID,
|
||||
SeriesID: a.SeriesID,
|
||||
SeriesCode: seriesCode,
|
||||
SeriesName: seriesName,
|
||||
PricingMode: a.PricingMode,
|
||||
PricingValue: a.PricingValue,
|
||||
AvailablePackageCount: availableCount,
|
||||
AllocatorShopName: allocatorShopName,
|
||||
Status: a.Status,
|
||||
}
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetCostPrice(ctx context.Context, shopID uint, pkg *model.Package, allocationMap map[uint]*model.ShopSeriesAllocation, overrideMap map[uint]*model.ShopPackageAllocation) (int64, string) {
|
||||
if override, ok := overrideMap[pkg.ID]; ok && override.Status == constants.StatusEnabled {
|
||||
return override.CostPrice, dto.PriceSourcePackageOverride
|
||||
}
|
||||
|
||||
allocation, ok := allocationMap[pkg.SeriesID]
|
||||
if !ok {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
parentCostPrice := s.getParentCostPriceRecursive(ctx, allocation.AllocatorShopID, pkg)
|
||||
costPrice := s.calculateCostPrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue)
|
||||
|
||||
return costPrice, dto.PriceSourceSeriesPricing
|
||||
}
|
||||
|
||||
func (s *Service) getParentCostPriceRecursive(ctx context.Context, shopID uint, pkg *model.Package) int64 {
|
||||
shop, err := s.shopStore.GetByID(ctx, shopID)
|
||||
if err != nil {
|
||||
return pkg.SuggestedCostPrice
|
||||
}
|
||||
|
||||
if shop.ParentID == nil || *shop.ParentID == 0 {
|
||||
return pkg.SuggestedCostPrice
|
||||
}
|
||||
|
||||
allocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, shopID, pkg.SeriesID)
|
||||
if err != nil {
|
||||
return pkg.SuggestedCostPrice
|
||||
}
|
||||
|
||||
parentCostPrice := s.getParentCostPriceRecursive(ctx, allocation.AllocatorShopID, pkg)
|
||||
return s.calculateCostPrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue)
|
||||
}
|
||||
|
||||
func (s *Service) calculateCostPrice(parentCostPrice int64, pricingMode string, pricingValue int64) int64 {
|
||||
switch pricingMode {
|
||||
case model.PricingModeFixed:
|
||||
return parentCostPrice + pricingValue
|
||||
case model.PricingModePercent:
|
||||
return parentCostPrice + (parentCostPrice * pricingValue / 1000)
|
||||
default:
|
||||
return parentCostPrice
|
||||
}
|
||||
}
|
||||
@@ -1,820 +0,0 @@
|
||||
package my_package
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_GetCostPrice_Priority(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
// 创建测试数据:套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_001",
|
||||
SeriesName: "测试系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建测试数据:套餐
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_001",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000, // 基础成本价:50元
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
// 创建测试数据:上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺",
|
||||
ShopCode: "ALLOCATOR_001",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建测试数据:下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺",
|
||||
ShopCode: "SHOP_001",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建测试数据:系列分配(系列加价模式)
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000, // 固定加价:10元
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
t.Run("套餐覆盖优先级最高", func(t *testing.T) {
|
||||
// 创建套餐覆盖(覆盖成本价:80元)
|
||||
packageOverride := &model.ShopPackageAllocation{
|
||||
ShopID: shop.ID,
|
||||
PackageID: pkg.ID,
|
||||
AllocationID: seriesAllocation.ID,
|
||||
CostPrice: 8000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageAllocationStore.Create(ctx, packageOverride))
|
||||
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{series.ID: seriesAllocation}
|
||||
overrideMap := map[uint]*model.ShopPackageAllocation{pkg.ID: packageOverride}
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg, allocationMap, overrideMap)
|
||||
|
||||
// 应该返回套餐覆盖的成本价
|
||||
assert.Equal(t, int64(8000), costPrice)
|
||||
assert.Equal(t, dto.PriceSourcePackageOverride, priceSource)
|
||||
})
|
||||
|
||||
t.Run("套餐覆盖禁用时使用系列加价", func(t *testing.T) {
|
||||
pkg2 := &model.Package{
|
||||
PackageCode: "TEST_PKG_001_DISABLED",
|
||||
PackageName: "测试套餐禁用",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg2))
|
||||
|
||||
packageOverride := &model.ShopPackageAllocation{
|
||||
ShopID: shop.ID,
|
||||
PackageID: pkg2.ID,
|
||||
AllocationID: seriesAllocation.ID,
|
||||
CostPrice: 8000,
|
||||
Status: constants.StatusDisabled,
|
||||
}
|
||||
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{series.ID: seriesAllocation}
|
||||
overrideMap := map[uint]*model.ShopPackageAllocation{pkg2.ID: packageOverride}
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg2, allocationMap, overrideMap)
|
||||
|
||||
assert.Equal(t, int64(6000), costPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, priceSource)
|
||||
})
|
||||
|
||||
t.Run("无套餐覆盖时使用系列加价", func(t *testing.T) {
|
||||
allocationMap := map[uint]*model.ShopSeriesAllocation{series.ID: seriesAllocation}
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg, allocationMap, overrideMap)
|
||||
|
||||
// 应该返回系列加价的成本价:5000 + 1000 = 6000
|
||||
assert.Equal(t, int64(6000), costPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, priceSource)
|
||||
})
|
||||
|
||||
t.Run("无系列分配时返回0", func(t *testing.T) {
|
||||
allocationMap := make(map[uint]*model.ShopSeriesAllocation)
|
||||
overrideMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
|
||||
costPrice, priceSource := svc.GetCostPrice(ctx, shop.ID, pkg, allocationMap, overrideMap)
|
||||
|
||||
// 应该返回0和空的价格来源
|
||||
assert.Equal(t, int64(0), costPrice)
|
||||
assert.Equal(t, "", priceSource)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_calculateCostPrice(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
parentCostPrice int64
|
||||
pricingMode string
|
||||
pricingValue int64
|
||||
expectedCostPrice int64
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "固定金额加价模式",
|
||||
parentCostPrice: 5000, // 50元
|
||||
pricingMode: model.PricingModeFixed,
|
||||
pricingValue: 1000, // 加价10元
|
||||
expectedCostPrice: 6000, // 60元
|
||||
description: "固定加价:5000 + 1000 = 6000",
|
||||
},
|
||||
{
|
||||
name: "百分比加价模式",
|
||||
parentCostPrice: 5000, // 50元
|
||||
pricingMode: model.PricingModePercent,
|
||||
pricingValue: 200, // 20%(千分比:200/1000 = 20%)
|
||||
expectedCostPrice: 6000, // 50 + 50*20% = 60元
|
||||
description: "百分比加价:5000 + (5000 * 200 / 1000) = 6000",
|
||||
},
|
||||
{
|
||||
name: "百分比加价模式-10%",
|
||||
parentCostPrice: 10000, // 100元
|
||||
pricingMode: model.PricingModePercent,
|
||||
pricingValue: 100, // 10%(千分比:100/1000 = 10%)
|
||||
expectedCostPrice: 11000, // 100 + 100*10% = 110元
|
||||
description: "百分比加价:10000 + (10000 * 100 / 1000) = 11000",
|
||||
},
|
||||
{
|
||||
name: "未知加价模式返回原价",
|
||||
parentCostPrice: 5000,
|
||||
pricingMode: "unknown",
|
||||
pricingValue: 1000,
|
||||
expectedCostPrice: 5000, // 返回原价不变
|
||||
description: "未知模式:返回 parentCostPrice 不变",
|
||||
},
|
||||
{
|
||||
name: "零加价",
|
||||
parentCostPrice: 5000,
|
||||
pricingMode: model.PricingModeFixed,
|
||||
pricingValue: 0,
|
||||
expectedCostPrice: 5000,
|
||||
description: "零加价:5000 + 0 = 5000",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
costPrice := svc.calculateCostPrice(tt.parentCostPrice, tt.pricingMode, tt.pricingValue)
|
||||
assert.Equal(t, tt.expectedCostPrice, costPrice, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ListMyPackages_Authorization(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
t.Run("店铺ID为0时返回错误", func(t *testing.T) {
|
||||
// 创建不包含店铺ID的context
|
||||
ctxWithoutShop := context.WithValue(ctx, constants.ContextKeyShopID, uint(0))
|
||||
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithoutShop, req)
|
||||
|
||||
// 应该返回错误
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, packages)
|
||||
assert.Equal(t, int64(0), total)
|
||||
assert.Contains(t, err.Error(), "当前用户不属于任何店铺")
|
||||
})
|
||||
|
||||
t.Run("无系列分配时返回空列表", func(t *testing.T) {
|
||||
// 创建店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "SHOP_TEST_001",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建包含店铺ID的context
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
// 应该返回空列表,无错误
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, packages)
|
||||
assert.Equal(t, 0, len(packages))
|
||||
assert.Equal(t, int64(0), total)
|
||||
})
|
||||
|
||||
t.Run("有系列分配时返回套餐列表", func(t *testing.T) {
|
||||
// 创建套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_002",
|
||||
SeriesName: "测试系列2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建套餐
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_002",
|
||||
PackageName: "测试套餐2",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺2",
|
||||
ShopCode: "ALLOCATOR_002",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺2",
|
||||
ShopCode: "SHOP_002",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建系列分配
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
// 创建包含店铺ID的context
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
// 应该返回套餐列表
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, packages)
|
||||
assert.Equal(t, 1, len(packages))
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, pkg.ID, packages[0].ID)
|
||||
assert.Equal(t, pkg.PackageName, packages[0].PackageName)
|
||||
// 验证成本价计算:5000 + 1000 = 6000
|
||||
assert.Equal(t, int64(6000), packages[0].CostPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, packages[0].PriceSource)
|
||||
})
|
||||
|
||||
t.Run("分页参数默认值", func(t *testing.T) {
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_PAGING",
|
||||
SeriesName: "分页测试系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
for i := range 5 {
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_PAGING_" + string(byte('0'+byte(i))),
|
||||
PackageName: "分页测试套餐_" + string(byte('0'+byte(i))),
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
}
|
||||
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "分页上级店铺",
|
||||
ShopCode: "ALLOCATOR_PAGING",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "分页下级店铺",
|
||||
ShopCode: "SHOP_PAGING",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
req := &dto.MyPackageListRequest{}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, packages)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
assert.LessOrEqual(t, len(packages), constants.DefaultPageSize)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_ListMyPackages_Filtering(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
// 创建两个套餐系列
|
||||
series1 := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_FILTER_001",
|
||||
SeriesName: "系列1",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series1))
|
||||
|
||||
series2 := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_FILTER_002",
|
||||
SeriesName: "系列2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series2))
|
||||
|
||||
// 创建不同类型的套餐
|
||||
pkg1 := &model.Package{
|
||||
PackageCode: "PKG_FILTER_001",
|
||||
PackageName: "正式套餐1",
|
||||
SeriesID: series1.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg1))
|
||||
|
||||
pkg2 := &model.Package{
|
||||
PackageCode: "PKG_FILTER_002",
|
||||
PackageName: "附加套餐1",
|
||||
SeriesID: series2.ID,
|
||||
PackageType: "addon",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 512,
|
||||
DataAmountMB: 512,
|
||||
Price: 4900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 2500,
|
||||
SuggestedRetailPrice: 4900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg2))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺过滤",
|
||||
ShopCode: "ALLOCATOR_FILTER",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺过滤",
|
||||
ShopCode: "SHOP_FILTER",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 为两个系列都创建分配
|
||||
for _, series := range []*model.PackageSeries{series1, series2} {
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
}
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
t.Run("按系列ID过滤", func(t *testing.T) {
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
SeriesID: &series1.ID,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, 1, len(packages))
|
||||
assert.Equal(t, pkg1.ID, packages[0].ID)
|
||||
})
|
||||
|
||||
t.Run("按套餐类型过滤", func(t *testing.T) {
|
||||
packageType := "addon"
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
PackageType: &packageType,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, 1, len(packages))
|
||||
assert.Equal(t, pkg2.ID, packages[0].ID)
|
||||
})
|
||||
|
||||
t.Run("无效的系列ID返回空列表", func(t *testing.T) {
|
||||
invalidSeriesID := uint(99999)
|
||||
req := &dto.MyPackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
SeriesID: &invalidSeriesID,
|
||||
}
|
||||
|
||||
packages, total, err := svc.ListMyPackages(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), total)
|
||||
assert.Equal(t, 0, len(packages))
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_GetMyPackage(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
// 创建套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "DETAIL_SERIES",
|
||||
SeriesName: "详情系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建套餐
|
||||
pkg := &model.Package{
|
||||
PackageCode: "DETAIL_PKG",
|
||||
PackageName: "详情套餐",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
SuggestedCostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "上级店铺详情",
|
||||
ShopCode: "ALLOCATOR_DETAIL",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "下级店铺详情",
|
||||
ShopCode: "SHOP_DETAIL",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建系列分配
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
|
||||
t.Run("店铺ID为0时返回错误", func(t *testing.T) {
|
||||
ctxWithoutShop := context.WithValue(ctx, constants.ContextKeyShopID, uint(0))
|
||||
_, err := svc.GetMyPackage(ctxWithoutShop, pkg.ID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "当前用户不属于任何店铺")
|
||||
})
|
||||
|
||||
t.Run("成功获取套餐详情", func(t *testing.T) {
|
||||
detail, err := svc.GetMyPackage(ctxWithShop, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, detail)
|
||||
assert.Equal(t, pkg.ID, detail.ID)
|
||||
assert.Equal(t, pkg.PackageName, detail.PackageName)
|
||||
assert.Equal(t, series.SeriesName, detail.SeriesName)
|
||||
// 验证成本价:5000 + 1000 = 6000
|
||||
assert.Equal(t, int64(6000), detail.CostPrice)
|
||||
assert.Equal(t, dto.PriceSourceSeriesPricing, detail.PriceSource)
|
||||
})
|
||||
|
||||
t.Run("无权限访问套餐时返回错误", func(t *testing.T) {
|
||||
// 创建另一个没有系列分配的店铺
|
||||
otherShop := &model.Shop{
|
||||
ShopName: "其他店铺",
|
||||
ShopCode: "OTHER_SHOP",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000002",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, otherShop))
|
||||
|
||||
ctxWithOtherShop := context.WithValue(ctx, constants.ContextKeyShopID, otherShop.ID)
|
||||
_, err := svc.GetMyPackage(ctxWithOtherShop, pkg.ID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "您没有该套餐的销售权限")
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_ListMySeriesAllocations(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageAllocationStore := postgres.NewShopPackageAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, nil)
|
||||
|
||||
// 创建 Service
|
||||
svc := New(seriesAllocationStore, packageAllocationStore, packageSeriesStore, packageStore, shopStore)
|
||||
|
||||
t.Run("店铺ID为0时返回错误", func(t *testing.T) {
|
||||
ctxWithoutShop := context.WithValue(ctx, constants.ContextKeyShopID, uint(0))
|
||||
req := &dto.MySeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
_, _, err := svc.ListMySeriesAllocations(ctxWithoutShop, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "当前用户不属于任何店铺")
|
||||
})
|
||||
|
||||
t.Run("无系列分配时返回空列表", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "分配测试店铺",
|
||||
ShopCode: "ALLOC_SHOP",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
req := &dto.MySeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
allocations, total, err := svc.ListMySeriesAllocations(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, allocations)
|
||||
assert.Equal(t, 0, len(allocations))
|
||||
assert.Equal(t, int64(0), total)
|
||||
})
|
||||
|
||||
t.Run("成功列表系列分配", func(t *testing.T) {
|
||||
// 创建套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "ALLOC_SERIES",
|
||||
SeriesName: "分配系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
// 创建上级店铺
|
||||
allocatorShop := &model.Shop{
|
||||
ShopName: "分配者店铺",
|
||||
ShopCode: "ALLOCATOR_ALLOC",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000000",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, allocatorShop))
|
||||
|
||||
// 创建下级店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "被分配店铺",
|
||||
ShopCode: "ALLOCATED_SHOP",
|
||||
Status: constants.StatusEnabled,
|
||||
Level: 2,
|
||||
ParentID: &allocatorShop.ID,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
// 创建系列分配
|
||||
seriesAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: allocatorShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, seriesAllocation))
|
||||
|
||||
ctxWithShop := context.WithValue(ctx, constants.ContextKeyShopID, shop.ID)
|
||||
req := &dto.MySeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
allocations, total, err := svc.ListMySeriesAllocations(ctxWithShop, req)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, allocations)
|
||||
assert.Equal(t, 1, len(allocations))
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, series.SeriesName, allocations[0].SeriesName)
|
||||
assert.Equal(t, allocatorShop.ShopName, allocations[0].AllocatorShopName)
|
||||
})
|
||||
}
|
||||
@@ -17,14 +17,26 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
commissionTierStore *postgres.ShopSeriesCommissionTierStore
|
||||
}
|
||||
|
||||
func New(packageStore *postgres.PackageStore, packageSeriesStore *postgres.PackageSeriesStore) *Service {
|
||||
func New(
|
||||
packageStore *postgres.PackageStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
commissionTierStore *postgres.ShopSeriesCommissionTierStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
commissionTierStore: commissionTierStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +51,16 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
|
||||
return nil, errors.New(errors.CodeConflict, "套餐编码已存在")
|
||||
}
|
||||
|
||||
var seriesName *string
|
||||
if req.SeriesID != nil && *req.SeriesID > 0 {
|
||||
_, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取套餐系列失败: %w", err)
|
||||
}
|
||||
seriesName = &series.SeriesName
|
||||
}
|
||||
|
||||
pkg := &model.Package{
|
||||
@@ -85,7 +99,9 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
|
||||
return nil, fmt.Errorf("创建套餐失败: %w", err)
|
||||
}
|
||||
|
||||
return s.toResponse(pkg), nil
|
||||
resp := s.toResponse(ctx, pkg)
|
||||
resp.SeriesName = seriesName
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error) {
|
||||
@@ -96,7 +112,16 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error
|
||||
}
|
||||
return nil, fmt.Errorf("获取套餐失败: %w", err)
|
||||
}
|
||||
return s.toResponse(pkg), nil
|
||||
|
||||
resp := s.toResponse(ctx, pkg)
|
||||
// 查询系列名称
|
||||
if pkg.SeriesID > 0 {
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||
if err == nil {
|
||||
resp.SeriesName = &series.SeriesName
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageRequest) (*dto.PackageResponse, error) {
|
||||
@@ -113,8 +138,9 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
|
||||
return nil, fmt.Errorf("获取套餐失败: %w", err)
|
||||
}
|
||||
|
||||
var seriesName *string
|
||||
if req.SeriesID != nil && *req.SeriesID > 0 {
|
||||
_, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *req.SeriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在")
|
||||
@@ -122,6 +148,13 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
|
||||
return nil, fmt.Errorf("获取套餐系列失败: %w", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if req.PackageName != nil {
|
||||
@@ -160,7 +193,9 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
|
||||
return nil, fmt.Errorf("更新套餐失败: %w", err)
|
||||
}
|
||||
|
||||
return s.toResponse(pkg), nil
|
||||
resp := s.toResponse(ctx, pkg)
|
||||
resp.SeriesName = seriesName
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
@@ -214,9 +249,40 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
return nil, 0, fmt.Errorf("查询套餐列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 收集所有唯一的 series_id
|
||||
seriesIDMap := make(map[uint]bool)
|
||||
for _, pkg := range packages {
|
||||
if pkg.SeriesID > 0 {
|
||||
seriesIDMap[pkg.SeriesID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询套餐系列
|
||||
seriesMap := make(map[uint]string)
|
||||
if len(seriesIDMap) > 0 {
|
||||
seriesIDs := make([]uint, 0, len(seriesIDMap))
|
||||
for id := range seriesIDMap {
|
||||
seriesIDs = append(seriesIDs, id)
|
||||
}
|
||||
seriesList, err := s.packageSeriesStore.GetByIDs(ctx, seriesIDs)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("批量查询套餐系列失败: %w", err)
|
||||
}
|
||||
for _, series := range seriesList {
|
||||
seriesMap[series.ID] = series.SeriesName
|
||||
}
|
||||
}
|
||||
|
||||
// 构建响应,填充系列名称
|
||||
responses := make([]*dto.PackageResponse, len(packages))
|
||||
for i, pkg := range packages {
|
||||
responses[i] = s.toResponse(pkg)
|
||||
resp := s.toResponse(ctx, pkg)
|
||||
if pkg.SeriesID > 0 {
|
||||
if seriesName, ok := seriesMap[pkg.SeriesID]; ok {
|
||||
resp.SeriesName = &seriesName
|
||||
}
|
||||
}
|
||||
responses[i] = resp
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
@@ -278,12 +344,13 @@ func (s *Service) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus in
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) toResponse(pkg *model.Package) *dto.PackageResponse {
|
||||
func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.PackageResponse {
|
||||
var seriesID *uint
|
||||
if pkg.SeriesID > 0 {
|
||||
seriesID = &pkg.SeriesID
|
||||
}
|
||||
return &dto.PackageResponse{
|
||||
|
||||
resp := &dto.PackageResponse{
|
||||
ID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
@@ -302,4 +369,55 @@ func (s *Service) toResponse(pkg *model.Package) *dto.PackageResponse {
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if userType == constants.UserTypeAgent && shopID > 0 {
|
||||
allocation, err := s.packageAllocationStore.GetByShopAndPackage(ctx, shopID, pkg.ID)
|
||||
if err == nil && allocation != nil {
|
||||
resp.CostPrice = &allocation.CostPrice
|
||||
profitMargin := pkg.SuggestedRetailPrice - allocation.CostPrice
|
||||
resp.ProfitMargin = &profitMargin
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if seriesAllocation.EnableTierCommission {
|
||||
tiers, err := s.commissionTierStore.ListByAllocationID(ctx, allocationID)
|
||||
if err == nil && len(tiers) > 0 {
|
||||
tier := tiers[0]
|
||||
info.NextThreshold = &tier.ThresholdValue
|
||||
if tier.CommissionMode == constants.CommissionModeFixed {
|
||||
nextRate := fmt.Sprintf("%.2f元/单", float64(tier.CommissionValue)/100)
|
||||
info.NextRate = nextRate
|
||||
} else {
|
||||
nextRate := fmt.Sprintf("%.1f%%", float64(tier.CommissionValue)/10)
|
||||
info.NextRate = nextRate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"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/pkg/constants"
|
||||
@@ -24,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)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -97,7 +98,7 @@ func TestPackageService_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -167,7 +168,7 @@ func TestPackageService_UpdateShelfStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -254,7 +255,7 @@ func TestPackageService_Get(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -292,7 +293,7 @@ func TestPackageService_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -341,7 +342,7 @@ func TestPackageService_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -376,7 +377,7 @@ func TestPackageService_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -454,3 +455,135 @@ func TestPackageService_List(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
// 创建套餐系列
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("SERIES_%d", time.Now().UnixNano()),
|
||||
SeriesName: "测试套餐系列",
|
||||
Description: "用于测试系列名称字段",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
series.Creator = 1
|
||||
err := packageSeriesStore.Create(ctx, series)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("创建套餐时返回系列名称", func(t *testing.T) {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_SERIES"),
|
||||
PackageName: "带系列的套餐",
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp.SeriesName)
|
||||
assert.Equal(t, series.SeriesName, *resp.SeriesName)
|
||||
})
|
||||
|
||||
t.Run("获取套餐时返回系列名称", func(t *testing.T) {
|
||||
// 先创建一个套餐
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_GET_SERIES"),
|
||||
PackageName: "获取测试套餐",
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 获取套餐
|
||||
resp, err := svc.Get(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp.SeriesName)
|
||||
assert.Equal(t, series.SeriesName, *resp.SeriesName)
|
||||
})
|
||||
|
||||
t.Run("更新套餐时返回系列名称", func(t *testing.T) {
|
||||
// 先创建一个套餐
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_UPDATE_SERIES"),
|
||||
PackageName: "更新测试套餐",
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 更新套餐
|
||||
newName := "更新后的套餐"
|
||||
updateReq := &dto.UpdatePackageRequest{
|
||||
PackageName: &newName,
|
||||
}
|
||||
resp, err := svc.Update(ctx, created.ID, updateReq)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp.SeriesName)
|
||||
assert.Equal(t, series.SeriesName, *resp.SeriesName)
|
||||
})
|
||||
|
||||
t.Run("列表查询时返回系列名称", func(t *testing.T) {
|
||||
// 创建多个带系列的套餐
|
||||
for i := 0; i < 3; i++ {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode(fmt.Sprintf("PKG_LIST_SERIES_%d", i)),
|
||||
PackageName: fmt.Sprintf("列表测试套餐%d", i),
|
||||
SeriesID: &series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
_, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 查询列表
|
||||
listReq := &dto.PackageListRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
SeriesID: &series.ID,
|
||||
}
|
||||
resp, _, err := svc.List(ctx, listReq)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, len(resp), 0)
|
||||
|
||||
// 验证所有套餐都有系列名称
|
||||
for _, pkg := range resp {
|
||||
if pkg.SeriesID != nil && *pkg.SeriesID == series.ID {
|
||||
assert.NotNil(t, pkg.SeriesName)
|
||||
assert.Equal(t, series.SeriesName, *pkg.SeriesName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("没有系列的套餐SeriesName为空", func(t *testing.T) {
|
||||
req := &dto.CreatePackageRequest{
|
||||
PackageCode: generateUniquePackageCode("PKG_NO_SERIES"),
|
||||
PackageName: "无系列套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
Price: 9900,
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, resp.SeriesID)
|
||||
assert.Nil(t, resp.SeriesName)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
type Service struct {
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore
|
||||
shopStore *postgres.ShopStore
|
||||
packageStore *postgres.PackageStore
|
||||
}
|
||||
@@ -25,12 +26,14 @@ type Service struct {
|
||||
func New(
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
priceHistoryStore: priceHistoryStore,
|
||||
shopStore: shopStore,
|
||||
packageStore: packageStore,
|
||||
}
|
||||
@@ -271,3 +274,76 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopPackageAllocat
|
||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCostPrice(ctx context.Context, id uint, newCostPrice int64, changeReason string) (*dto.ShopPackageAllocationResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
allocation, err := s.packageAllocationStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if allocation.CostPrice == newCostPrice {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "新成本价与当前成本价相同")
|
||||
}
|
||||
|
||||
oldCostPrice := allocation.CostPrice
|
||||
now := time.Now()
|
||||
|
||||
priceHistory := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldCostPrice,
|
||||
NewCostPrice: newCostPrice,
|
||||
ChangeReason: changeReason,
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
if err := s.priceHistoryStore.Create(ctx, priceHistory); err != nil {
|
||||
return nil, fmt.Errorf("创建价格历史记录失败: %w", err)
|
||||
}
|
||||
|
||||
allocation.CostPrice = newCostPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := s.packageAllocationStore.Update(ctx, allocation); err != nil {
|
||||
return nil, fmt.Errorf("更新成本价失败: %w", err)
|
||||
}
|
||||
|
||||
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
||||
pkg, _ := s.packageStore.GetByID(ctx, allocation.PackageID)
|
||||
|
||||
shopName := ""
|
||||
packageName := ""
|
||||
packageCode := ""
|
||||
if shop != nil {
|
||||
shopName = shop.ShopName
|
||||
}
|
||||
if pkg != nil {
|
||||
packageName = pkg.PackageName
|
||||
packageCode = pkg.PackageCode
|
||||
}
|
||||
|
||||
return s.buildResponse(ctx, allocation, shopName, packageName, packageCode)
|
||||
}
|
||||
|
||||
func (s *Service) GetPriceHistory(ctx context.Context, allocationID uint) ([]*model.ShopPackageAllocationPriceHistory, error) {
|
||||
_, err := s.packageAllocationStore.GetByID(ctx, allocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
history, err := s.priceHistoryStore.ListByAllocation(ctx, allocationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取价格历史失败: %w", err)
|
||||
}
|
||||
|
||||
return history, nil
|
||||
}
|
||||
|
||||
193
internal/service/shop_package_batch_allocation/service.go
Normal file
193
internal/service/shop_package_batch_allocation/service.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package shop_package_batch_allocation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
packageStore *postgres.PackageStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore
|
||||
commissionTierStore *postgres.ShopSeriesCommissionTierStore
|
||||
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
shopStore *postgres.ShopStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
packageStore *postgres.PackageStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
commissionTierStore *postgres.ShopSeriesCommissionTierStore,
|
||||
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
packageStore: packageStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
configStore: configStore,
|
||||
commissionTierStore: commissionTierStore,
|
||||
commissionStatsStore: commissionStatsStore,
|
||||
shopStore: shopStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePackagesRequest) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
allocatorShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
if userType == constants.UserTypeAgent && allocatorShopID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
targetShop, err := s.shopStore.GetByID(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
}
|
||||
return fmt.Errorf("获取目标店铺失败: %w", err)
|
||||
}
|
||||
|
||||
if userType == constants.UserTypeAgent {
|
||||
if targetShop.ParentID == nil || *targetShop.ParentID != allocatorShopID {
|
||||
return errors.New(errors.CodeForbidden, "只能分配给直属下级店铺")
|
||||
}
|
||||
}
|
||||
|
||||
packages, err := s.getEnabledPackagesBySeries(ctx, req.SeriesID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(packages) == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "该系列下没有启用的套餐")
|
||||
}
|
||||
|
||||
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,
|
||||
EnableTierCommission: req.EnableTierCommission,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
if err := tx.Create(seriesAllocation).Error; err != nil {
|
||||
return fmt.Errorf("创建系列分配失败: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
config := &model.ShopSeriesAllocationConfig{
|
||||
AllocationID: seriesAllocation.ID,
|
||||
Version: 1,
|
||||
BaseCommissionMode: req.BaseCommission.Mode,
|
||||
BaseCommissionValue: req.BaseCommission.Value,
|
||||
EnableTierCommission: req.EnableTierCommission,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
|
||||
if err := tx.Create(config).Error; err != nil {
|
||||
return fmt.Errorf("创建配置版本失败: %w", err)
|
||||
}
|
||||
|
||||
packageAllocations := make([]*model.ShopPackageAllocation, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
costPrice := pkg.SuggestedCostPrice
|
||||
if req.PriceAdjustment != nil {
|
||||
costPrice = s.calculateAdjustedPrice(pkg.SuggestedCostPrice, 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,
|
||||
}
|
||||
packageAllocations = append(packageAllocations, allocation)
|
||||
}
|
||||
|
||||
if err := tx.CreateInBatches(packageAllocations, 100).Error; err != nil {
|
||||
return fmt.Errorf("批量创建套餐分配失败: %w", err)
|
||||
}
|
||||
|
||||
if req.EnableTierCommission && req.TierConfig != nil {
|
||||
if err := s.createCommissionTiers(tx, seriesAllocation.ID, req.TierConfig, currentUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) getEnabledPackagesBySeries(ctx context.Context, seriesID uint) ([]*model.Package, error) {
|
||||
filters := map[string]interface{}{
|
||||
"series_id": seriesID,
|
||||
"status": constants.StatusEnabled,
|
||||
"shelf_status": 1,
|
||||
}
|
||||
|
||||
packages, _, err := s.packageStore.List(ctx, nil, filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取套餐列表失败: %w", err)
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
func (s *Service) calculateAdjustedPrice(basePrice int64, adjustment *dto.PriceAdjustment) int64 {
|
||||
if adjustment == nil {
|
||||
return basePrice
|
||||
}
|
||||
|
||||
if adjustment.Type == "fixed" {
|
||||
return basePrice + adjustment.Value
|
||||
}
|
||||
|
||||
return basePrice + (basePrice * adjustment.Value / 1000)
|
||||
}
|
||||
|
||||
func (s *Service) createCommissionTiers(tx *gorm.DB, allocationID uint, config *dto.TierCommissionConfig, creatorID uint) error {
|
||||
for _, tierReq := range config.Tiers {
|
||||
tier := &model.ShopSeriesCommissionTier{
|
||||
BaseModel: model.BaseModel{Creator: creatorID, Updater: creatorID},
|
||||
AllocationID: allocationID,
|
||||
PeriodType: config.PeriodType,
|
||||
TierType: config.TierType,
|
||||
ThresholdValue: tierReq.Threshold,
|
||||
CommissionMode: tierReq.Mode,
|
||||
CommissionValue: tierReq.Value,
|
||||
}
|
||||
|
||||
if err := tx.Create(tier).Error; err != nil {
|
||||
return fmt.Errorf("创建佣金梯度失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
129
internal/service/shop_package_batch_pricing/service.go
Normal file
129
internal/service/shop_package_batch_pricing/service.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package shop_package_batch_pricing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore
|
||||
shopStore *postgres.ShopStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
priceHistoryStore *postgres.ShopPackageAllocationPriceHistoryStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
priceHistoryStore: priceHistoryStore,
|
||||
shopStore: shopStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) BatchUpdatePricing(ctx context.Context, req *dto.BatchUpdateCostPriceRequest) (*dto.BatchUpdateCostPriceResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
if userType == constants.UserTypeAgent && shopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
|
||||
}
|
||||
|
||||
filters := map[string]interface{}{
|
||||
"shop_id": req.ShopID,
|
||||
"status": constants.StatusEnabled,
|
||||
}
|
||||
|
||||
if req.SeriesID != nil {
|
||||
filters["series_id"] = *req.SeriesID
|
||||
}
|
||||
|
||||
allocations, _, err := s.packageAllocationStore.List(ctx, nil, filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if len(allocations) == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "没有找到符合条件的分配记录")
|
||||
}
|
||||
|
||||
updatedCount := 0
|
||||
now := time.Now()
|
||||
|
||||
affectedIDs := make([]uint, 0)
|
||||
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, allocation := range allocations {
|
||||
oldPrice := allocation.CostPrice
|
||||
newPrice := s.calculateAdjustedPrice(oldPrice, &req.PriceAdjustment)
|
||||
|
||||
if newPrice == oldPrice {
|
||||
continue
|
||||
}
|
||||
|
||||
history := &model.ShopPackageAllocationPriceHistory{
|
||||
AllocationID: allocation.ID,
|
||||
OldCostPrice: oldPrice,
|
||||
NewCostPrice: newPrice,
|
||||
ChangeReason: req.ChangeReason,
|
||||
ChangedBy: currentUserID,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
|
||||
if err := tx.Create(history).Error; err != nil {
|
||||
return fmt.Errorf("创建价格历史失败: %w", err)
|
||||
}
|
||||
|
||||
allocation.CostPrice = newPrice
|
||||
allocation.Updater = currentUserID
|
||||
if err := tx.Save(allocation).Error; err != nil {
|
||||
return fmt.Errorf("更新成本价失败: %w", err)
|
||||
}
|
||||
|
||||
affectedIDs = append(affectedIDs, allocation.ID)
|
||||
updatedCount++
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.BatchUpdateCostPriceResponse{
|
||||
UpdatedCount: updatedCount,
|
||||
AffectedIDs: affectedIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) calculateAdjustedPrice(basePrice int64, adjustment *dto.PriceAdjustment) int64 {
|
||||
if adjustment == nil {
|
||||
return basePrice
|
||||
}
|
||||
|
||||
if adjustment.Type == "fixed" {
|
||||
return basePrice + adjustment.Value
|
||||
}
|
||||
|
||||
return basePrice + (basePrice * adjustment.Value / 1000)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
type Service struct {
|
||||
allocationStore *postgres.ShopSeriesAllocationStore
|
||||
tierStore *postgres.ShopSeriesCommissionTierStore
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore
|
||||
shopStore *postgres.ShopStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageStore *postgres.PackageStore
|
||||
@@ -26,6 +27,7 @@ type Service struct {
|
||||
func New(
|
||||
allocationStore *postgres.ShopSeriesAllocationStore,
|
||||
tierStore *postgres.ShopSeriesCommissionTierStore,
|
||||
configStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
@@ -33,6 +35,7 @@ func New(
|
||||
return &Service{
|
||||
allocationStore: allocationStore,
|
||||
tierStore: tierStore,
|
||||
configStore: configStore,
|
||||
shopStore: shopStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageStore: packageStore,
|
||||
@@ -97,15 +100,13 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
|
||||
}
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: req.ShopID,
|
||||
SeriesID: req.SeriesID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
PricingMode: req.PricingMode,
|
||||
PricingValue: req.PricingValue,
|
||||
OneTimeCommissionTrigger: req.OneTimeCommissionTrigger,
|
||||
OneTimeCommissionThreshold: req.OneTimeCommissionThreshold,
|
||||
OneTimeCommissionAmount: req.OneTimeCommissionAmount,
|
||||
Status: constants.StatusEnabled,
|
||||
ShopID: req.ShopID,
|
||||
SeriesID: req.SeriesID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
BaseCommissionMode: req.BaseCommission.Mode,
|
||||
BaseCommissionValue: req.BaseCommission.Value,
|
||||
EnableTierCommission: req.EnableTierCommission,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation.Creator = currentUserID
|
||||
|
||||
@@ -154,23 +155,29 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if req.PricingMode != nil {
|
||||
allocation.PricingMode = *req.PricingMode
|
||||
configChanged := false
|
||||
if req.BaseCommission != nil {
|
||||
if allocation.BaseCommissionMode != req.BaseCommission.Mode ||
|
||||
allocation.BaseCommissionValue != req.BaseCommission.Value {
|
||||
configChanged = true
|
||||
}
|
||||
allocation.BaseCommissionMode = req.BaseCommission.Mode
|
||||
allocation.BaseCommissionValue = req.BaseCommission.Value
|
||||
}
|
||||
if req.PricingValue != nil {
|
||||
allocation.PricingValue = *req.PricingValue
|
||||
}
|
||||
if req.OneTimeCommissionTrigger != nil {
|
||||
allocation.OneTimeCommissionTrigger = *req.OneTimeCommissionTrigger
|
||||
}
|
||||
if req.OneTimeCommissionThreshold != nil {
|
||||
allocation.OneTimeCommissionThreshold = *req.OneTimeCommissionThreshold
|
||||
}
|
||||
if req.OneTimeCommissionAmount != nil {
|
||||
allocation.OneTimeCommissionAmount = *req.OneTimeCommissionAmount
|
||||
if req.EnableTierCommission != nil {
|
||||
if allocation.EnableTierCommission != *req.EnableTierCommission {
|
||||
configChanged = true
|
||||
}
|
||||
allocation.EnableTierCommission = *req.EnableTierCommission
|
||||
}
|
||||
allocation.Updater = currentUserID
|
||||
|
||||
if configChanged {
|
||||
if err := s.createNewConfigVersion(ctx, allocation); err != nil {
|
||||
return nil, fmt.Errorf("创建配置版本失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.allocationStore.Update(ctx, allocation); err != nil {
|
||||
return nil, fmt.Errorf("更新分配失败: %w", err)
|
||||
}
|
||||
@@ -306,177 +313,7 @@ func (s *Service) GetParentCostPrice(ctx context.Context, shopID, packageID uint
|
||||
return pkg.SuggestedCostPrice, nil
|
||||
}
|
||||
|
||||
allocation, err := s.allocationStore.GetByShopAndSeries(ctx, shopID, pkg.SeriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeNotFound, "未找到分配记录")
|
||||
}
|
||||
return 0, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
parentCostPrice, err := s.GetParentCostPrice(ctx, allocation.AllocatorShopID, packageID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return s.CalculateCostPrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue), nil
|
||||
}
|
||||
|
||||
func (s *Service) CalculateCostPrice(parentCostPrice int64, pricingMode string, pricingValue int64) int64 {
|
||||
switch pricingMode {
|
||||
case model.PricingModeFixed:
|
||||
return parentCostPrice + pricingValue
|
||||
case model.PricingModePercent:
|
||||
return parentCostPrice + (parentCostPrice * pricingValue / 1000)
|
||||
default:
|
||||
return parentCostPrice
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) AddTier(ctx context.Context, allocationID uint, req *dto.CreateCommissionTierRequest) (*dto.CommissionTierResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
_, err := s.allocationStore.GetByID(ctx, allocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
if req.PeriodType == model.PeriodTypeCustom {
|
||||
if req.PeriodStartDate == nil || req.PeriodEndDate == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "自定义周期必须指定开始和结束日期")
|
||||
}
|
||||
}
|
||||
|
||||
tier := &model.ShopSeriesCommissionTier{
|
||||
AllocationID: allocationID,
|
||||
TierType: req.TierType,
|
||||
PeriodType: req.PeriodType,
|
||||
ThresholdValue: req.ThresholdValue,
|
||||
CommissionAmount: req.CommissionAmount,
|
||||
}
|
||||
tier.Creator = currentUserID
|
||||
|
||||
if req.PeriodStartDate != nil {
|
||||
t, err := time.Parse("2006-01-02", *req.PeriodStartDate)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "开始日期格式无效")
|
||||
}
|
||||
tier.PeriodStartDate = &t
|
||||
}
|
||||
if req.PeriodEndDate != nil {
|
||||
t, err := time.Parse("2006-01-02", *req.PeriodEndDate)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "结束日期格式无效")
|
||||
}
|
||||
tier.PeriodEndDate = &t
|
||||
}
|
||||
|
||||
if err := s.tierStore.Create(ctx, tier); err != nil {
|
||||
return nil, fmt.Errorf("创建梯度配置失败: %w", err)
|
||||
}
|
||||
|
||||
return s.buildTierResponse(tier), nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateTier(ctx context.Context, allocationID, tierID uint, req *dto.UpdateCommissionTierRequest) (*dto.CommissionTierResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
tier, err := s.tierStore.GetByID(ctx, tierID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "梯度配置不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取梯度配置失败: %w", err)
|
||||
}
|
||||
|
||||
if tier.AllocationID != allocationID {
|
||||
return nil, errors.New(errors.CodeForbidden, "梯度配置不属于该分配")
|
||||
}
|
||||
|
||||
if req.TierType != nil {
|
||||
tier.TierType = *req.TierType
|
||||
}
|
||||
if req.PeriodType != nil {
|
||||
tier.PeriodType = *req.PeriodType
|
||||
}
|
||||
if req.ThresholdValue != nil {
|
||||
tier.ThresholdValue = *req.ThresholdValue
|
||||
}
|
||||
if req.CommissionAmount != nil {
|
||||
tier.CommissionAmount = *req.CommissionAmount
|
||||
}
|
||||
if req.PeriodStartDate != nil {
|
||||
t, err := time.Parse("2006-01-02", *req.PeriodStartDate)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "开始日期格式无效")
|
||||
}
|
||||
tier.PeriodStartDate = &t
|
||||
}
|
||||
if req.PeriodEndDate != nil {
|
||||
t, err := time.Parse("2006-01-02", *req.PeriodEndDate)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "结束日期格式无效")
|
||||
}
|
||||
tier.PeriodEndDate = &t
|
||||
}
|
||||
tier.Updater = currentUserID
|
||||
|
||||
if err := s.tierStore.Update(ctx, tier); err != nil {
|
||||
return nil, fmt.Errorf("更新梯度配置失败: %w", err)
|
||||
}
|
||||
|
||||
return s.buildTierResponse(tier), nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteTier(ctx context.Context, allocationID, tierID uint) error {
|
||||
tier, err := s.tierStore.GetByID(ctx, tierID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "梯度配置不存在")
|
||||
}
|
||||
return fmt.Errorf("获取梯度配置失败: %w", err)
|
||||
}
|
||||
|
||||
if tier.AllocationID != allocationID {
|
||||
return errors.New(errors.CodeForbidden, "梯度配置不属于该分配")
|
||||
}
|
||||
|
||||
if err := s.tierStore.Delete(ctx, tierID); err != nil {
|
||||
return fmt.Errorf("删除梯度配置失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListTiers(ctx context.Context, allocationID uint) ([]*dto.CommissionTierResponse, error) {
|
||||
_, err := s.allocationStore.GetByID(ctx, allocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "分配记录不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
tiers, err := s.tierStore.ListByAllocationID(ctx, allocationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询梯度配置失败: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]*dto.CommissionTierResponse, len(tiers))
|
||||
for i, t := range tiers {
|
||||
responses[i] = s.buildTierResponse(t)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
return 0, errors.New(errors.CodeInvalidParam, "自动计算成本价功能已移除,请手动设置成本价")
|
||||
}
|
||||
|
||||
func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocation, shopName, seriesName string) (*dto.ShopSeriesAllocationResponse, error) {
|
||||
@@ -486,46 +323,78 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocati
|
||||
allocatorShopName = allocatorShop.ShopName
|
||||
}
|
||||
|
||||
var calculatedCostPrice int64 = 0
|
||||
|
||||
return &dto.ShopSeriesAllocationResponse{
|
||||
ID: a.ID,
|
||||
ShopID: a.ShopID,
|
||||
ShopName: shopName,
|
||||
SeriesID: a.SeriesID,
|
||||
SeriesName: seriesName,
|
||||
AllocatorShopID: a.AllocatorShopID,
|
||||
AllocatorShopName: allocatorShopName,
|
||||
PricingMode: a.PricingMode,
|
||||
PricingValue: a.PricingValue,
|
||||
CalculatedCostPrice: calculatedCostPrice,
|
||||
OneTimeCommissionTrigger: a.OneTimeCommissionTrigger,
|
||||
OneTimeCommissionThreshold: a.OneTimeCommissionThreshold,
|
||||
OneTimeCommissionAmount: a.OneTimeCommissionAmount,
|
||||
Status: a.Status,
|
||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||
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,
|
||||
},
|
||||
EnableTierCommission: a.EnableTierCommission,
|
||||
Status: a.Status,
|
||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildTierResponse(t *model.ShopSeriesCommissionTier) *dto.CommissionTierResponse {
|
||||
resp := &dto.CommissionTierResponse{
|
||||
ID: t.ID,
|
||||
AllocationID: t.AllocationID,
|
||||
TierType: t.TierType,
|
||||
PeriodType: t.PeriodType,
|
||||
ThresholdValue: t.ThresholdValue,
|
||||
CommissionAmount: t.CommissionAmount,
|
||||
CreatedAt: t.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: t.UpdatedAt.Format(time.RFC3339),
|
||||
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 fmt.Errorf("失效当前配置版本失败: %w", err)
|
||||
}
|
||||
|
||||
if t.PeriodStartDate != nil {
|
||||
resp.PeriodStartDate = t.PeriodStartDate.Format("2006-01-02")
|
||||
}
|
||||
if t.PeriodEndDate != nil {
|
||||
resp.PeriodEndDate = t.PeriodEndDate.Format("2006-01-02")
|
||||
latestVersion, err := s.configStore.GetLatestVersion(ctx, allocation.ID)
|
||||
newVersion := 1
|
||||
if err == nil && latestVersion != nil {
|
||||
newVersion = latestVersion.Version + 1
|
||||
}
|
||||
|
||||
return resp
|
||||
newConfig := &model.ShopSeriesAllocationConfig{
|
||||
AllocationID: allocation.ID,
|
||||
Version: newVersion,
|
||||
BaseCommissionMode: allocation.BaseCommissionMode,
|
||||
BaseCommissionValue: allocation.BaseCommissionValue,
|
||||
EnableTierCommission: allocation.EnableTierCommission,
|
||||
EffectiveFrom: now,
|
||||
}
|
||||
|
||||
if err := s.configStore.Create(ctx, newConfig); err != nil {
|
||||
return fmt.Errorf("创建新配置版本失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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, fmt.Errorf("获取生效配置失败: %w", 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, fmt.Errorf("获取分配记录失败: %w", err)
|
||||
}
|
||||
|
||||
configs, err := s.configStore.List(ctx, allocationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取配置版本列表失败: %w", err)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
package shop_series_allocation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func createTestService(t *testing.T) (*Service, *postgres.ShopSeriesAllocationStore, *postgres.ShopStore, *postgres.PackageSeriesStore, *postgres.PackageStore, *postgres.ShopSeriesCommissionTierStore) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
allocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
tierStore := postgres.NewShopSeriesCommissionTierStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
|
||||
svc := New(allocationStore, tierStore, shopStore, packageSeriesStore, packageStore)
|
||||
return svc, allocationStore, shopStore, packageSeriesStore, packageStore, tierStore
|
||||
}
|
||||
|
||||
func createContextWithUser(userID uint, userType int, shopID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
info := &middleware.UserContextInfo{
|
||||
UserID: userID,
|
||||
UserType: userType,
|
||||
ShopID: shopID,
|
||||
}
|
||||
return middleware.SetUserContext(ctx, info)
|
||||
}
|
||||
|
||||
func createTestShop(t *testing.T, store *postgres.ShopStore, ctx context.Context, shopName string, parentID *uint) *model.Shop {
|
||||
shop := &model.Shop{
|
||||
ShopName: shopName,
|
||||
ShopCode: shopName,
|
||||
ParentID: parentID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
err := store.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
return shop
|
||||
}
|
||||
|
||||
func createTestSeries(t *testing.T, store *postgres.PackageSeriesStore, ctx context.Context, seriesName string) *model.PackageSeries {
|
||||
series := &model.PackageSeries{
|
||||
SeriesName: seriesName,
|
||||
SeriesCode: seriesName,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
series.Creator = 1
|
||||
err := store.Create(ctx, series)
|
||||
require.NoError(t, err)
|
||||
return series
|
||||
}
|
||||
|
||||
func TestService_CalculateCostPrice(t *testing.T) {
|
||||
svc, _, _, _, _, _ := createTestService(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
parentCostPrice int64
|
||||
pricingMode string
|
||||
pricingValue int64
|
||||
expectedCostPrice int64
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "固定加价模式:10000 + 500 = 10500",
|
||||
parentCostPrice: 10000,
|
||||
pricingMode: model.PricingModeFixed,
|
||||
pricingValue: 500,
|
||||
expectedCostPrice: 10500,
|
||||
description: "固定金额加价",
|
||||
},
|
||||
{
|
||||
name: "百分比加价模式:10000 + 10000*100/1000 = 11000",
|
||||
parentCostPrice: 10000,
|
||||
pricingMode: model.PricingModePercent,
|
||||
pricingValue: 100,
|
||||
expectedCostPrice: 11000,
|
||||
description: "百分比加价(100 = 10%)",
|
||||
},
|
||||
{
|
||||
name: "百分比加价模式:5000 + 5000*50/1000 = 5250",
|
||||
parentCostPrice: 5000,
|
||||
pricingMode: model.PricingModePercent,
|
||||
pricingValue: 50,
|
||||
expectedCostPrice: 5250,
|
||||
description: "百分比加价(50 = 5%)",
|
||||
},
|
||||
{
|
||||
name: "未知加价模式:返回原价",
|
||||
parentCostPrice: 10000,
|
||||
pricingMode: "unknown",
|
||||
pricingValue: 500,
|
||||
expectedCostPrice: 10000,
|
||||
description: "未知加价模式返回原价",
|
||||
},
|
||||
{
|
||||
name: "固定加价为0:10000 + 0 = 10000",
|
||||
parentCostPrice: 10000,
|
||||
pricingMode: model.PricingModeFixed,
|
||||
pricingValue: 0,
|
||||
expectedCostPrice: 10000,
|
||||
description: "固定加价为0",
|
||||
},
|
||||
{
|
||||
name: "百分比加价为0:10000 + 0 = 10000",
|
||||
parentCostPrice: 10000,
|
||||
pricingMode: model.PricingModePercent,
|
||||
pricingValue: 0,
|
||||
expectedCostPrice: 10000,
|
||||
description: "百分比加价为0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := svc.CalculateCostPrice(tt.parentCostPrice, tt.pricingMode, tt.pricingValue)
|
||||
assert.Equal(t, tt.expectedCostPrice, result, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Create_Validation(t *testing.T) {
|
||||
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
|
||||
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
|
||||
unrelatedShop := createTestShop(t, shopStore, ctx, "无关店铺", nil)
|
||||
series := createTestSeries(t, seriesStore, ctx, "测试系列")
|
||||
|
||||
t.Run("未授权访问:无用户上下文", func(t *testing.T) {
|
||||
emptyCtx := context.Background()
|
||||
|
||||
req := &dto.CreateShopSeriesAllocationRequest{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
}
|
||||
|
||||
_, err := svc.Create(emptyCtx, req)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("代理账号无店铺上下文", func(t *testing.T) {
|
||||
ctxWithoutShop := createContextWithUser(1, constants.UserTypeAgent, 0)
|
||||
|
||||
req := &dto.CreateShopSeriesAllocationRequest{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
}
|
||||
|
||||
_, err := svc.Create(ctxWithoutShop, req)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("分配给非直属下级店铺", func(t *testing.T) {
|
||||
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
|
||||
req := &dto.CreateShopSeriesAllocationRequest{
|
||||
ShopID: unrelatedShop.ID,
|
||||
SeriesID: series.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
}
|
||||
|
||||
_, err := svc.Create(ctxParent, req)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeForbidden, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("代理账号无该系列分配权限", func(t *testing.T) {
|
||||
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
series2 := createTestSeries(t, seriesStore, ctx, "测试系列2")
|
||||
|
||||
req := &dto.CreateShopSeriesAllocationRequest{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series2.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
}
|
||||
|
||||
_, err := svc.Create(ctxParent, req)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeForbidden, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("重复分配:同一店铺和系列已分配", func(t *testing.T) {
|
||||
series3 := createTestSeries(t, seriesStore, ctx, "测试系列3")
|
||||
childShop2 := createTestShop(t, shopStore, ctx, "二级代理2", &parentShop.ID)
|
||||
|
||||
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
|
||||
parentAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: parentShop.ID,
|
||||
SeriesID: series3.ID,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
parentAllocation.Creator = 1
|
||||
err := allocationStore.Create(ctx, parentAllocation)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.CreateShopSeriesAllocationRequest{
|
||||
ShopID: childShop2.ID,
|
||||
SeriesID: series3.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
}
|
||||
|
||||
resp1, err := svc.Create(ctxParent, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp1)
|
||||
|
||||
_, err = svc.Create(ctxParent, req)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeConflict, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("成功创建分配:代理有该系列权限", func(t *testing.T) {
|
||||
series4 := createTestSeries(t, seriesStore, ctx, "测试系列4")
|
||||
childShop3 := createTestShop(t, shopStore, ctx, "二级代理3", &parentShop.ID)
|
||||
|
||||
ctxParent := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
|
||||
parentAllocation := &model.ShopSeriesAllocation{
|
||||
ShopID: parentShop.ID,
|
||||
SeriesID: series4.ID,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
parentAllocation.Creator = 1
|
||||
err := allocationStore.Create(ctx, parentAllocation)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.CreateShopSeriesAllocationRequest{
|
||||
ShopID: childShop3.ID,
|
||||
SeriesID: series4.ID,
|
||||
PricingMode: model.PricingModePercent,
|
||||
PricingValue: 100,
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctxParent, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, childShop3.ID, resp.ShopID)
|
||||
assert.Equal(t, series4.ID, resp.SeriesID)
|
||||
assert.Equal(t, model.PricingModePercent, resp.PricingMode)
|
||||
assert.Equal(t, int64(100), resp.PricingValue)
|
||||
})
|
||||
|
||||
t.Run("平台用户需要有店铺上下文才能分配", func(t *testing.T) {
|
||||
series5 := createTestSeries(t, seriesStore, ctx, "测试系列5")
|
||||
childShop4 := createTestShop(t, shopStore, ctx, "二级代理4", &parentShop.ID)
|
||||
|
||||
ctxPlatform := createContextWithUser(2, constants.UserTypePlatform, 0)
|
||||
|
||||
req := &dto.CreateShopSeriesAllocationRequest{
|
||||
ShopID: childShop4.ID,
|
||||
SeriesID: series5.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
}
|
||||
|
||||
_, err := svc.Create(ctxPlatform, req)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeForbidden, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_Delete_WithDependency(t *testing.T) {
|
||||
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
|
||||
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
|
||||
_ = createTestShop(t, shopStore, ctx, "三级代理", &childShop.ID)
|
||||
series := createTestSeries(t, seriesStore, ctx, "测试系列")
|
||||
|
||||
t.Run("删除无依赖的分配成功", func(t *testing.T) {
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: parentShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation.Creator = 1
|
||||
err := allocationStore.Create(ctx, allocation)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.Delete(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = allocationStore.GetByID(ctx, allocation.ID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("删除分配成功(无依赖关系)", func(t *testing.T) {
|
||||
series2 := createTestSeries(t, seriesStore, ctx, "测试系列2")
|
||||
|
||||
allocation1 := &model.ShopSeriesAllocation{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series2.ID,
|
||||
AllocatorShopID: parentShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation1.Creator = 1
|
||||
err := allocationStore.Create(ctx, allocation1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.Delete(ctx, allocation1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = allocationStore.GetByID(ctx, allocation1.ID)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("删除不存在的分配返回错误", func(t *testing.T) {
|
||||
err := svc.Delete(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_Get(t *testing.T) {
|
||||
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
|
||||
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
|
||||
series := createTestSeries(t, seriesStore, ctx, "测试系列")
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: parentShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation.Creator = 1
|
||||
err := allocationStore.Create(ctx, allocation)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("获取存在的分配", func(t *testing.T) {
|
||||
resp, err := svc.Get(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, allocation.ID, resp.ID)
|
||||
assert.Equal(t, childShop.ID, resp.ShopID)
|
||||
assert.Equal(t, series.ID, resp.SeriesID)
|
||||
})
|
||||
|
||||
t.Run("获取不存在的分配", func(t *testing.T) {
|
||||
_, err := svc.Get(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_Update(t *testing.T) {
|
||||
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
|
||||
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
|
||||
series := createTestSeries(t, seriesStore, ctx, "测试系列")
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: parentShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation.Creator = 1
|
||||
err := allocationStore.Create(ctx, allocation)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("更新加价模式和加价值", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
newMode := model.PricingModePercent
|
||||
newValue := int64(100)
|
||||
|
||||
req := &dto.UpdateShopSeriesAllocationRequest{
|
||||
PricingMode: &newMode,
|
||||
PricingValue: &newValue,
|
||||
}
|
||||
|
||||
resp, err := svc.Update(ctxWithUser, allocation.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, model.PricingModePercent, resp.PricingMode)
|
||||
assert.Equal(t, int64(100), resp.PricingValue)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的分配", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
newMode := model.PricingModeFixed
|
||||
|
||||
req := &dto.UpdateShopSeriesAllocationRequest{
|
||||
PricingMode: &newMode,
|
||||
}
|
||||
|
||||
_, err := svc.Update(ctxWithUser, 99999, req)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_UpdateStatus(t *testing.T) {
|
||||
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
|
||||
childShop := createTestShop(t, shopStore, ctx, "二级代理", &parentShop.ID)
|
||||
series := createTestSeries(t, seriesStore, ctx, "测试系列")
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: childShop.ID,
|
||||
SeriesID: series.ID,
|
||||
AllocatorShopID: parentShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation.Creator = 1
|
||||
err := allocationStore.Create(ctx, allocation)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("禁用分配", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
err := svc.UpdateStatus(ctxWithUser, allocation.ID, constants.StatusDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := allocationStore.GetByID(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, updated.Status)
|
||||
})
|
||||
|
||||
t.Run("启用分配", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
err := svc.UpdateStatus(ctxWithUser, allocation.ID, constants.StatusEnabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := allocationStore.GetByID(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, updated.Status)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的分配状态", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
err := svc.UpdateStatus(ctxWithUser, 99999, constants.StatusDisabled)
|
||||
require.Error(t, err)
|
||||
appErr := err.(*errors.AppError)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_List(t *testing.T) {
|
||||
svc, allocationStore, shopStore, seriesStore, _, _ := createTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
parentShop := createTestShop(t, shopStore, ctx, "一级代理", nil)
|
||||
childShop1 := createTestShop(t, shopStore, ctx, "二级代理1", &parentShop.ID)
|
||||
childShop2 := createTestShop(t, shopStore, ctx, "二级代理2", &parentShop.ID)
|
||||
series1 := createTestSeries(t, seriesStore, ctx, "测试系列1")
|
||||
series2 := createTestSeries(t, seriesStore, ctx, "测试系列2")
|
||||
|
||||
allocation1 := &model.ShopSeriesAllocation{
|
||||
ShopID: childShop1.ID,
|
||||
SeriesID: series1.ID,
|
||||
AllocatorShopID: parentShop.ID,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation1.Creator = 1
|
||||
err := allocationStore.Create(ctx, allocation1)
|
||||
require.NoError(t, err)
|
||||
|
||||
allocation2 := &model.ShopSeriesAllocation{
|
||||
ShopID: childShop2.ID,
|
||||
SeriesID: series2.ID,
|
||||
AllocatorShopID: parentShop.ID,
|
||||
PricingMode: model.PricingModePercent,
|
||||
PricingValue: 100,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
allocation2.Creator = 1
|
||||
err = allocationStore.Create(ctx, allocation2)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("查询所有分配", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
req := &dto.ShopSeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
resp, total, err := svc.List(ctxWithUser, req)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(2))
|
||||
assert.GreaterOrEqual(t, len(resp), 2)
|
||||
})
|
||||
|
||||
t.Run("按店铺ID过滤", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
req := &dto.ShopSeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
ShopID: &childShop1.ID,
|
||||
}
|
||||
|
||||
resp, total, err := svc.List(ctxWithUser, req)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, a := range resp {
|
||||
assert.Equal(t, childShop1.ID, a.ShopID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按系列ID过滤", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
req := &dto.ShopSeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
SeriesID: &series1.ID,
|
||||
}
|
||||
|
||||
resp, total, err := svc.List(ctxWithUser, req)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, a := range resp {
|
||||
assert.Equal(t, series1.ID, a.SeriesID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按状态过滤", func(t *testing.T) {
|
||||
ctxWithUser := createContextWithUser(1, constants.UserTypeAgent, parentShop.ID)
|
||||
status := constants.StatusEnabled
|
||||
req := &dto.ShopSeriesAllocationListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
Status: &status,
|
||||
}
|
||||
|
||||
resp, total, err := svc.List(ctxWithUser, req)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(2))
|
||||
for _, a := range resp {
|
||||
assert.Equal(t, constants.StatusEnabled, a.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -37,6 +37,18 @@ func (s *PackageSeriesStore) GetByCode(ctx context.Context, code string) (*model
|
||||
return &series, nil
|
||||
}
|
||||
|
||||
// GetByIDs 批量查询套餐系列
|
||||
func (s *PackageSeriesStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.PackageSeries, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*model.PackageSeries{}, nil
|
||||
}
|
||||
var seriesList []*model.PackageSeries
|
||||
if err := s.db.WithContext(ctx).Where("id IN ?", ids).Find(&seriesList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return seriesList, nil
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) Update(ctx context.Context, series *model.PackageSeries) error {
|
||||
return s.db.WithContext(ctx).Save(series).Error
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
type PackageStore struct {
|
||||
@@ -51,20 +53,29 @@ func (s *PackageStore) List(ctx context.Context, opts *store.QueryOptions, filte
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.Package{})
|
||||
|
||||
// 代理用户额外过滤:只能看到已分配的套餐
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if userType == constants.UserTypeAgent && shopID > 0 {
|
||||
query = query.Joins("INNER JOIN tb_shop_package_allocation ON tb_shop_package_allocation.package_id = tb_package.id").
|
||||
Where("tb_shop_package_allocation.shop_id = ? AND tb_shop_package_allocation.status = ?",
|
||||
shopID, constants.StatusEnabled)
|
||||
}
|
||||
|
||||
if packageName, ok := filters["package_name"].(string); ok && packageName != "" {
|
||||
query = query.Where("package_name LIKE ?", "%"+packageName+"%")
|
||||
query = query.Where("tb_package.package_name LIKE ?", "%"+packageName+"%")
|
||||
}
|
||||
if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
|
||||
query = query.Where("series_id = ?", seriesID)
|
||||
query = query.Where("tb_package.series_id = ?", seriesID)
|
||||
}
|
||||
if status, ok := filters["status"]; ok {
|
||||
query = query.Where("status = ?", status)
|
||||
query = query.Where("tb_package.status = ?", status)
|
||||
}
|
||||
if shelfStatus, ok := filters["shelf_status"]; ok {
|
||||
query = query.Where("shelf_status = ?", shelfStatus)
|
||||
query = query.Where("tb_package.shelf_status = ?", shelfStatus)
|
||||
}
|
||||
if packageType, ok := filters["package_type"].(string); ok && packageType != "" {
|
||||
query = query.Where("package_type = ?", packageType)
|
||||
query = query.Where("tb_package.package_type = ?", packageType)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ShopPackageAllocationPriceHistoryStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewShopPackageAllocationPriceHistoryStore(db *gorm.DB) *ShopPackageAllocationPriceHistoryStore {
|
||||
return &ShopPackageAllocationPriceHistoryStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationPriceHistoryStore) Create(ctx context.Context, history *model.ShopPackageAllocationPriceHistory) error {
|
||||
return s.db.WithContext(ctx).Create(history).Error
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationPriceHistoryStore) BatchCreate(ctx context.Context, histories []*model.ShopPackageAllocationPriceHistory) error {
|
||||
return s.db.WithContext(ctx).CreateInBatches(histories, 500).Error
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationPriceHistoryStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.ShopPackageAllocationPriceHistory, int64, error) {
|
||||
var histories []*model.ShopPackageAllocationPriceHistory
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.ShopPackageAllocationPriceHistory{})
|
||||
|
||||
if allocationID, ok := filters["allocation_id"].(uint); ok && allocationID > 0 {
|
||||
query = query.Where("allocation_id = ?", allocationID)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = store.DefaultQueryOptions()
|
||||
}
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
query = query.Offset(offset).Limit(opts.PageSize)
|
||||
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
} else {
|
||||
query = query.Order("effective_from DESC")
|
||||
}
|
||||
|
||||
if err := query.Find(&histories).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return histories, total, nil
|
||||
}
|
||||
|
||||
func (s *ShopPackageAllocationPriceHistoryStore) ListByAllocation(ctx context.Context, allocationID uint) ([]*model.ShopPackageAllocationPriceHistory, error) {
|
||||
var histories []*model.ShopPackageAllocationPriceHistory
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Order("effective_from DESC").
|
||||
Find(&histories).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return histories, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShopSeriesAllocationStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 1,
|
||||
SeriesID: 1,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, allocation)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, allocation.ID)
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 2,
|
||||
SeriesID: 2,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModePercent,
|
||||
PricingValue: 500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
t.Run("查询存在的分配", func(t *testing.T) {
|
||||
result, err := s.GetByID(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, allocation.ShopID, result.ShopID)
|
||||
assert.Equal(t, allocation.SeriesID, result.SeriesID)
|
||||
assert.Equal(t, allocation.PricingMode, result.PricingMode)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的分配", func(t *testing.T) {
|
||||
_, err := s.GetByID(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_GetByShopAndSeries(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 3,
|
||||
SeriesID: 3,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 2000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
t.Run("查询存在的店铺和系列组合", func(t *testing.T) {
|
||||
result, err := s.GetByShopAndSeries(ctx, 3, 3)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, allocation.ID, result.ID)
|
||||
assert.Equal(t, uint(3), result.ShopID)
|
||||
assert.Equal(t, uint(3), result.SeriesID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的组合", func(t *testing.T) {
|
||||
_, err := s.GetByShopAndSeries(ctx, 99, 99)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 4,
|
||||
SeriesID: 4,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1500,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
allocation.PricingValue = 2500
|
||||
allocation.PricingMode = model.PricingModePercent
|
||||
err := s.Update(ctx, allocation)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2500), updated.PricingValue)
|
||||
assert.Equal(t, model.PricingModePercent, updated.PricingMode)
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 5,
|
||||
SeriesID: 5,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
err := s.Delete(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.GetByID(ctx, allocation.ID)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocations := []*model.ShopSeriesAllocation{
|
||||
{ShopID: 10, SeriesID: 10, AllocatorShopID: 0, PricingMode: model.PricingModeFixed, PricingValue: 1000, Status: constants.StatusEnabled},
|
||||
{ShopID: 11, SeriesID: 11, AllocatorShopID: 0, PricingMode: model.PricingModePercent, PricingValue: 500, Status: constants.StatusEnabled},
|
||||
{ShopID: 12, SeriesID: 12, AllocatorShopID: 1, PricingMode: model.PricingModeFixed, PricingValue: 2000, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, a := range allocations {
|
||||
require.NoError(t, s.Create(ctx, a))
|
||||
}
|
||||
// 显式更新第三个分配为禁用状态
|
||||
allocations[2].Status = constants.StatusDisabled
|
||||
require.NoError(t, s.Update(ctx, allocations[2]))
|
||||
|
||||
t.Run("查询所有分配", func(t *testing.T) {
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(3))
|
||||
assert.GreaterOrEqual(t, len(result), 3)
|
||||
})
|
||||
|
||||
t.Run("按店铺ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"shop_id": uint(10)}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, a := range result {
|
||||
assert.Equal(t, uint(10), a.ShopID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按系列ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"series_id": uint(11)}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, a := range result {
|
||||
assert.Equal(t, uint(11), a.SeriesID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按分配者店铺ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"allocator_shop_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(1))
|
||||
for _, a := range result {
|
||||
assert.Equal(t, uint(1), a.AllocatorShopID)
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(2))
|
||||
for _, a := range result {
|
||||
assert.Equal(t, 1, a.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按状态过滤-启用", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"status": constants.StatusEnabled}
|
||||
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, constants.StatusEnabled, a.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 2}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(3))
|
||||
assert.LessOrEqual(t, len(result), 2)
|
||||
})
|
||||
|
||||
t.Run("默认分页选项", func(t *testing.T) {
|
||||
result, _, err := s.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 20,
|
||||
SeriesID: 20,
|
||||
AllocatorShopID: 0,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
err := s.UpdateStatus(ctx, allocation.ID, constants.StatusDisabled, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, allocation.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, updated.Status)
|
||||
assert.Equal(t, uint(1), updated.Updater)
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocationStore_HasDependentAllocations(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewShopSeriesAllocationStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: 30,
|
||||
SeriesID: 30,
|
||||
AllocatorShopID: 100,
|
||||
PricingMode: model.PricingModeFixed,
|
||||
PricingValue: 1000,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, allocation))
|
||||
|
||||
t.Run("检查存在的依赖分配", func(t *testing.T) {
|
||||
// 注意:这个测试依赖于数据库中存在特定的店铺层级关系
|
||||
// 由于测试环境可能没有这样的关系,我们只验证函数可以执行
|
||||
has, err := s.HasDependentAllocations(ctx, 100, 30)
|
||||
require.NoError(t, err)
|
||||
// 结果取决于数据库中的实际店铺关系
|
||||
assert.IsType(t, true, has)
|
||||
})
|
||||
|
||||
t.Run("检查不存在的依赖分配", func(t *testing.T) {
|
||||
has, err := s.HasDependentAllocations(ctx, 99999, 99999)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ShopSeriesCommissionStatsStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewShopSeriesCommissionStatsStore(db *gorm.DB) *ShopSeriesCommissionStatsStore {
|
||||
return &ShopSeriesCommissionStatsStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ShopSeriesCommissionStatsStore) Create(ctx context.Context, stats *model.ShopSeriesCommissionStats) error {
|
||||
return s.db.WithContext(ctx).Create(stats).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesCommissionStatsStore) GetCurrent(ctx context.Context, allocationID uint, periodType string, now time.Time) (*model.ShopSeriesCommissionStats, error) {
|
||||
var stats model.ShopSeriesCommissionStats
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("allocation_id = ?", allocationID).
|
||||
Where("period_type = ?", periodType).
|
||||
Where("period_start <= ? AND period_end >= ?", now, now).
|
||||
Where("status = ?", model.StatsStatusActive).
|
||||
First(&stats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
func (s *ShopSeriesCommissionStatsStore) Update(ctx context.Context, stats *model.ShopSeriesCommissionStats) error {
|
||||
return s.db.WithContext(ctx).Save(stats).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesCommissionStatsStore) IncrementSales(ctx context.Context, id uint, salesCount int64, salesAmount int64, version int) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.ShopSeriesCommissionStats{}).
|
||||
Where("id = ? AND version = ?", id, version).
|
||||
Updates(map[string]interface{}{
|
||||
"total_sales_count": gorm.Expr("total_sales_count + ?", salesCount),
|
||||
"total_sales_amount": gorm.Expr("total_sales_amount + ?", salesAmount),
|
||||
"last_updated_at": time.Now(),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesCommissionStatsStore) CompletePeriod(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.ShopSeriesCommissionStats{}).
|
||||
Where("id = ?", id).
|
||||
Update("status", model.StatsStatusCompleted).Error
|
||||
}
|
||||
|
||||
func (s *ShopSeriesCommissionStatsStore) ListExpired(ctx context.Context, before time.Time) ([]*model.ShopSeriesCommissionStats, error) {
|
||||
var stats []*model.ShopSeriesCommissionStats
|
||||
err := s.db.WithContext(ctx).
|
||||
Where("period_end < ?", before).
|
||||
Where("status = ?", model.StatsStatusActive).
|
||||
Find(&stats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
120
internal/task/commission_stats_archive.go
Normal file
120
internal/task/commission_stats_archive.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"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"
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
)
|
||||
|
||||
type CommissionStatsArchiveHandler struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewCommissionStatsArchiveHandler(
|
||||
db *gorm.DB,
|
||||
redis *redis.Client,
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore,
|
||||
logger *zap.Logger,
|
||||
) *CommissionStatsArchiveHandler {
|
||||
return &CommissionStatsArchiveHandler{
|
||||
db: db,
|
||||
redis: redis,
|
||||
statsStore: statsStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CommissionStatsArchiveHandler) HandleCommissionStatsArchive(ctx context.Context, task *asynq.Task) error {
|
||||
ctx = pkggorm.SkipDataPermission(ctx)
|
||||
|
||||
now := time.Now()
|
||||
lastMonthStart := now.AddDate(0, -1, 0)
|
||||
lastMonthStart = time.Date(lastMonthStart.Year(), lastMonthStart.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
lastMonthEnd := now.AddDate(0, 0, -now.Day()).Add(24*time.Hour - time.Second)
|
||||
|
||||
var stats []model.ShopSeriesCommissionStats
|
||||
err := h.db.Where("period_start >= ? AND period_end <= ? AND status = ?",
|
||||
lastMonthStart, lastMonthEnd, model.StatsStatusActive).
|
||||
Find(&stats).Error
|
||||
|
||||
if err != nil {
|
||||
h.logger.Error("查询需要归档的统计记录失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(stats) == 0 {
|
||||
h.logger.Info("没有需要归档的统计记录")
|
||||
return nil
|
||||
}
|
||||
|
||||
archivedCount := 0
|
||||
for _, stat := range stats {
|
||||
period := stat.PeriodStart.Format("2006-01")
|
||||
redisKey := constants.RedisCommissionStatsKey(stat.AllocationID, period)
|
||||
|
||||
if err := h.archiveStats(ctx, &stat, redisKey); err != nil {
|
||||
h.logger.Error("归档统计失败",
|
||||
zap.Uint("allocation_id", stat.AllocationID),
|
||||
zap.String("period", period),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
archivedCount++
|
||||
}
|
||||
|
||||
h.logger.Info("统计归档完成",
|
||||
zap.Int("total", len(stats)),
|
||||
zap.Int("archived", archivedCount),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CommissionStatsArchiveHandler) archiveStats(ctx context.Context, stats *model.ShopSeriesCommissionStats, redisKey string) error {
|
||||
return h.db.Transaction(func(tx *gorm.DB) error {
|
||||
exists, err := h.redis.Exists(ctx, redisKey).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists > 0 {
|
||||
data, err := h.redis.HGetAll(ctx, redisKey).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(data) > 0 {
|
||||
if err := h.redis.Del(ctx, redisKey).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Model(stats).
|
||||
Where("id = ? AND version = ?", stats.ID, stats.Version).
|
||||
Updates(map[string]interface{}{
|
||||
"status": model.StatsStatusCompleted,
|
||||
"last_updated_at": time.Now(),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
155
internal/task/commission_stats_sync.go
Normal file
155
internal/task/commission_stats_sync.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"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"
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
)
|
||||
|
||||
type CommissionStatsSyncHandler struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewCommissionStatsSyncHandler(
|
||||
db *gorm.DB,
|
||||
redis *redis.Client,
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore,
|
||||
logger *zap.Logger,
|
||||
) *CommissionStatsSyncHandler {
|
||||
return &CommissionStatsSyncHandler{
|
||||
db: db,
|
||||
redis: redis,
|
||||
statsStore: statsStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CommissionStatsSyncHandler) HandleCommissionStatsSync(ctx context.Context, task *asynq.Task) error {
|
||||
ctx = pkggorm.SkipDataPermission(ctx)
|
||||
|
||||
lockKey := constants.RedisCommissionStatsLockKey()
|
||||
locked, err := h.redis.SetNX(ctx, lockKey, "1", 5*time.Minute).Result()
|
||||
if err != nil {
|
||||
h.logger.Error("获取同步锁失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if !locked {
|
||||
h.logger.Info("同步任务已在执行,跳过本次")
|
||||
return nil
|
||||
}
|
||||
defer h.redis.Del(ctx, lockKey)
|
||||
|
||||
pattern := "commission:stats:*"
|
||||
var cursor uint64
|
||||
syncCount := 0
|
||||
|
||||
for {
|
||||
keys, nextCursor, err := h.redis.Scan(ctx, cursor, pattern, 100).Result()
|
||||
if err != nil {
|
||||
h.logger.Error("扫描 Redis keys 失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if err := h.syncStatsFromRedis(ctx, key); err != nil {
|
||||
h.logger.Error("同步统计失败",
|
||||
zap.String("key", key),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
syncCount++
|
||||
}
|
||||
|
||||
cursor = nextCursor
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("统计同步完成", zap.Int("sync_count", syncCount))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *CommissionStatsSyncHandler) syncStatsFromRedis(ctx context.Context, redisKey string) error {
|
||||
parts := strings.Split(redisKey, ":")
|
||||
if len(parts) != 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
allocationID, err := strconv.ParseUint(parts[2], 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
period := parts[3]
|
||||
|
||||
data, err := h.redis.HGetAll(ctx, redisKey).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalCount, _ := strconv.ParseInt(data["total_count"], 10, 64)
|
||||
totalAmount, _ := strconv.ParseInt(data["total_amount"], 10, 64)
|
||||
|
||||
periodStart, periodEnd := parsePeriod(period)
|
||||
|
||||
return h.db.Transaction(func(tx *gorm.DB) error {
|
||||
var stats model.ShopSeriesCommissionStats
|
||||
err := tx.Where("allocation_id = ? AND period_start = ? AND period_end = ?",
|
||||
allocationID, periodStart, periodEnd).
|
||||
First(&stats).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
stats = model.ShopSeriesCommissionStats{
|
||||
AllocationID: uint(allocationID),
|
||||
PeriodType: "monthly",
|
||||
PeriodStart: periodStart,
|
||||
PeriodEnd: periodEnd,
|
||||
TotalSalesCount: totalCount,
|
||||
TotalSalesAmount: totalAmount,
|
||||
Status: "active",
|
||||
LastUpdatedAt: time.Now(),
|
||||
}
|
||||
return tx.Create(&stats).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Model(&stats).
|
||||
Where("version = ?", stats.Version).
|
||||
Updates(map[string]interface{}{
|
||||
"total_sales_count": totalCount,
|
||||
"total_sales_amount": totalAmount,
|
||||
"last_updated_at": time.Now(),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func parsePeriod(period string) (time.Time, time.Time) {
|
||||
t, _ := time.Parse("2006-01", period)
|
||||
periodStart := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
periodEnd := periodStart.AddDate(0, 1, 0).Add(-time.Second)
|
||||
return periodStart, periodEnd
|
||||
}
|
||||
111
internal/task/commission_stats_update.go
Normal file
111
internal/task/commission_stats_update.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
)
|
||||
|
||||
type CommissionStatsUpdatePayload struct {
|
||||
AllocationID uint `json:"allocation_id"`
|
||||
SalesCount int64 `json:"sales_count"`
|
||||
SalesAmount int64 `json:"sales_amount"`
|
||||
}
|
||||
|
||||
type CommissionStatsUpdateHandler struct {
|
||||
redis *redis.Client
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore
|
||||
allocationStore *postgres.ShopSeriesAllocationStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewCommissionStatsUpdateHandler(
|
||||
redis *redis.Client,
|
||||
statsStore *postgres.ShopSeriesCommissionStatsStore,
|
||||
allocationStore *postgres.ShopSeriesAllocationStore,
|
||||
logger *zap.Logger,
|
||||
) *CommissionStatsUpdateHandler {
|
||||
return &CommissionStatsUpdateHandler{
|
||||
redis: redis,
|
||||
statsStore: statsStore,
|
||||
allocationStore: allocationStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CommissionStatsUpdateHandler) HandleCommissionStatsUpdate(ctx context.Context, task *asynq.Task) error {
|
||||
ctx = pkggorm.SkipDataPermission(ctx)
|
||||
|
||||
var payload CommissionStatsUpdatePayload
|
||||
if err := sonic.Unmarshal(task.Payload(), &payload); err != nil {
|
||||
h.logger.Error("解析统计更新任务载荷失败",
|
||||
zap.Error(err),
|
||||
zap.String("task_id", task.ResultWriter().TaskID()),
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
allocation, err := h.allocationStore.GetByID(ctx, payload.AllocationID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取分配记录失败",
|
||||
zap.Uint("allocation_id", payload.AllocationID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
if !allocation.EnableTierCommission {
|
||||
h.logger.Info("分配未启用梯度返佣,跳过统计更新",
|
||||
zap.Uint("allocation_id", payload.AllocationID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
period := getCurrentPeriod(now)
|
||||
redisKey := constants.RedisCommissionStatsKey(payload.AllocationID, period)
|
||||
|
||||
pipe := h.redis.Pipeline()
|
||||
pipe.HIncrBy(ctx, redisKey, "total_count", payload.SalesCount)
|
||||
pipe.HIncrBy(ctx, redisKey, "total_amount", payload.SalesAmount)
|
||||
|
||||
periodEnd := getPeriodEnd(now)
|
||||
expireAt := periodEnd.AddDate(0, 0, 7)
|
||||
pipe.ExpireAt(ctx, redisKey, expireAt)
|
||||
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
h.logger.Error("更新 Redis 统计失败",
|
||||
zap.Uint("allocation_id", payload.AllocationID),
|
||||
zap.String("period", period),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("统计更新成功",
|
||||
zap.Uint("allocation_id", payload.AllocationID),
|
||||
zap.String("period", period),
|
||||
zap.Int64("sales_count", payload.SalesCount),
|
||||
zap.Int64("sales_amount", payload.SalesAmount),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCurrentPeriod(t time.Time) string {
|
||||
return t.Format("2006-01")
|
||||
}
|
||||
|
||||
func getPeriodEnd(t time.Time) time.Time {
|
||||
year, month, _ := t.Date()
|
||||
nextMonth := time.Date(year, month+1, 1, 0, 0, 0, 0, t.Location())
|
||||
return nextMonth.Add(-time.Second)
|
||||
}
|
||||
Reference in New Issue
Block a user