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,291 @@
package admin
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PollingAlertHandler 轮询告警处理器
type PollingAlertHandler struct {
service *polling.AlertService
}
// NewPollingAlertHandler 创建轮询告警处理器
func NewPollingAlertHandler(service *polling.AlertService) *PollingAlertHandler {
return &PollingAlertHandler{service: service}
}
// CreateRule 创建告警规则
// @Summary 创建轮询告警规则
// @Description 创建新的轮询告警规则
// @Tags 轮询管理-告警
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.CreatePollingAlertRuleReq true "创建请求"
// @Success 200 {object} response.Response{data=dto.PollingAlertRuleResp}
// @Router /api/admin/polling-alert-rules [post]
func (h *PollingAlertHandler) CreateRule(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.CreatePollingAlertRuleReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
rule := &model.PollingAlertRule{
RuleName: req.RuleName,
TaskType: req.TaskType,
MetricType: req.MetricType,
Operator: req.Operator,
Threshold: req.Threshold,
AlertLevel: req.AlertLevel,
CooldownMinutes: req.CooldownMinutes,
NotificationChannels: req.NotifyChannels,
}
if err := h.service.CreateRule(ctx, rule); err != nil {
return err
}
return response.Success(c, h.toRuleResp(rule))
}
// ListRules 获取告警规则列表
// @Summary 获取轮询告警规则列表
// @Description 获取所有轮询告警规则
// @Tags 轮询管理-告警
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.PollingAlertRuleListResp}
// @Router /api/admin/polling-alert-rules [get]
func (h *PollingAlertHandler) ListRules(c *fiber.Ctx) error {
ctx := c.UserContext()
rules, err := h.service.ListRules(ctx)
if err != nil {
return err
}
items := make([]*dto.PollingAlertRuleResp, 0, len(rules))
for _, rule := range rules {
items = append(items, h.toRuleResp(rule))
}
return response.Success(c, &dto.PollingAlertRuleListResp{Items: items})
}
// GetRule 获取告警规则详情
// @Summary 获取轮询告警规则详情
// @Description 获取指定轮询告警规则的详细信息
// @Tags 轮询管理-告警
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "规则ID"
// @Success 200 {object} response.Response{data=dto.PollingAlertRuleResp}
// @Router /api/admin/polling-alert-rules/{id} [get]
func (h *PollingAlertHandler) GetRule(c *fiber.Ctx) error {
ctx := c.UserContext()
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return errors.New(errors.CodeInvalidParam, "无效的规则ID")
}
rule, err := h.service.GetRule(ctx, uint(id))
if err != nil {
return err
}
return response.Success(c, h.toRuleResp(rule))
}
// UpdateRule 更新告警规则
// @Summary 更新轮询告警规则
// @Description 更新指定轮询告警规则
// @Tags 轮询管理-告警
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "规则ID"
// @Param request body dto.UpdatePollingAlertRuleReq true "更新请求"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-alert-rules/{id} [put]
func (h *PollingAlertHandler) UpdateRule(c *fiber.Ctx) error {
ctx := c.UserContext()
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return errors.New(errors.CodeInvalidParam, "无效的规则ID")
}
var req dto.UpdatePollingAlertRuleReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
updates := make(map[string]interface{})
if req.RuleName != nil {
updates["rule_name"] = *req.RuleName
}
if req.Threshold != nil {
updates["threshold"] = *req.Threshold
}
if req.AlertLevel != nil {
updates["alert_level"] = *req.AlertLevel
}
if req.Status != nil {
updates["status"] = *req.Status
}
if req.CooldownMinutes != nil {
updates["cooldown_minutes"] = *req.CooldownMinutes
}
if req.NotifyChannels != nil {
updates["notification_channels"] = *req.NotifyChannels
}
if err := h.service.UpdateRule(ctx, uint(id), updates); err != nil {
return err
}
return response.Success(c, nil)
}
// DeleteRule 删除告警规则
// @Summary 删除轮询告警规则
// @Description 删除指定轮询告警规则
// @Tags 轮询管理-告警
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "规则ID"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-alert-rules/{id} [delete]
func (h *PollingAlertHandler) DeleteRule(c *fiber.Ctx) error {
ctx := c.UserContext()
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return errors.New(errors.CodeInvalidParam, "无效的规则ID")
}
if err := h.service.DeleteRule(ctx, uint(id)); err != nil {
return err
}
return response.Success(c, nil)
}
// ListHistory 获取告警历史
// @Summary 获取轮询告警历史
// @Description 获取轮询告警历史记录
// @Tags 轮询管理-告警
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param rule_id query int false "规则ID"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} response.Response{data=dto.PollingAlertHistoryListResp}
// @Router /api/admin/polling-alert-history [get]
func (h *PollingAlertHandler) ListHistory(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.ListPollingAlertHistoryReq
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
histories, total, err := h.service.ListHistory(ctx, req.Page, req.PageSize, req.RuleID)
if err != nil {
return err
}
items := make([]*dto.PollingAlertHistoryResp, 0, len(histories))
for _, h := range histories {
items = append(items, &dto.PollingAlertHistoryResp{
ID: h.ID,
RuleID: h.RuleID,
RuleName: "", // 历史记录中没有规则名称,需要单独查询
TaskType: h.TaskType,
MetricType: h.MetricType,
AlertLevel: h.AlertLevel,
Threshold: h.Threshold,
CurrentValue: h.CurrentValue,
Message: h.AlertMessage,
CreatedAt: h.CreatedAt,
})
}
totalPages := int(total) / req.PageSize
if int(total)%req.PageSize > 0 {
totalPages++
}
return response.Success(c, &dto.PollingAlertHistoryListResp{
Items: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
})
}
func (h *PollingAlertHandler) toRuleResp(rule *model.PollingAlertRule) *dto.PollingAlertRuleResp {
return &dto.PollingAlertRuleResp{
ID: rule.ID,
RuleName: rule.RuleName,
TaskType: rule.TaskType,
TaskTypeName: h.getTaskTypeName(rule.TaskType),
MetricType: rule.MetricType,
MetricTypeName: h.getMetricTypeName(rule.MetricType),
Operator: rule.Operator,
Threshold: rule.Threshold,
AlertLevel: rule.AlertLevel,
Status: int(rule.Status),
CooldownMinutes: rule.CooldownMinutes,
NotifyChannels: rule.NotificationChannels,
CreatedAt: rule.CreatedAt,
UpdatedAt: rule.UpdatedAt,
}
}
func (h *PollingAlertHandler) getTaskTypeName(taskType string) string {
switch taskType {
case constants.TaskTypePollingRealname:
return "实名检查"
case constants.TaskTypePollingCarddata:
return "流量检查"
case constants.TaskTypePollingPackage:
return "套餐检查"
default:
return taskType
}
}
func (h *PollingAlertHandler) getMetricTypeName(metricType string) string {
switch metricType {
case "queue_size":
return "队列积压"
case "success_rate":
return "成功率"
case "avg_duration":
return "平均耗时"
case "concurrency":
return "并发数"
default:
return metricType
}
}

