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

@@ -0,0 +1,283 @@
package polling
import (
"context"
"time"
"github.com/redis/go-redis/v9"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// MonitoringService 轮询监控服务
type MonitoringService struct {
redis *redis.Client
}
// NewMonitoringService 创建轮询监控服务实例
func NewMonitoringService(redis *redis.Client) *MonitoringService {
return &MonitoringService{redis: redis}
}
// OverviewStats 总览统计
type OverviewStats struct {
TotalCards int64 `json:"total_cards"` // 总卡数
InitializedCards int64 `json:"initialized_cards"` // 已初始化卡数
InitProgress float64 `json:"init_progress"` // 初始化进度 (0-100)
IsInitializing bool `json:"is_initializing"` // 是否正在初始化
RealnameQueueSize int64 `json:"realname_queue_size"` // 实名检查队列大小
CarddataQueueSize int64 `json:"carddata_queue_size"` // 流量检查队列大小
PackageQueueSize int64 `json:"package_queue_size"` // 套餐检查队列大小
}
// QueueStatus 队列状态
type QueueStatus struct {
TaskType string `json:"task_type"` // 任务类型
TaskTypeName string `json:"task_type_name"` // 任务类型名称
QueueSize int64 `json:"queue_size"` // 队列大小
ManualPending int64 `json:"manual_pending"` // 手动触发待处理数
DueCount int64 `json:"due_count"` // 到期待处理数
AvgWaitTime float64 `json:"avg_wait_time_s"` // 平均等待时间(秒)
}
// TaskStats 任务统计
type TaskStats struct {
TaskType string `json:"task_type"` // 任务类型
TaskTypeName string `json:"task_type_name"` // 任务类型名称
SuccessCount1h int64 `json:"success_count_1h"` // 1小时成功数
FailureCount1h int64 `json:"failure_count_1h"` // 1小时失败数
TotalCount1h int64 `json:"total_count_1h"` // 1小时总数
SuccessRate float64 `json:"success_rate"` // 成功率 (0-100)
AvgDurationMs float64 `json:"avg_duration_ms"` // 平均耗时(毫秒)
}
// InitProgress 初始化进度
type InitProgress struct {
TotalCards int64 `json:"total_cards"` // 总卡数
InitializedCards int64 `json:"initialized_cards"` // 已初始化卡数
Progress float64 `json:"progress"` // 进度百分比 (0-100)
IsComplete bool `json:"is_complete"` // 是否完成
StartedAt time.Time `json:"started_at"` // 开始时间
EstimatedETA string `json:"estimated_eta"` // 预计完成时间
}
// GetOverview 获取总览统计
func (s *MonitoringService) GetOverview(ctx context.Context) (*OverviewStats, error) {
stats := &OverviewStats{}
// 获取初始化进度
progressKey := constants.RedisPollingInitProgressKey()
progressData, err := s.redis.HGetAll(ctx, progressKey).Result()
if err != nil && err != redis.Nil {
return nil, errors.Wrap(errors.CodeRedisError, err, "获取初始化进度失败")
}
if total, ok := progressData["total"]; ok {
stats.TotalCards = parseInt64(total)
}
if initialized, ok := progressData["initialized"]; ok {
stats.InitializedCards = parseInt64(initialized)
}
if stats.TotalCards > 0 {
stats.InitProgress = float64(stats.InitializedCards) / float64(stats.TotalCards) * 100
}
stats.IsInitializing = stats.InitializedCards < stats.TotalCards && stats.TotalCards > 0
// 获取队列大小
stats.RealnameQueueSize, _ = s.redis.ZCard(ctx, constants.RedisPollingQueueRealnameKey()).Result()
stats.CarddataQueueSize, _ = s.redis.ZCard(ctx, constants.RedisPollingQueueCarddataKey()).Result()
stats.PackageQueueSize, _ = s.redis.ZCard(ctx, constants.RedisPollingQueuePackageKey()).Result()
return stats, nil
}
// GetQueueStatuses 获取所有队列状态
func (s *MonitoringService) GetQueueStatuses(ctx context.Context) ([]*QueueStatus, error) {
taskTypes := []string{
constants.TaskTypePollingRealname,
constants.TaskTypePollingCarddata,
constants.TaskTypePollingPackage,
}
result := make([]*QueueStatus, 0, len(taskTypes))
now := time.Now().Unix()
for _, taskType := range taskTypes {
status := &QueueStatus{
TaskType: taskType,
TaskTypeName: s.getTaskTypeName(taskType),
}
// 获取队列 key
var queueKey string
switch taskType {
case constants.TaskTypePollingRealname:
queueKey = constants.RedisPollingQueueRealnameKey()
case constants.TaskTypePollingCarddata:
queueKey = constants.RedisPollingQueueCarddataKey()
case constants.TaskTypePollingPackage:
queueKey = constants.RedisPollingQueuePackageKey()
}
// 获取队列大小
status.QueueSize, _ = s.redis.ZCard(ctx, queueKey).Result()
// 获取到期数量score <= now
status.DueCount, _ = s.redis.ZCount(ctx, queueKey, "-inf", formatInt64(now)).Result()
// 获取手动触发队列待处理数
manualKey := constants.RedisPollingManualQueueKey(taskType)
status.ManualPending, _ = s.redis.LLen(ctx, manualKey).Result()
// 计算平均等待时间取最早的10个任务的平均等待时间
earliest, err := s.redis.ZRangeWithScores(ctx, queueKey, 0, 9).Result()
if err == nil && len(earliest) > 0 {
var totalWait float64
for _, z := range earliest {
waitTime := float64(now) - z.Score
if waitTime > 0 {
totalWait += waitTime
}
}
status.AvgWaitTime = totalWait / float64(len(earliest))
}
result = append(result, status)
}
return result, nil
}
// GetTaskStatuses 获取所有任务统计
func (s *MonitoringService) GetTaskStatuses(ctx context.Context) ([]*TaskStats, error) {
taskTypes := []string{
constants.TaskTypePollingRealname,
constants.TaskTypePollingCarddata,
constants.TaskTypePollingPackage,
}
result := make([]*TaskStats, 0, len(taskTypes))
for _, taskType := range taskTypes {
stats := &TaskStats{
TaskType: taskType,
TaskTypeName: s.getTaskTypeName(taskType),
}
// 获取统计数据
statsKey := constants.RedisPollingStatsKey(taskType)
data, err := s.redis.HGetAll(ctx, statsKey).Result()
if err != nil && err != redis.Nil {
continue
}
if success, ok := data["success_count_1h"]; ok {
stats.SuccessCount1h = parseInt64(success)
}
if failure, ok := data["failure_count_1h"]; ok {
stats.FailureCount1h = parseInt64(failure)
}
stats.TotalCount1h = stats.SuccessCount1h + stats.FailureCount1h
if stats.TotalCount1h > 0 {
stats.SuccessRate = float64(stats.SuccessCount1h) / float64(stats.TotalCount1h) * 100
if duration, ok := data["total_duration_1h"]; ok {
totalDuration := parseInt64(duration)
stats.AvgDurationMs = float64(totalDuration) / float64(stats.TotalCount1h)
}
}
result = append(result, stats)
}
return result, nil
}
// GetInitProgress 获取初始化进度详情
func (s *MonitoringService) GetInitProgress(ctx context.Context) (*InitProgress, error) {
progressKey := constants.RedisPollingInitProgressKey()
data, err := s.redis.HGetAll(ctx, progressKey).Result()
if err != nil && err != redis.Nil {
return nil, errors.Wrap(errors.CodeRedisError, err, "获取初始化进度失败")
}
progress := &InitProgress{}
if total, ok := data["total"]; ok {
progress.TotalCards = parseInt64(total)
}
if initialized, ok := data["initialized"]; ok {
progress.InitializedCards = parseInt64(initialized)
}
if startedAt, ok := data["started_at"]; ok {
if t, err := time.Parse(time.RFC3339, startedAt); err == nil {
progress.StartedAt = t
}
}
if progress.TotalCards > 0 {
progress.Progress = float64(progress.InitializedCards) / float64(progress.TotalCards) * 100
}
progress.IsComplete = progress.InitializedCards >= progress.TotalCards && progress.TotalCards > 0
// 估算完成时间
if !progress.IsComplete && progress.InitializedCards > 0 && !progress.StartedAt.IsZero() {
elapsed := time.Since(progress.StartedAt)
remaining := progress.TotalCards - progress.InitializedCards
rate := float64(progress.InitializedCards) / elapsed.Seconds()
if rate > 0 {
etaSeconds := float64(remaining) / rate
eta := time.Now().Add(time.Duration(etaSeconds) * time.Second)
progress.EstimatedETA = eta.Format("15:04:05")
}
}
return progress, nil
}
// getTaskTypeName 获取任务类型的中文名称
func (s *MonitoringService) getTaskTypeName(taskType string) string {
switch taskType {
case constants.TaskTypePollingRealname:
return "实名检查"
case constants.TaskTypePollingCarddata:
return "流量检查"
case constants.TaskTypePollingPackage:
return "套餐检查"
default:
return taskType
}
}
// parseInt64 解析字符串为 int64
func parseInt64(s string) int64 {
var result int64
for _, c := range s {
if c >= '0' && c <= '9' {
result = result*10 + int64(c-'0')
}
}
return result
}
// formatInt64 格式化 int64 为字符串
func formatInt64(n int64) string {
if n == 0 {
return "0"
}
var result []byte
negative := n < 0
if negative {
n = -n
}
for n > 0 {
result = append([]byte{byte('0' + n%10)}, result...)
n /= 10
}
if negative {
result = append([]byte{'-'}, result...)
}
return string(result)
}