feat: 实现 IoT 卡轮询系统(支持千万级卡规模)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m35s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m35s
实现功能: - 实名状态检查轮询(可配置间隔) - 卡流量检查轮询(支持跨月流量追踪) - 套餐检查与超额自动停机 - 分布式并发控制(Redis 信号量) - 手动触发轮询(单卡/批量/条件筛选) - 数据清理配置与执行 - 告警规则与历史记录 - 实时监控统计(队列/性能/并发) 性能优化: - Redis 缓存卡信息,减少 DB 查询 - Pipeline 批量写入 Redis - 异步流量记录写入 - 渐进式初始化(10万卡/批) 压测工具(scripts/benchmark/): - Mock Gateway 模拟上游服务 - 测试卡生成器 - 配置初始化脚本 - 实时监控脚本 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package packagepkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -16,20 +17,23 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
}
|
||||
|
||||
func New(
|
||||
packageStore *postgres.PackageStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
packageAllocationStore: packageAllocationStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,8 +266,9 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询套餐系列
|
||||
// 批量查询套餐系列(名称和配置)
|
||||
seriesMap := make(map[uint]string)
|
||||
seriesConfigMap := make(map[uint]*model.OneTimeCommissionConfig)
|
||||
if len(seriesIDMap) > 0 {
|
||||
seriesIDs := make([]uint, 0, len(seriesIDMap))
|
||||
for id := range seriesIDMap {
|
||||
@@ -275,19 +280,29 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
}
|
||||
for _, series := range seriesList {
|
||||
seriesMap[series.ID] = series.SeriesName
|
||||
// 解析一次性佣金配置
|
||||
if series.EnableOneTimeCommission {
|
||||
config, _ := series.GetOneTimeCommissionConfig()
|
||||
if config != nil {
|
||||
seriesConfigMap[series.ID] = config
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
var allocationMap map[uint]*model.ShopPackageAllocation
|
||||
var seriesAllocationMap map[uint]*model.ShopSeriesAllocation
|
||||
if userType == constants.UserTypeAgent && shopID > 0 && len(packageIDs) > 0 {
|
||||
allocationMap = s.batchGetAllocationsForShop(ctx, shopID, packageIDs)
|
||||
// 批量获取店铺的系列分配
|
||||
seriesAllocationMap = s.batchGetSeriesAllocationsForShop(ctx, shopID, seriesIDMap)
|
||||
}
|
||||
|
||||
responses := make([]*dto.PackageResponse, len(packages))
|
||||
for i, pkg := range packages {
|
||||
resp := s.toResponseWithAllocation(pkg, allocationMap)
|
||||
resp := s.toResponseWithAllocation(ctx, pkg, allocationMap, seriesAllocationMap, seriesConfigMap)
|
||||
if pkg.SeriesID > 0 {
|
||||
if seriesName, ok := seriesMap[pkg.SeriesID]; ok {
|
||||
resp.SeriesName = &seriesName
|
||||
@@ -299,6 +314,27 @@ func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
// batchGetSeriesAllocationsForShop 批量获取店铺的系列分配
|
||||
func (s *Service) batchGetSeriesAllocationsForShop(ctx context.Context, shopID uint, seriesIDMap map[uint]bool) map[uint]*model.ShopSeriesAllocation {
|
||||
result := make(map[uint]*model.ShopSeriesAllocation)
|
||||
if len(seriesIDMap) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
allocations, err := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
|
||||
if err != nil || len(allocations) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, alloc := range allocations {
|
||||
if seriesIDMap[alloc.SeriesID] {
|
||||
result[alloc.SeriesID] = alloc
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
@@ -408,7 +444,7 @@ func (s *Service) batchGetAllocationsForShop(ctx context.Context, shopID uint, p
|
||||
return allocationMap
|
||||
}
|
||||
|
||||
func (s *Service) toResponseWithAllocation(pkg *model.Package, allocationMap map[uint]*model.ShopPackageAllocation) *dto.PackageResponse {
|
||||
func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package, allocationMap map[uint]*model.ShopPackageAllocation, seriesAllocationMap map[uint]*model.ShopSeriesAllocation, seriesConfigMap map[uint]*model.OneTimeCommissionConfig) *dto.PackageResponse {
|
||||
var seriesID *uint
|
||||
if pkg.SeriesID > 0 {
|
||||
seriesID = &pkg.SeriesID
|
||||
@@ -440,5 +476,77 @@ func (s *Service) toResponseWithAllocation(pkg *model.Package, allocationMap map
|
||||
}
|
||||
}
|
||||
|
||||
// 填充返佣信息(仅代理用户可见)
|
||||
if pkg.SeriesID > 0 && seriesAllocationMap != nil && seriesConfigMap != nil {
|
||||
s.fillCommissionInfo(resp, pkg.SeriesID, seriesAllocationMap, seriesConfigMap)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// fillCommissionInfo 填充返佣信息到响应中
|
||||
func (s *Service) fillCommissionInfo(resp *dto.PackageResponse, seriesID uint, seriesAllocationMap map[uint]*model.ShopSeriesAllocation, seriesConfigMap map[uint]*model.OneTimeCommissionConfig) {
|
||||
seriesAllocation, hasAllocation := seriesAllocationMap[seriesID]
|
||||
config, hasConfig := seriesConfigMap[seriesID]
|
||||
|
||||
if !hasAllocation || !hasConfig {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否启用一次性佣金
|
||||
if !seriesAllocation.EnableOneTimeCommission || !config.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置一次性佣金金额
|
||||
oneTimeAmount := seriesAllocation.OneTimeCommissionAmount
|
||||
resp.OneTimeCommissionAmount = &oneTimeAmount
|
||||
|
||||
// 设置当前返佣比例(格式化为可读字符串)
|
||||
if config.CommissionType == "fixed" {
|
||||
// 固定金额模式:显示代理能拿到的金额
|
||||
resp.CurrentCommissionRate = formatAmount(seriesAllocation.OneTimeCommissionAmount)
|
||||
} else if config.CommissionType == "tiered" && len(config.Tiers) > 0 {
|
||||
// 梯度模式:显示基础金额,并设置梯度信息
|
||||
resp.CurrentCommissionRate = formatAmount(seriesAllocation.OneTimeCommissionAmount)
|
||||
|
||||
// 构建梯度信息
|
||||
tierInfo := s.buildTierInfo(config.Tiers, seriesAllocation.OneTimeCommissionAmount)
|
||||
if tierInfo != nil {
|
||||
resp.TierInfo = tierInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildTierInfo 构建梯度返佣信息
|
||||
func (s *Service) buildTierInfo(tiers []model.OneTimeCommissionTier, currentAmount int64) *dto.CommissionTierInfo {
|
||||
if len(tiers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tierInfo := &dto.CommissionTierInfo{
|
||||
CurrentRate: formatAmount(currentAmount),
|
||||
}
|
||||
|
||||
// 找到下一个可达到的梯度
|
||||
// 梯度按 threshold 升序排列,找到第一个 amount > currentAmount 的梯度
|
||||
for _, tier := range tiers {
|
||||
if tier.Amount > currentAmount {
|
||||
tierInfo.NextThreshold = &tier.Threshold
|
||||
nextRate := formatAmount(tier.Amount)
|
||||
tierInfo.NextRate = nextRate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return tierInfo
|
||||
}
|
||||
|
||||
// formatAmount 格式化金额为可读字符串(分转元)
|
||||
func formatAmount(amountFen int64) string {
|
||||
yuan := float64(amountFen) / 100
|
||||
if yuan == float64(int64(yuan)) {
|
||||
return fmt.Sprintf("%.0f元/张", yuan)
|
||||
}
|
||||
return fmt.Sprintf("%.2f元/张", yuan)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestPackageService_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -94,7 +94,7 @@ func TestPackageService_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -162,7 +162,7 @@ func TestPackageService_UpdateShelfStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -246,7 +246,7 @@ func TestPackageService_Get(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -283,7 +283,7 @@ func TestPackageService_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -328,7 +328,7 @@ func TestPackageService_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -362,7 +362,7 @@ func TestPackageService_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -442,7 +442,7 @@ func TestPackageService_VirtualDataValidation(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
@@ -549,7 +549,7 @@ func TestPackageService_SeriesNameInResponse(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
svc := New(packageStore, packageSeriesStore, nil)
|
||||
svc := New(packageStore, packageSeriesStore, nil, nil)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
|
||||
Reference in New Issue
Block a user