View File

@@ -0,0 +1,351 @@
package admin
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PollingCleanupHandler 轮询数据清理处理器
type PollingCleanupHandler struct {
service *polling.CleanupService
}
// NewPollingCleanupHandler 创建轮询数据清理处理器
func NewPollingCleanupHandler(service *polling.CleanupService) *PollingCleanupHandler {
return &PollingCleanupHandler{service: service}
}
// CreateConfig 创建清理配置
// @Summary 创建数据清理配置
// @Description 创建新的数据清理配置
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.CreateDataCleanupConfigReq true "创建请求"
// @Success 200 {object} response.Response{data=dto.DataCleanupConfigResp}
// @Router /api/admin/data-cleanup-configs [post]
func (h *PollingCleanupHandler) CreateConfig(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.CreateDataCleanupConfigReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
config := &model.DataCleanupConfig{
TargetTable: req.TargetTable,
RetentionDays: req.RetentionDays,
BatchSize: req.BatchSize,
Description: req.Description,
}
if err := h.service.CreateConfig(ctx, config); err != nil {
return err
}
return response.Success(c, h.toConfigResp(config))
}
// ListConfigs 获取清理配置列表
// @Summary 获取数据清理配置列表
// @Description 获取所有数据清理配置
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.DataCleanupConfigListResp}
// @Router /api/admin/data-cleanup-configs [get]
func (h *PollingCleanupHandler) ListConfigs(c *fiber.Ctx) error {
ctx := c.UserContext()
configs, err := h.service.ListConfigs(ctx)
if err != nil {
return err
}
items := make([]*dto.DataCleanupConfigResp, 0, len(configs))
for _, config := range configs {
items = append(items, h.toConfigResp(config))
}
return response.Success(c, &dto.DataCleanupConfigListResp{Items: items})
}
// GetConfig 获取清理配置详情
// @Summary 获取数据清理配置详情
// @Description 获取指定数据清理配置的详细信息
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "配置ID"
// @Success 200 {object} response.Response{data=dto.DataCleanupConfigResp}
// @Router /api/admin/data-cleanup-configs/{id} [get]
func (h *PollingCleanupHandler) GetConfig(c *fiber.Ctx) error {
ctx := c.UserContext()
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return errors.New(errors.CodeInvalidParam, "无效的配置ID")
}
config, err := h.service.GetConfig(ctx, uint(id))
if err != nil {
return err
}
return response.Success(c, h.toConfigResp(config))
}
// UpdateConfig 更新清理配置
// @Summary 更新数据清理配置
// @Description 更新指定数据清理配置
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "配置ID"
// @Param request body dto.UpdateDataCleanupConfigReq true "更新请求"
// @Success 200 {object} response.Response
// @Router /api/admin/data-cleanup-configs/{id} [put]
func (h *PollingCleanupHandler) UpdateConfig(c *fiber.Ctx) error {
ctx := c.UserContext()
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return errors.New(errors.CodeInvalidParam, "无效的配置ID")
}
var req dto.UpdateDataCleanupConfigReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
updates := make(map[string]any)
if req.RetentionDays != nil {
updates["retention_days"] = *req.RetentionDays
}
if req.BatchSize != nil {
updates["batch_size"] = *req.BatchSize
}
if req.Enabled != nil {
updates["enabled"] = *req.Enabled
}
if req.Description != nil {
updates["description"] = *req.Description
}
// 获取当前用户ID
userID := middleware.GetUserIDFromContext(ctx)
if userID > 0 {
updates["updated_by"] = userID
}
if err := h.service.UpdateConfig(ctx, uint(id), updates); err != nil {
return err
}
return response.Success(c, nil)
}
// DeleteConfig 删除清理配置
// @Summary 删除数据清理配置
// @Description 删除指定数据清理配置
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "配置ID"
// @Success 200 {object} response.Response
// @Router /api/admin/data-cleanup-configs/{id} [delete]
func (h *PollingCleanupHandler) DeleteConfig(c *fiber.Ctx) error {
ctx := c.UserContext()
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return errors.New(errors.CodeInvalidParam, "无效的配置ID")
}
if err := h.service.DeleteConfig(ctx, uint(id)); err != nil {
return err
}
return response.Success(c, nil)
}
// ListLogs 获取清理日志列表
// @Summary 获取数据清理日志列表
// @Description 获取数据清理日志记录
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param table_name query string false "表名筛选"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} response.Response{data=dto.DataCleanupLogListResp}
// @Router /api/admin/data-cleanup-logs [get]
func (h *PollingCleanupHandler) ListLogs(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.ListDataCleanupLogReq
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
logs, total, err := h.service.ListLogs(ctx, req.Page, req.PageSize, req.TableName)
if err != nil {
return err
}
items := make([]*dto.DataCleanupLogResp, 0, len(logs))
for _, log := range logs {
items = append(items, h.toLogResp(log))
}
totalPages := int(total) / req.PageSize
if int(total)%req.PageSize > 0 {
totalPages++
}
return response.Success(c, &dto.DataCleanupLogListResp{
Items: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
})
}
// Preview 预览待清理数据
// @Summary 预览待清理数据
// @Description 预览各表待清理的数据量
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.DataCleanupPreviewResp}
// @Router /api/admin/data-cleanup/preview [get]
func (h *PollingCleanupHandler) Preview(c *fiber.Ctx) error {
ctx := c.UserContext()
previews, err := h.service.Preview(ctx)
if err != nil {
return err
}
items := make([]*dto.DataCleanupPreviewItem, 0, len(previews))
for _, p := range previews {
items = append(items, &dto.DataCleanupPreviewItem{
TableName: p.TableName,
RetentionDays: p.RetentionDays,
RecordCount: p.RecordCount,
Description: p.Description,
})
}
return response.Success(c, &dto.DataCleanupPreviewResp{Items: items})
}
// GetProgress 获取清理进度
// @Summary 获取数据清理进度
// @Description 获取当前数据清理任务的进度
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.DataCleanupProgressResp}
// @Router /api/admin/data-cleanup/progress [get]
func (h *PollingCleanupHandler) GetProgress(c *fiber.Ctx) error {
ctx := c.UserContext()
progress, err := h.service.GetProgress(ctx)
if err != nil {
return err
}
resp := &dto.DataCleanupProgressResp{
IsRunning: progress.IsRunning,
CurrentTable: progress.CurrentTable,
TotalTables: progress.TotalTables,
ProcessedTables: progress.ProcessedTables,
TotalDeleted: progress.TotalDeleted,
StartedAt: progress.StartedAt,
}
if progress.LastLog != nil {
resp.LastLog = h.toLogResp(progress.LastLog)
}
return response.Success(c, resp)
}
// TriggerCleanup 手动触发清理
// @Summary 手动触发数据清理
// @Description 手动触发数据清理任务
// @Tags 轮询管理-数据清理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.TriggerDataCleanupReq true "触发请求"
// @Success 200 {object} response.Response
// @Router /api/admin/data-cleanup/trigger [post]
func (h *PollingCleanupHandler) TriggerCleanup(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.TriggerDataCleanupReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
// 获取当前用户ID
userID := middleware.GetUserIDFromContext(ctx)
// 异步执行清理
go func() {
_ = h.service.TriggerCleanup(ctx, req.TableName, userID)
}()
return response.Success(c, nil)
}
func (h *PollingCleanupHandler) toConfigResp(config *model.DataCleanupConfig) *dto.DataCleanupConfigResp {
return &dto.DataCleanupConfigResp{
ID: config.ID,
TargetTable: config.TargetTable,
RetentionDays: config.RetentionDays,
BatchSize: config.BatchSize,
Enabled: int(config.Enabled),
Description: config.Description,
CreatedAt: config.CreatedAt,
UpdatedAt: config.UpdatedAt,
UpdatedBy: config.UpdatedBy,
}
}
func (h *PollingCleanupHandler) toLogResp(log *model.DataCleanupLog) *dto.DataCleanupLogResp {
return &dto.DataCleanupLogResp{
ID: log.ID,
TargetTable: log.TargetTable,
CleanupType: log.CleanupType,
RetentionDays: log.RetentionDays,
DeletedCount: log.DeletedCount,
DurationMs: log.DurationMs,
Status: log.Status,
ErrorMessage: log.ErrorMessage,
StartedAt: log.StartedAt,
CompletedAt: log.CompletedAt,
TriggeredBy: log.TriggeredBy,
}
}

