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>
284 lines
8.9 KiB
Go
284 lines
8.9 KiB
Go
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)
|
||
}
|