feat: 实现 IoT 卡轮询系统(支持千万级卡规模)
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:
2026-02-05 17:32:44 +08:00
parent b11edde720
commit 931e140e8e
104 changed files with 16883 additions and 87 deletions

View File

@@ -6,10 +6,17 @@ import (
"os/signal"
"strconv"
"syscall"
"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/gateway"
"github.com/break/junhong_cmp_fiber/internal/polling"
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/bootstrap"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/database"
@@ -97,17 +104,47 @@ func main() {
// 初始化对象存储服务(可选)
storageSvc := initStorage(cfg, appLogger)
// 初始化 Gateway 客户端(可选,用于轮询任务)
gatewayClient := initGateway(cfg, appLogger)
// 创建 Asynq 客户端(用于调度器提交任务)
asynqClient := asynq.NewClient(asynq.RedisClientOpt{
Addr: redisAddr,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
})
defer func() {
if err := asynqClient.Close(); err != nil {
appLogger.Error("关闭 Asynq 客户端失败", zap.Error(err))
}
}()
// 创建 Asynq Worker 服务器
workerServer := queue.NewServer(redisClient, &cfg.Queue, appLogger)
// 创建任务处理器管理器并注册所有处理器
taskHandler := queue.NewHandler(db, redisClient, storageSvc, appLogger)
// 初始化轮询调度器(在创建 Handler 之前,因为 Handler 需要使用调度器作为回调)
scheduler := polling.NewScheduler(db, redisClient, asynqClient, appLogger)
if err := scheduler.Start(ctx); err != nil {
appLogger.Error("启动轮询调度器失败", zap.Error(err))
// 调度器启动失败不阻止 Worker 启动,但不传递给 Handler
} else {
appLogger.Info("轮询调度器已启动")
}
// 创建任务处理器管理器并注册所有处理器(传递 scheduler 作为轮询回调)
taskHandler := queue.NewHandler(db, redisClient, storageSvc, gatewayClient, scheduler, appLogger)
taskHandler.RegisterHandlers()
appLogger.Info("Worker 服务器配置完成",
zap.Int("concurrency", cfg.Queue.Concurrency),
zap.Any("queues", cfg.Queue.Queues))
// 初始化告警服务并启动告警检查器
alertChecker := startAlertChecker(ctx, db, redisClient, appLogger)
// 初始化数据清理服务并启动定时清理任务
cleanupChecker := startCleanupScheduler(ctx, db, appLogger)
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
@@ -125,6 +162,15 @@ func main() {
<-quit
appLogger.Info("正在关闭 Worker 服务器...")
// 停止告警检查器
close(alertChecker)
// 停止数据清理定时任务
close(cleanupChecker)
// 停止轮询调度器
scheduler.Stop()
// 优雅关闭 Worker 服务器(等待正在执行的任务完成)
workerServer.Shutdown()
@@ -150,3 +196,107 @@ func initStorage(cfg *config.Config, appLogger *zap.Logger) *storage.Service {
return storage.NewService(provider, &cfg.Storage)
}
// initGateway 初始化 Gateway 客户端
func initGateway(cfg *config.Config, appLogger *zap.Logger) *gateway.Client {
if cfg.Gateway.BaseURL == "" {
appLogger.Info("Gateway 未配置,跳过初始化(轮询任务将无法查询真实数据)")
return nil
}
client := gateway.NewClient(
cfg.Gateway.BaseURL,
cfg.Gateway.AppID,
cfg.Gateway.AppSecret,
).WithTimeout(time.Duration(cfg.Gateway.Timeout) * time.Second)
appLogger.Info("Gateway 客户端初始化成功",
zap.String("base_url", cfg.Gateway.BaseURL),
zap.String("app_id", cfg.Gateway.AppID))
return client
}
// startAlertChecker 启动告警检查器
// 返回一个 stop channel关闭它可以停止检查器
func startAlertChecker(ctx context.Context, db *gorm.DB, redisClient *redis.Client, appLogger *zap.Logger) chan struct{} {
stopChan := make(chan struct{})
// 创建告警服务所需的 stores
ruleStore := postgres.NewPollingAlertRuleStore(db)
historyStore := postgres.NewPollingAlertHistoryStore(db)
alertService := pollingSvc.NewAlertService(ruleStore, historyStore, redisClient, appLogger)
// 启动检查器 goroutine
go func() {
ticker := time.NewTicker(1 * time.Minute) // 每分钟检查一次
defer ticker.Stop()
appLogger.Info("告警检查器已启动,检查间隔: 1分钟")
for {
select {
case <-ticker.C:
if err := alertService.CheckAlerts(ctx); err != nil {
appLogger.Error("告警检查失败", zap.Error(err))
}
case <-stopChan:
appLogger.Info("告警检查器已停止")
return
case <-ctx.Done():
appLogger.Info("告警检查器因 context 取消而停止")
return
}
}
}()
return stopChan
}
// startCleanupScheduler 启动数据清理定时任务
// 每天凌晨2点运行清理任务
func startCleanupScheduler(ctx context.Context, db *gorm.DB, appLogger *zap.Logger) chan struct{} {
stopChan := make(chan struct{})
// 创建清理服务
configStore := postgres.NewDataCleanupConfigStore(db)
logStore := postgres.NewDataCleanupLogStore(db)
cleanupService := pollingSvc.NewCleanupService(configStore, logStore, appLogger)
go func() {
// 计算到下一个凌晨2点的时间间隔
calcNextRun := func() time.Duration {
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day(), 2, 0, 0, 0, now.Location())
if now.After(next) {
next = next.Add(24 * time.Hour)
}
return time.Until(next)
}
timer := time.NewTimer(calcNextRun())
defer timer.Stop()
appLogger.Info("数据清理定时任务已启动每天凌晨2点执行")
for {
select {
case <-timer.C:
appLogger.Info("开始执行定时数据清理")
if err := cleanupService.RunScheduledCleanup(ctx); err != nil {
appLogger.Error("定时数据清理失败", zap.Error(err))
}
// 重置定时器到下一个凌晨2点
timer.Reset(calcNextRun())
case <-stopChan:
appLogger.Info("数据清理定时任务已停止")
return
case <-ctx.Done():
appLogger.Info("数据清理定时任务因 context 取消而停止")
return
}
}
}()
return stopChan
}