View File

@@ -0,0 +1,147 @@
package admin
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PollingConcurrencyHandler 轮询并发控制处理器
type PollingConcurrencyHandler struct {
service *polling.ConcurrencyService
}
// NewPollingConcurrencyHandler 创建轮询并发控制处理器
func NewPollingConcurrencyHandler(service *polling.ConcurrencyService) *PollingConcurrencyHandler {
return &PollingConcurrencyHandler{service: service}
}
// List 获取所有并发配置
// @Summary 获取轮询并发配置列表
// @Description 获取所有轮询任务类型的并发配置及当前状态
// @Tags 轮询管理-并发控制
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.PollingConcurrencyListResp}
// @Router /api/admin/polling-concurrency [get]
func (h *PollingConcurrencyHandler) List(c *fiber.Ctx) error {
ctx := c.UserContext()
statuses, err := h.service.List(ctx)
if err != nil {
return err
}
items := make([]*dto.PollingConcurrencyResp, 0, len(statuses))
for _, s := range statuses {
items = append(items, &dto.PollingConcurrencyResp{
TaskType: s.TaskType,
TaskTypeName: s.TaskTypeName,
MaxConcurrency: s.MaxConcurrency,
Current: s.Current,
Available: s.Available,
Utilization: s.Utilization,
})
}
return response.Success(c, &dto.PollingConcurrencyListResp{Items: items})
}
// Get 获取指定任务类型的并发配置
// @Summary 获取指定任务类型的并发配置
// @Description 获取指定轮询任务类型的并发配置及当前状态
// @Tags 轮询管理-并发控制
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param task_type path string true "任务类型"
// @Success 200 {object} response.Response{data=dto.PollingConcurrencyResp}
// @Router /api/admin/polling-concurrency/{task_type} [get]
func (h *PollingConcurrencyHandler) Get(c *fiber.Ctx) error {
ctx := c.UserContext()
taskType := c.Params("task_type")
if taskType == "" {
return errors.New(errors.CodeInvalidParam, "任务类型不能为空")
}
status, err := h.service.GetByTaskType(ctx, taskType)
if err != nil {
return err
}
return response.Success(c, &dto.PollingConcurrencyResp{
TaskType: status.TaskType,
TaskTypeName: status.TaskTypeName,
MaxConcurrency: status.MaxConcurrency,
Current: status.Current,
Available: status.Available,
Utilization: status.Utilization,
})
}
// Update 更新并发配置
// @Summary 更新轮询并发配置
// @Description 更新指定轮询任务类型的最大并发数
// @Tags 轮询管理-并发控制
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param task_type path string true "任务类型"
// @Param request body dto.UpdatePollingConcurrencyReq true "更新请求"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-concurrency/{task_type} [put]
func (h *PollingConcurrencyHandler) Update(c *fiber.Ctx) error {
ctx := c.UserContext()
taskType := c.Params("task_type")
if taskType == "" {
return errors.New(errors.CodeInvalidParam, "任务类型不能为空")
}
var req dto.UpdatePollingConcurrencyReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
userID := middleware.GetUserIDFromContext(ctx)
if err := h.service.UpdateMaxConcurrency(ctx, taskType, req.MaxConcurrency, userID); err != nil {
return err
}
return response.Success(c, nil)
}
// Reset 重置并发计数
// @Summary 重置轮询并发计数
// @Description 重置指定轮询任务类型的当前并发计数为0用于信号量修复
// @Tags 轮询管理-并发控制
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.ResetPollingConcurrencyReq true "重置请求"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-concurrency/reset [post]
func (h *PollingConcurrencyHandler) Reset(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.ResetPollingConcurrencyReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
if req.TaskType == "" {
return errors.New(errors.CodeInvalidParam, "任务类型不能为空")
}
if err := h.service.ResetConcurrency(ctx, req.TaskType); err != nil {
return err
}
return response.Success(c, nil)
}

View File

@@ -0,0 +1,193 @@
package admin
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
pollingService "github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PollingConfigHandler 轮询配置 Handler
type PollingConfigHandler struct {
service *pollingService.ConfigService
}
// NewPollingConfigHandler 创建轮询配置 Handler 实例
func NewPollingConfigHandler(service *pollingService.ConfigService) *PollingConfigHandler {
return &PollingConfigHandler{service: service}
}
// List 获取轮询配置列表
// @Summary 获取轮询配置列表
// @Description 获取轮询配置列表,支持分页和筛选
// @Tags 轮询配置管理
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Param status query int false "状态 (1:启用, 0:禁用)"
// @Param card_condition query string false "卡状态条件"
// @Param card_category query string false "卡业务类型"
// @Param carrier_id query int false "运营商ID"
// @Param config_name query string false "配置名称"
// @Success 200 {object} response.Response{data=dto.PollingConfigPageResult}
// @Router /api/admin/polling-configs [get]
func (h *PollingConfigHandler) List(c *fiber.Ctx) error {
var req dto.PollingConfigListRequest
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
configs, total, err := h.service.List(c.UserContext(), &req)
if err != nil {
return err
}
return response.SuccessWithPagination(c, configs, total, req.Page, req.PageSize)
}
// Create 创建轮询配置
// @Summary 创建轮询配置
// @Description 创建新的轮询配置
// @Tags 轮询配置管理
// @Accept json
// @Produce json
// @Param body body dto.CreatePollingConfigRequest true "创建轮询配置请求"
// @Success 200 {object} response.Response{data=dto.PollingConfigResponse}
// @Router /api/admin/polling-configs [post]
func (h *PollingConfigHandler) Create(c *fiber.Ctx) error {
var req dto.CreatePollingConfigRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
config, err := h.service.Create(c.UserContext(), &req)
if err != nil {
return err
}
return response.Success(c, config)
}
// Get 获取轮询配置详情
// @Summary 获取轮询配置详情
// @Description 根据 ID 获取轮询配置详情
// @Tags 轮询配置管理
// @Accept json
// @Produce json
// @Param id path int true "配置ID"
// @Success 200 {object} response.Response{data=dto.PollingConfigResponse}
// @Router /api/admin/polling-configs/{id} [get]
func (h *PollingConfigHandler) Get(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的配置 ID")
}
config, err := h.service.Get(c.UserContext(), uint(id))
if err != nil {
return err
}
return response.Success(c, config)
}
// Update 更新轮询配置
// @Summary 更新轮询配置
// @Description 根据 ID 更新轮询配置
// @Tags 轮询配置管理
// @Accept json
// @Produce json
// @Param id path int true "配置ID"
// @Param body body dto.UpdatePollingConfigRequest true "更新轮询配置请求"
// @Success 200 {object} response.Response{data=dto.PollingConfigResponse}
// @Router /api/admin/polling-configs/{id} [put]
func (h *PollingConfigHandler) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的配置 ID")
}
var req dto.UpdatePollingConfigRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
config, err := h.service.Update(c.UserContext(), uint(id), &req)
if err != nil {
return err
}
return response.Success(c, config)
}
// Delete 删除轮询配置
// @Summary 删除轮询配置
// @Description 根据 ID 删除轮询配置
// @Tags 轮询配置管理
// @Accept json
// @Produce json
// @Param id path int true "配置ID"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-configs/{id} [delete]
func (h *PollingConfigHandler) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的配置 ID")
}
if err := h.service.Delete(c.UserContext(), uint(id)); err != nil {
return err
}
return response.Success(c, nil)
}
// UpdateStatus 更新轮询配置状态
// @Summary 更新轮询配置状态
// @Description 启用或禁用轮询配置
// @Tags 轮询配置管理
// @Accept json
// @Produce json
// @Param id path int true "配置ID"
// @Param body body dto.UpdatePollingConfigStatusRequest true "更新状态请求"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-configs/{id}/status [put]
func (h *PollingConfigHandler) UpdateStatus(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "无效的配置 ID")
}
var req dto.UpdatePollingConfigStatusRequest
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
if err := h.service.UpdateStatus(c.UserContext(), uint(id), req.Status); err != nil {
return err
}
return response.Success(c, nil)
}
// ListEnabled 获取所有启用的配置
// @Summary 获取所有启用的配置
// @Description 获取所有启用状态的轮询配置,按优先级排序
// @Tags 轮询配置管理
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]dto.PollingConfigResponse}
// @Router /api/admin/polling-configs/enabled [get]
func (h *PollingConfigHandler) ListEnabled(c *fiber.Ctx) error {
configs, err := h.service.ListEnabled(c.UserContext())
if err != nil {
return err
}
return response.Success(c, configs)
}

View File

@@ -0,0 +1,311 @@
package admin
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/polling"
"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/pkg/response"
)
// PollingManualTriggerHandler 轮询手动触发处理器
type PollingManualTriggerHandler struct {
service *polling.ManualTriggerService
}
// NewPollingManualTriggerHandler 创建轮询手动触发处理器
func NewPollingManualTriggerHandler(service *polling.ManualTriggerService) *PollingManualTriggerHandler {
return &PollingManualTriggerHandler{service: service}
}
// TriggerSingle 单卡手动触发
// @Summary 单卡手动触发
// @Description 触发单张卡的轮询任务
// @Tags 轮询管理-手动触发
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.TriggerSingleReq true "触发请求"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-manual-trigger/single [post]
func (h *PollingManualTriggerHandler) TriggerSingle(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.TriggerSingleReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return errors.New(errors.CodeUnauthorized)
}
if err := h.service.TriggerSingle(ctx, req.CardID, req.TaskType, userID); err != nil {
return err
}
return response.Success(c, nil)
}
// TriggerBatch 批量手动触发
// @Summary 批量手动触发
// @Description 批量触发多张卡的轮询任务
// @Tags 轮询管理-手动触发
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.TriggerBatchReq true "触发请求"
// @Success 200 {object} response.Response{data=dto.ManualTriggerLogResp}
// @Router /api/admin/polling-manual-trigger/batch [post]
func (h *PollingManualTriggerHandler) TriggerBatch(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.TriggerBatchReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return errors.New(errors.CodeUnauthorized)
}
log, err := h.service.TriggerBatch(ctx, req.CardIDs, req.TaskType, userID)
if err != nil {
return err
}
return response.Success(c, h.toLogResp(log))
}
// TriggerByCondition 条件筛选触发
// @Summary 条件筛选触发
// @Description 根据条件筛选卡并触发轮询任务
// @Tags 轮询管理-手动触发
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.TriggerByConditionReq true "触发请求"
// @Success 200 {object} response.Response{data=dto.ManualTriggerLogResp}
// @Router /api/admin/polling-manual-trigger/by-condition [post]
func (h *PollingManualTriggerHandler) TriggerByCondition(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.TriggerByConditionReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return errors.New(errors.CodeUnauthorized)
}
filter := &polling.ConditionFilter{
CardStatus: req.CardStatus,
CarrierCode: req.CarrierCode,
CardType: req.CardType,
ShopID: req.ShopID,
PackageIDs: req.PackageIDs,
EnablePolling: req.EnablePolling,
Limit: req.Limit,
}
log, err := h.service.TriggerByCondition(ctx, filter, req.TaskType, userID)
if err != nil {
return err
}
return response.Success(c, h.toLogResp(log))
}
// GetStatus 获取触发状态
// @Summary 获取手动触发状态
// @Description 获取当前用户的手动触发状态
// @Tags 轮询管理-手动触发
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.ManualTriggerStatusResp}
// @Router /api/admin/polling-manual-trigger/status [get]
func (h *PollingManualTriggerHandler) GetStatus(c *fiber.Ctx) error {
ctx := c.UserContext()
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return errors.New(errors.CodeUnauthorized)
}
// 获取正在运行的任务
runningTasks, err := h.service.GetRunningTasks(ctx, userID)
if err != nil {
return err
}
items := make([]*dto.ManualTriggerLogResp, 0, len(runningTasks))
for _, log := range runningTasks {
items = append(items, h.toLogResp(log))
}
// 获取各队列大小
queueSizes := make(map[string]int64)
for _, taskType := range []string{
constants.TaskTypePollingRealname,
constants.TaskTypePollingCarddata,
constants.TaskTypePollingPackage,
} {
size, _ := h.service.GetQueueSize(ctx, taskType)
queueSizes[taskType] = size
}
return response.Success(c, &dto.ManualTriggerStatusResp{
RunningTasks: items,
QueueSizes: queueSizes,
})
}
// ListHistory 获取触发历史
// @Summary 获取手动触发历史
// @Description 获取手动触发历史记录
// @Tags 轮询管理-手动触发
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param task_type query string false "任务类型筛选"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} response.Response{data=dto.ManualTriggerLogListResp}
// @Router /api/admin/polling-manual-trigger/history [get]
func (h *PollingManualTriggerHandler) ListHistory(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.ListManualTriggerLogReq
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
logs, total, err := h.service.ListHistory(ctx, req.Page, req.PageSize, req.TaskType, nil)
if err != nil {
return err
}
items := make([]*dto.ManualTriggerLogResp, 0, len(logs))
for _, log := range logs {
items = append(items, h.toLogResp(log))
}
totalPages := int(total) / req.PageSize
if int(total)%req.PageSize > 0 {
totalPages++
}
return response.Success(c, &dto.ManualTriggerLogListResp{
Items: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
})
}
// CancelTrigger 取消触发任务
// @Summary 取消手动触发任务
// @Description 取消正在执行的手动触发任务
// @Tags 轮询管理-手动触发
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.CancelTriggerReq true "取消请求"
// @Success 200 {object} response.Response
// @Router /api/admin/polling-manual-trigger/cancel [post]
func (h *PollingManualTriggerHandler) CancelTrigger(c *fiber.Ctx) error {
ctx := c.UserContext()
var req dto.CancelTriggerReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam)
}
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return errors.New(errors.CodeUnauthorized)
}
if err := h.service.CancelTrigger(ctx, req.TriggerID, userID); err != nil {
return err
}
return response.Success(c, nil)
}
func (h *PollingManualTriggerHandler) toLogResp(log *model.PollingManualTriggerLog) *dto.ManualTriggerLogResp {
return &dto.ManualTriggerLogResp{
ID: log.ID,
TaskType: log.TaskType,
TaskTypeName: h.getTaskTypeName(log.TaskType),
TriggerType: log.TriggerType,
TriggerTypeName: h.getTriggerTypeName(log.TriggerType),
TotalCount: log.TotalCount,
ProcessedCount: log.ProcessedCount,
SuccessCount: log.SuccessCount,
FailedCount: log.FailedCount,
Status: log.Status,
StatusName: h.getStatusName(log.Status),
TriggeredBy: log.TriggeredBy,
TriggeredAt: log.TriggeredAt,
CompletedAt: log.CompletedAt,
}
}
func (h *PollingManualTriggerHandler) getTaskTypeName(taskType string) string {
switch taskType {
case constants.TaskTypePollingRealname:
return "实名检查"
case constants.TaskTypePollingCarddata:
return "流量检查"
case constants.TaskTypePollingPackage:
return "套餐检查"
default:
return taskType
}
}
func (h *PollingManualTriggerHandler) getTriggerTypeName(triggerType string) string {
switch triggerType {
case "single":
return "单卡触发"
case "batch":
return "批量触发"
case "by_condition":
return "条件筛选"
default:
return triggerType
}
}
func (h *PollingManualTriggerHandler) getStatusName(status string) string {
switch status {
case "pending":
return "待处理"
case "processing":
return "处理中"
case "completed":
return "已完成"
case "cancelled":
return "已取消"
default:
return status
}
}

View File

@@ -0,0 +1,139 @@
package admin
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/polling"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
// PollingMonitoringHandler 轮询监控处理器
type PollingMonitoringHandler struct {
service *polling.MonitoringService
}
// NewPollingMonitoringHandler 创建轮询监控处理器
func NewPollingMonitoringHandler(service *polling.MonitoringService) *PollingMonitoringHandler {
return &PollingMonitoringHandler{service: service}
}
// GetOverview 获取轮询总览
// @Summary 获取轮询总览统计
// @Description 获取轮询系统的总览统计数据,包括初始化进度、队列大小等
// @Tags 轮询管理-监控
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.PollingOverviewResp}
// @Router /api/admin/polling-stats [get]
func (h *PollingMonitoringHandler) GetOverview(c *fiber.Ctx) error {
ctx := c.UserContext()
stats, err := h.service.GetOverview(ctx)
if err != nil {
return err
}
return response.Success(c, &dto.PollingOverviewResp{
TotalCards: stats.TotalCards,
InitializedCards: stats.InitializedCards,
InitProgress: stats.InitProgress,
IsInitializing: stats.IsInitializing,
RealnameQueueSize: stats.RealnameQueueSize,
CarddataQueueSize: stats.CarddataQueueSize,
PackageQueueSize: stats.PackageQueueSize,
})
}
// GetQueueStatuses 获取队列状态
// @Summary 获取轮询队列状态
// @Description 获取所有轮询队列的详细状态,包括队列大小、到期数、等待时间等
// @Tags 轮询管理-监控
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.PollingQueueStatusListResp}
// @Router /api/admin/polling-stats/queues [get]
func (h *PollingMonitoringHandler) GetQueueStatuses(c *fiber.Ctx) error {
ctx := c.UserContext()
statuses, err := h.service.GetQueueStatuses(ctx)
if err != nil {
return err
}
items := make([]*dto.PollingQueueStatusResp, 0, len(statuses))
for _, s := range statuses {
items = append(items, &dto.PollingQueueStatusResp{
TaskType: s.TaskType,
TaskTypeName: s.TaskTypeName,
QueueSize: s.QueueSize,
ManualPending: s.ManualPending,
DueCount: s.DueCount,
AvgWaitTime: s.AvgWaitTime,
})
}
return response.Success(c, &dto.PollingQueueStatusListResp{Items: items})
}
// GetTaskStatuses 获取任务统计
// @Summary 获取轮询任务统计
// @Description 获取所有轮询任务类型的执行统计,包括成功率、平均耗时等
// @Tags 轮询管理-监控
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.PollingTaskStatsListResp}
// @Router /api/admin/polling-stats/tasks [get]
func (h *PollingMonitoringHandler) GetTaskStatuses(c *fiber.Ctx) error {
ctx := c.UserContext()
statuses, err := h.service.GetTaskStatuses(ctx)
if err != nil {
return err
}
items := make([]*dto.PollingTaskStatsResp, 0, len(statuses))
for _, s := range statuses {
items = append(items, &dto.PollingTaskStatsResp{
TaskType: s.TaskType,
TaskTypeName: s.TaskTypeName,
SuccessCount1h: s.SuccessCount1h,
FailureCount1h: s.FailureCount1h,
TotalCount1h: s.TotalCount1h,
SuccessRate: s.SuccessRate,
AvgDurationMs: s.AvgDurationMs,
})
}
return response.Success(c, &dto.PollingTaskStatsListResp{Items: items})
}
// GetInitProgress 获取初始化进度
// @Summary 获取轮询初始化进度
// @Description 获取轮询系统初始化的详细进度,包括已处理数、预计完成时间等
// @Tags 轮询管理-监控
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.Response{data=dto.PollingInitProgressResp}
// @Router /api/admin/polling-stats/init-progress [get]
func (h *PollingMonitoringHandler) GetInitProgress(c *fiber.Ctx) error {
ctx := c.UserContext()
progress, err := h.service.GetInitProgress(ctx)
if err != nil {
return err
}
return response.Success(c, &dto.PollingInitProgressResp{
TotalCards: progress.TotalCards,
InitializedCards: progress.InitializedCards,
Progress: progress.Progress,
IsComplete: progress.IsComplete,
StartedAt: progress.StartedAt,
EstimatedETA: progress.EstimatedETA,
})
}