10 changed files with 1648 additions and 2 deletions
@ -0,0 +1,248 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.Rendering; |
|||
using LTEMvcApp.Services; |
|||
using LTEMvcApp.Models; |
|||
using System.Text; |
|||
using Microsoft.AspNetCore.Http; |
|||
|
|||
namespace LTEMvcApp.Controllers |
|||
{ |
|||
/// <summary>
|
|||
/// 统计控制器
|
|||
/// </summary>
|
|||
public class StatisticsController : Controller |
|||
{ |
|||
private readonly WebSocketManagerService _webSocketManager; |
|||
private readonly ILogger<StatisticsController> _logger; |
|||
|
|||
public StatisticsController(WebSocketManagerService webSocketManager, ILogger<StatisticsController> logger) |
|||
{ |
|||
_webSocketManager = webSocketManager; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 统计页面
|
|||
/// </summary>
|
|||
public IActionResult Index() |
|||
{ |
|||
var summary = _webSocketManager.GetStatisticsSummary(); |
|||
ViewBag.Summary = summary; |
|||
return View(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有统计数据
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public IActionResult GetAllStats() |
|||
{ |
|||
try |
|||
{ |
|||
var stats = _webSocketManager.GetAllStatistics(); |
|||
return Json(new { success = true, data = stats }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取所有统计数据时出错"); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取最新统计数据
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public IActionResult GetLatestStats() |
|||
{ |
|||
try |
|||
{ |
|||
var stats = _webSocketManager.GetLatestStatistics(); |
|||
return Json(new { success = true, data = stats }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取最新统计数据时出错"); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取指定客户端的统计数据
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public IActionResult GetClientStats(string clientName) |
|||
{ |
|||
try |
|||
{ |
|||
var stats = _webSocketManager.GetClientStatistics(clientName); |
|||
if (stats == null) |
|||
{ |
|||
return Json(new { success = false, message = "客户端未找到" }); |
|||
} |
|||
return Json(new { success = true, data = stats }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取客户端统计数据时出错: {ClientName}", clientName); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取指定客户端的历史统计数据
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public IActionResult GetClientStatsHistory(string clientName) |
|||
{ |
|||
try |
|||
{ |
|||
var stats = _webSocketManager.GetClientStatisticsHistory(clientName); |
|||
return Json(new { success = true, data = stats }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取客户端历史统计数据时出错: {ClientName}", clientName); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有客户端的最新统计数据
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public IActionResult GetAllClientStats() |
|||
{ |
|||
try |
|||
{ |
|||
var stats = _webSocketManager.GetAllClientStatistics(); |
|||
return Json(new { success = true, data = stats }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取所有客户端统计数据时出错"); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 清空统计数据
|
|||
/// </summary>
|
|||
[HttpPost] |
|||
public IActionResult ClearStats() |
|||
{ |
|||
try |
|||
{ |
|||
_webSocketManager.ClearStatistics(); |
|||
return Json(new { success = true, message = "统计数据已清空" }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "清空统计数据时出错"); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取统计摘要
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public IActionResult GetSummary() |
|||
{ |
|||
try |
|||
{ |
|||
var summary = _webSocketManager.GetStatisticsSummary(); |
|||
return Json(new { success = true, data = summary }); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取统计摘要时出错"); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// SSE推送 - 所有统计数据
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public async Task SSEStats() |
|||
{ |
|||
Response.Headers.Add("Content-Type", "text/event-stream"); |
|||
Response.Headers.Add("Cache-Control", "no-cache"); |
|||
Response.Headers.Add("Connection", "keep-alive"); |
|||
Response.Headers.Add("Access-Control-Allow-Origin", "*"); |
|||
|
|||
try |
|||
{ |
|||
while (!HttpContext.RequestAborted.IsCancellationRequested) |
|||
{ |
|||
var sseData = _webSocketManager.GetStatisticsAsSSE(); |
|||
var bytes = Encoding.UTF8.GetBytes(sseData); |
|||
await Response.Body.WriteAsync(bytes, 0, bytes.Length); |
|||
await Response.Body.FlushAsync(); |
|||
|
|||
await Task.Delay(1000); // 每秒推送一次
|
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "SSE推送统计数据时出错"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// SSE推送 - 指定客户端统计数据
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public async Task SSEClientStats(string clientName) |
|||
{ |
|||
Response.Headers.Add("Content-Type", "text/event-stream"); |
|||
Response.Headers.Add("Cache-Control", "no-cache"); |
|||
Response.Headers.Add("Connection", "keep-alive"); |
|||
Response.Headers.Add("Access-Control-Allow-Origin", "*"); |
|||
|
|||
try |
|||
{ |
|||
while (!HttpContext.RequestAborted.IsCancellationRequested) |
|||
{ |
|||
var sseData = _webSocketManager.GetClientStatisticsAsSSE(clientName); |
|||
var bytes = Encoding.UTF8.GetBytes(sseData); |
|||
await Response.Body.WriteAsync(bytes, 0, bytes.Length); |
|||
await Response.Body.FlushAsync(); |
|||
|
|||
await Task.Delay(1000); // 每秒推送一次
|
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "SSE推送客户端统计数据时出错: {ClientName}", clientName); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取统计队列信息
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public IActionResult GetQueueInfo() |
|||
{ |
|||
try |
|||
{ |
|||
var queueCount = _webSocketManager.GetStatisticsQueueCount(); |
|||
var clientCount = _webSocketManager.GetStatisticsClientCount(); |
|||
|
|||
return Json(new { |
|||
success = true, |
|||
data = new { |
|||
queueCount = queueCount, |
|||
clientCount = clientCount |
|||
} |
|||
}); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取统计队列信息时出错"); |
|||
return Json(new { success = false, message = ex.Message }); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,336 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Concurrent; |
|||
using Newtonsoft.Json.Linq; |
|||
|
|||
namespace LTEMvcApp.Models |
|||
{ |
|||
/// <summary>
|
|||
/// 统计数据模型
|
|||
/// </summary>
|
|||
public class StatisticsData |
|||
{ |
|||
/// <summary>
|
|||
/// 消息类型
|
|||
/// </summary>
|
|||
public string Message { get; set; } = ""; |
|||
|
|||
/// <summary>
|
|||
/// 实例ID
|
|||
/// </summary>
|
|||
public string InstanceId { get; set; } = ""; |
|||
|
|||
/// <summary>
|
|||
/// CPU信息
|
|||
/// </summary>
|
|||
public CpuInfo Cpu { get; set; } = new CpuInfo(); |
|||
|
|||
/// <summary>
|
|||
/// 小区信息
|
|||
/// </summary>
|
|||
public Dictionary<string, CellInfo> Cells { get; set; } = new Dictionary<string, CellInfo>(); |
|||
|
|||
/// <summary>
|
|||
/// RF端口信息
|
|||
/// </summary>
|
|||
public Dictionary<string, RfPortInfo> RfPorts { get; set; } = new Dictionary<string, RfPortInfo>(); |
|||
|
|||
/// <summary>
|
|||
/// 计数器信息
|
|||
/// </summary>
|
|||
public CountersInfo Counters { get; set; } = new CountersInfo(); |
|||
|
|||
/// <summary>
|
|||
/// 持续时间
|
|||
/// </summary>
|
|||
public double Duration { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 消息ID
|
|||
/// </summary>
|
|||
public int MessageId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 时间戳
|
|||
/// </summary>
|
|||
public double Time { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 接收时间
|
|||
/// </summary>
|
|||
public DateTime ReceivedAt { get; set; } = DateTime.Now; |
|||
|
|||
/// <summary>
|
|||
/// 客户端名称
|
|||
/// </summary>
|
|||
public string ClientName { get; set; } = ""; |
|||
|
|||
/// <summary>
|
|||
/// 从JObject创建StatisticsData
|
|||
/// </summary>
|
|||
public static StatisticsData FromJObject(JObject data, string clientName) |
|||
{ |
|||
var stats = new StatisticsData |
|||
{ |
|||
Message = data["message"]?.ToString() ?? "", |
|||
InstanceId = data["instance_id"]?.ToString() ?? "", |
|||
Duration = data["duration"]?.Value<double>() ?? 0, |
|||
MessageId = data["message_id"]?.Value<int>() ?? 0, |
|||
Time = data["time"]?.Value<double>() ?? 0, |
|||
ClientName = clientName |
|||
}; |
|||
|
|||
// 解析CPU信息
|
|||
if (data["cpu"] is JObject cpuObj) |
|||
{ |
|||
stats.Cpu.Global = cpuObj["global"]?.Value<double>() ?? 0; |
|||
} |
|||
|
|||
// 解析小区信息
|
|||
if (data["cells"] is JObject cellsObj) |
|||
{ |
|||
foreach (var cell in cellsObj) |
|||
{ |
|||
if (cell.Value is JObject cellObj) |
|||
{ |
|||
var cellInfo = new CellInfo |
|||
{ |
|||
DlBitrate = cellObj["dl_bitrate"]?.Value<double>() ?? 0, |
|||
UlBitrate = cellObj["ul_bitrate"]?.Value<double>() ?? 0, |
|||
DlTx = cellObj["dl_tx"]?.Value<double>() ?? 0, |
|||
UlTx = cellObj["ul_tx"]?.Value<double>() ?? 0, |
|||
DlRetx = cellObj["dl_retx"]?.Value<double>() ?? 0, |
|||
UlRetx = cellObj["ul_retx"]?.Value<double>() ?? 0, |
|||
DlUseMin = cellObj["dl_use_min"]?.Value<double>() ?? 0, |
|||
DlUseMax = cellObj["dl_use_max"]?.Value<double>() ?? 0, |
|||
DlUseAvg = cellObj["dl_use_avg"]?.Value<double>() ?? 0, |
|||
UlUseMin = cellObj["ul_use_min"]?.Value<double>() ?? 0, |
|||
UlUseMax = cellObj["ul_use_max"]?.Value<double>() ?? 0, |
|||
UlUseAvg = cellObj["ul_use_avg"]?.Value<double>() ?? 0, |
|||
DlSchedUsersMin = cellObj["dl_sched_users_min"]?.Value<double>() ?? 0, |
|||
DlSchedUsersMax = cellObj["dl_sched_users_max"]?.Value<double>() ?? 0, |
|||
DlSchedUsersAvg = cellObj["dl_sched_users_avg"]?.Value<double>() ?? 0, |
|||
UlSchedUsersMin = cellObj["ul_sched_users_min"]?.Value<double>() ?? 0, |
|||
UlSchedUsersMax = cellObj["ul_sched_users_max"]?.Value<double>() ?? 0, |
|||
UlSchedUsersAvg = cellObj["ul_sched_users_avg"]?.Value<double>() ?? 0, |
|||
UeCountMin = cellObj["ue_count_min"]?.Value<double>() ?? 0, |
|||
UeCountMax = cellObj["ue_count_max"]?.Value<double>() ?? 0, |
|||
UeCountAvg = cellObj["ue_count_avg"]?.Value<double>() ?? 0, |
|||
ErabCountMin = cellObj["erab_count_min"]?.Value<double>() ?? 0, |
|||
ErabCountMax = cellObj["erab_count_max"]?.Value<double>() ?? 0, |
|||
ErabCountAvg = cellObj["erab_count_avg"]?.Value<double>() ?? 0, |
|||
DlGbrUseMin = cellObj["dl_gbr_use_min"]?.Value<double>() ?? 0, |
|||
DlGbrUseMax = cellObj["dl_gbr_use_max"]?.Value<double>() ?? 0, |
|||
DlGbrUseAvg = cellObj["dl_gbr_use_avg"]?.Value<double>() ?? 0, |
|||
UlGbrUseMin = cellObj["ul_gbr_use_min"]?.Value<double>() ?? 0, |
|||
UlGbrUseMax = cellObj["ul_gbr_use_max"]?.Value<double>() ?? 0, |
|||
UlGbrUseAvg = cellObj["ul_gbr_use_avg"]?.Value<double>() ?? 0 |
|||
}; |
|||
|
|||
// 解析计数器
|
|||
if (cellObj["counters"] is JObject countersObj) |
|||
{ |
|||
if (countersObj["messages"] is JObject messagesObj) |
|||
{ |
|||
foreach (var msg in messagesObj) |
|||
{ |
|||
cellInfo.Counters.Messages[msg.Key] = msg.Value?.Value<int>() ?? 0; |
|||
} |
|||
} |
|||
if (countersObj["errors"] is JObject errorsObj) |
|||
{ |
|||
foreach (var err in errorsObj) |
|||
{ |
|||
cellInfo.Counters.Errors[err.Key] = err.Value?.Value<int>() ?? 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
stats.Cells[cell.Key] = cellInfo; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 解析RF端口信息
|
|||
if (data["rf_ports"] is JObject rfPortsObj) |
|||
{ |
|||
foreach (var port in rfPortsObj) |
|||
{ |
|||
if (port.Value is JObject portObj) |
|||
{ |
|||
var rfPortInfo = new RfPortInfo(); |
|||
if (portObj["rxtx_delay"] is JObject delayObj) |
|||
{ |
|||
foreach (var delay in delayObj) |
|||
{ |
|||
rfPortInfo.RxtxDelay[delay.Key] = delay.Value?.Value<double>() ?? 0; |
|||
} |
|||
} |
|||
stats.RfPorts[port.Key] = rfPortInfo; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 解析全局计数器
|
|||
if (data["counters"] is JObject globalCountersObj) |
|||
{ |
|||
if (globalCountersObj["messages"] is JObject messagesObj) |
|||
{ |
|||
foreach (var msg in messagesObj) |
|||
{ |
|||
stats.Counters.Messages[msg.Key] = msg.Value?.Value<int>() ?? 0; |
|||
} |
|||
} |
|||
if (globalCountersObj["errors"] is JObject errorsObj) |
|||
{ |
|||
foreach (var err in errorsObj) |
|||
{ |
|||
stats.Counters.Errors[err.Key] = err.Value?.Value<int>() ?? 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return stats; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// CPU信息
|
|||
/// </summary>
|
|||
public class CpuInfo |
|||
{ |
|||
public double Global { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 小区信息
|
|||
/// </summary>
|
|||
public class CellInfo |
|||
{ |
|||
public double DlBitrate { get; set; } |
|||
public double UlBitrate { get; set; } |
|||
public double DlTx { get; set; } |
|||
public double UlTx { get; set; } |
|||
public double DlRetx { get; set; } |
|||
public double UlRetx { get; set; } |
|||
public double DlUseMin { get; set; } |
|||
public double DlUseMax { get; set; } |
|||
public double DlUseAvg { get; set; } |
|||
public double UlUseMin { get; set; } |
|||
public double UlUseMax { get; set; } |
|||
public double UlUseAvg { get; set; } |
|||
public double DlSchedUsersMin { get; set; } |
|||
public double DlSchedUsersMax { get; set; } |
|||
public double DlSchedUsersAvg { get; set; } |
|||
public double UlSchedUsersMin { get; set; } |
|||
public double UlSchedUsersMax { get; set; } |
|||
public double UlSchedUsersAvg { get; set; } |
|||
public double UeCountMin { get; set; } |
|||
public double UeCountMax { get; set; } |
|||
public double UeCountAvg { get; set; } |
|||
public double ErabCountMin { get; set; } |
|||
public double ErabCountMax { get; set; } |
|||
public double ErabCountAvg { get; set; } |
|||
public double DlGbrUseMin { get; set; } |
|||
public double DlGbrUseMax { get; set; } |
|||
public double DlGbrUseAvg { get; set; } |
|||
public double UlGbrUseMin { get; set; } |
|||
public double UlGbrUseMax { get; set; } |
|||
public double UlGbrUseAvg { get; set; } |
|||
public CountersInfo Counters { get; set; } = new CountersInfo(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// RF端口信息
|
|||
/// </summary>
|
|||
public class RfPortInfo |
|||
{ |
|||
public Dictionary<string, double> RxtxDelay { get; set; } = new Dictionary<string, double>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 计数器信息
|
|||
/// </summary>
|
|||
public class CountersInfo |
|||
{ |
|||
public Dictionary<string, int> Messages { get; set; } = new Dictionary<string, int>(); |
|||
public Dictionary<string, int> Errors { get; set; } = new Dictionary<string, int>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 统计数据队列管理器
|
|||
/// </summary>
|
|||
public class StatisticsQueueManager |
|||
{ |
|||
private readonly ConcurrentQueue<StatisticsData> _statsQueue = new ConcurrentQueue<StatisticsData>(); |
|||
private readonly int _maxQueueSize = 1000; // 最大队列大小
|
|||
private readonly object _lockObject = new object(); |
|||
|
|||
/// <summary>
|
|||
/// 添加统计数据到队列
|
|||
/// </summary>
|
|||
public void EnqueueStats(StatisticsData stats) |
|||
{ |
|||
lock (_lockObject) |
|||
{ |
|||
_statsQueue.Enqueue(stats); |
|||
|
|||
// 维持队列大小
|
|||
while (_statsQueue.Count > _maxQueueSize) |
|||
{ |
|||
_statsQueue.TryDequeue(out _); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取队列中的所有统计数据
|
|||
/// </summary>
|
|||
public List<StatisticsData> GetAllStats() |
|||
{ |
|||
lock (_lockObject) |
|||
{ |
|||
return _statsQueue.ToList(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取最新的统计数据
|
|||
/// </summary>
|
|||
public StatisticsData? GetLatestStats() |
|||
{ |
|||
lock (_lockObject) |
|||
{ |
|||
return _statsQueue.TryPeek(out var stats) ? stats : null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 清空队列
|
|||
/// </summary>
|
|||
public void Clear() |
|||
{ |
|||
lock (_lockObject) |
|||
{ |
|||
while (_statsQueue.TryDequeue(out _)) |
|||
{ |
|||
// 清空所有数据
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取队列大小
|
|||
/// </summary>
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
lock (_lockObject) |
|||
{ |
|||
return _statsQueue.Count; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,245 @@ |
|||
# LTE统计数据监控系统实现 |
|||
|
|||
## 概述 |
|||
|
|||
本系统实现了对LTE客户端统计数据的完整监控功能,包括数据队列管理、实时显示和SSE推送。 |
|||
|
|||
## 功能特性 |
|||
|
|||
### 1. 数据结构模型 |
|||
- **StatisticsData**: 主要统计数据模型 |
|||
- **CellInfo**: 小区信息模型 |
|||
- **CpuInfo**: CPU信息模型 |
|||
- **RfPortInfo**: RF端口信息模型 |
|||
- **CountersInfo**: 计数器信息模型 |
|||
|
|||
### 2. 数据队列管理 |
|||
- **StatisticsQueueManager**: 线程安全的统计数据队列 |
|||
- 支持最大队列大小限制(默认1000条) |
|||
- 自动维护队列大小,防止内存溢出 |
|||
|
|||
### 3. 统计服务 |
|||
- **StatisticsService**: 核心统计服务 |
|||
- 处理WebSocket接收的统计数据 |
|||
- 管理客户端历史记录 |
|||
- 提供SSE格式数据推送 |
|||
|
|||
### 4. WebSocket集成 |
|||
- 在**WebSocketManagerService**中集成统计服务 |
|||
- 自动处理`OnStatsReceived`事件 |
|||
- 支持多客户端统计数据管理 |
|||
|
|||
### 5. API接口 |
|||
- **StatisticsController**: 提供完整的REST API |
|||
- 支持获取所有统计数据 |
|||
- 支持获取指定客户端数据 |
|||
- 支持清空统计数据 |
|||
- 支持SSE实时推送 |
|||
|
|||
### 6. 前端界面 |
|||
- 响应式统计数据显示页面 |
|||
- 实时数据更新 |
|||
- 小区详细信息查看 |
|||
- SSE连接状态监控 |
|||
|
|||
## 数据结构说明 |
|||
|
|||
### 统计数据格式 |
|||
```json |
|||
{ |
|||
"message": "stats", |
|||
"instance_id": "685f9485", |
|||
"cpu": { |
|||
"global": 328.32038445519004 |
|||
}, |
|||
"cells": { |
|||
"1": { |
|||
"dl_bitrate": 0, |
|||
"ul_bitrate": 0, |
|||
"dl_use_avg": 0.003762019146681548, |
|||
"ul_use_avg": 0.08999999798834324, |
|||
"ue_count_avg": 0, |
|||
"erab_count_avg": 0, |
|||
"counters": { |
|||
"messages": {}, |
|||
"errors": {} |
|||
} |
|||
} |
|||
}, |
|||
"rf_ports": { |
|||
"0": { |
|||
"rxtx_delay": {} |
|||
} |
|||
}, |
|||
"counters": { |
|||
"messages": { |
|||
"s1_setup_request": 2, |
|||
"ng_setup_request": 2 |
|||
}, |
|||
"errors": {} |
|||
}, |
|||
"duration": 1.04, |
|||
"message_id": 33, |
|||
"time": 146.155 |
|||
} |
|||
``` |
|||
|
|||
## API接口说明 |
|||
|
|||
### 1. 获取所有统计数据 |
|||
``` |
|||
GET /Statistics/GetAllStats |
|||
``` |
|||
|
|||
### 2. 获取最新统计数据 |
|||
``` |
|||
GET /Statistics/GetLatestStats |
|||
``` |
|||
|
|||
### 3. 获取指定客户端统计数据 |
|||
``` |
|||
GET /Statistics/GetClientStats?clientName={clientName} |
|||
``` |
|||
|
|||
### 4. 获取客户端历史数据 |
|||
``` |
|||
GET /Statistics/GetClientStatsHistory?clientName={clientName} |
|||
``` |
|||
|
|||
### 5. 获取所有客户端最新数据 |
|||
``` |
|||
GET /Statistics/GetAllClientStats |
|||
``` |
|||
|
|||
### 6. 清空统计数据 |
|||
``` |
|||
POST /Statistics/ClearStats |
|||
``` |
|||
|
|||
### 7. 获取统计摘要 |
|||
``` |
|||
GET /Statistics/GetSummary |
|||
``` |
|||
|
|||
### 8. SSE实时推送 |
|||
``` |
|||
GET /Statistics/SSEStats |
|||
``` |
|||
|
|||
### 9. 指定客户端SSE推送 |
|||
``` |
|||
GET /Statistics/SSEClientStats?clientName={clientName} |
|||
``` |
|||
|
|||
## 使用说明 |
|||
|
|||
### 1. 启动统计监控 |
|||
1. 访问 `/Statistics/Index` 页面 |
|||
2. 点击"启动SSE"按钮开始实时监控 |
|||
3. 统计数据会自动更新显示 |
|||
|
|||
### 2. 查看小区详情 |
|||
1. 在统计表格中点击"查看小区"按钮 |
|||
2. 系统会显示该客户端的所有小区详细信息 |
|||
|
|||
### 3. 测试功能 |
|||
1. 访问 `/Home/StatisticsTest` 页面 |
|||
2. 使用各种测试按钮验证功能 |
|||
|
|||
### 4. API测试 |
|||
可以使用Postman或其他工具测试API接口: |
|||
```bash |
|||
# 获取统计摘要 |
|||
curl http://localhost:15001/Statistics/GetSummary |
|||
|
|||
# 获取所有统计数据 |
|||
curl http://localhost:15001/Statistics/GetAllStats |
|||
|
|||
# 清空统计数据 |
|||
curl -X POST http://localhost:15001/Statistics/ClearStats |
|||
``` |
|||
|
|||
## 技术实现 |
|||
|
|||
### 1. 线程安全 |
|||
- 使用`ConcurrentDictionary`和`ConcurrentQueue` |
|||
- 所有数据操作都是线程安全的 |
|||
|
|||
### 2. 内存管理 |
|||
- 自动限制队列大小 |
|||
- 定期清理历史数据 |
|||
- 防止内存泄漏 |
|||
|
|||
### 3. 实时推送 |
|||
- 使用Server-Sent Events (SSE) |
|||
- 支持多客户端同时连接 |
|||
- 自动重连机制 |
|||
|
|||
### 4. 错误处理 |
|||
- 完整的异常捕获和日志记录 |
|||
- 优雅的错误恢复机制 |
|||
|
|||
## 配置说明 |
|||
|
|||
### 1. 队列大小配置 |
|||
在`StatisticsQueueManager`中可以调整: |
|||
```csharp |
|||
private readonly int _maxQueueSize = 1000; // 最大队列大小 |
|||
``` |
|||
|
|||
### 2. 历史记录大小 |
|||
在`StatisticsService`中可以调整: |
|||
```csharp |
|||
private readonly int _maxHistorySize = 100; // 每个客户端最多保存100条历史记录 |
|||
``` |
|||
|
|||
### 3. SSE推送间隔 |
|||
在`StatisticsController`中可以调整: |
|||
```csharp |
|||
await Task.Delay(1000); // 每秒推送一次 |
|||
``` |
|||
|
|||
## 扩展功能 |
|||
|
|||
### 1. 数据持久化 |
|||
可以添加数据库存储功能,将统计数据保存到数据库。 |
|||
|
|||
### 2. 图表显示 |
|||
可以集成Chart.js等图表库,显示统计数据的趋势图。 |
|||
|
|||
### 3. 告警功能 |
|||
可以添加阈值告警功能,当某些指标超过阈值时发送通知。 |
|||
|
|||
### 4. 数据导出 |
|||
可以添加数据导出功能,支持CSV、Excel等格式。 |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **性能考虑**: 大量数据时注意内存使用 |
|||
2. **网络带宽**: SSE推送会占用一定带宽 |
|||
3. **浏览器兼容性**: 确保浏览器支持SSE |
|||
4. **数据准确性**: 统计数据可能有延迟,注意时间同步 |
|||
|
|||
## 故障排除 |
|||
|
|||
### 1. SSE连接失败 |
|||
- 检查浏览器是否支持SSE |
|||
- 检查网络连接 |
|||
- 查看服务器日志 |
|||
|
|||
### 2. 数据不更新 |
|||
- 检查WebSocket连接状态 |
|||
- 确认客户端正在发送统计数据 |
|||
- 查看浏览器控制台错误 |
|||
|
|||
### 3. 内存使用过高 |
|||
- 调整队列大小限制 |
|||
- 检查是否有内存泄漏 |
|||
- 重启应用程序 |
|||
|
|||
## 更新日志 |
|||
|
|||
- **v1.0**: 初始版本,实现基本统计功能 |
|||
- 支持多客户端数据管理 |
|||
- 实现SSE实时推送 |
|||
- 添加完整的API接口 |
@ -0,0 +1,230 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using LTEMvcApp.Models; |
|||
using Microsoft.Extensions.Logging; |
|||
using Newtonsoft.Json.Linq; |
|||
using System.Text; |
|||
|
|||
namespace LTEMvcApp.Services |
|||
{ |
|||
/// <summary>
|
|||
/// 统计服务 - 管理统计数据的队列和SSE推送
|
|||
/// </summary>
|
|||
public class StatisticsService |
|||
{ |
|||
private readonly ILogger<StatisticsService> _logger; |
|||
private readonly StatisticsQueueManager _statsQueue; |
|||
private readonly ConcurrentDictionary<string, StatisticsData> _latestStatsByClient; |
|||
private readonly ConcurrentDictionary<string, List<StatisticsData>> _statsHistoryByClient; |
|||
private readonly int _maxHistorySize = 100; // 每个客户端最多保存100条历史记录
|
|||
|
|||
/// <summary>
|
|||
/// 统计数据更新事件
|
|||
/// </summary>
|
|||
public event EventHandler<StatisticsData>? StatsUpdated; |
|||
|
|||
/// <summary>
|
|||
/// 构造函数
|
|||
/// </summary>
|
|||
public StatisticsService(ILogger<StatisticsService> logger) |
|||
{ |
|||
_logger = logger; |
|||
_statsQueue = new StatisticsQueueManager(); |
|||
_latestStatsByClient = new ConcurrentDictionary<string, StatisticsData>(); |
|||
_statsHistoryByClient = new ConcurrentDictionary<string, List<StatisticsData>>(); |
|||
|
|||
_logger.LogInformation("StatisticsService 初始化完成"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理接收到的统计数据
|
|||
/// </summary>
|
|||
public void ProcessStatsData(JObject data, string clientName) |
|||
{ |
|||
try |
|||
{ |
|||
var statsData = StatisticsData.FromJObject(data, clientName); |
|||
|
|||
// 添加到队列
|
|||
_statsQueue.EnqueueStats(statsData); |
|||
|
|||
// 更新最新统计数据
|
|||
_latestStatsByClient[clientName] = statsData; |
|||
|
|||
// 添加到历史记录
|
|||
AddToHistory(clientName, statsData); |
|||
|
|||
// 触发事件
|
|||
StatsUpdated?.Invoke(this, statsData); |
|||
|
|||
_logger.LogDebug($"处理统计数据: 客户端 {clientName}, 消息ID {statsData.MessageId}, 小区数量 {statsData.Cells.Count}"); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, $"处理统计数据时出错: 客户端 {clientName}"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 添加统计数据到历史记录
|
|||
/// </summary>
|
|||
private void AddToHistory(string clientName, StatisticsData statsData) |
|||
{ |
|||
if (!_statsHistoryByClient.TryGetValue(clientName, out var history)) |
|||
{ |
|||
history = new List<StatisticsData>(); |
|||
_statsHistoryByClient[clientName] = history; |
|||
} |
|||
|
|||
lock (history) |
|||
{ |
|||
history.Add(statsData); |
|||
|
|||
// 维持历史记录大小
|
|||
while (history.Count > _maxHistorySize) |
|||
{ |
|||
history.RemoveAt(0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有统计数据
|
|||
/// </summary>
|
|||
public List<StatisticsData> GetAllStats() |
|||
{ |
|||
return _statsQueue.GetAllStats(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取最新的统计数据
|
|||
/// </summary>
|
|||
public StatisticsData? GetLatestStats() |
|||
{ |
|||
return _statsQueue.GetLatestStats(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取指定客户端的最新统计数据
|
|||
/// </summary>
|
|||
public StatisticsData? GetLatestStatsByClient(string clientName) |
|||
{ |
|||
_latestStatsByClient.TryGetValue(clientName, out var stats); |
|||
return stats; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取指定客户端的历史统计数据
|
|||
/// </summary>
|
|||
public List<StatisticsData> GetStatsHistoryByClient(string clientName) |
|||
{ |
|||
_statsHistoryByClient.TryGetValue(clientName, out var history); |
|||
return history ?? new List<StatisticsData>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有客户端的最新统计数据
|
|||
/// </summary>
|
|||
public Dictionary<string, StatisticsData> GetAllLatestStats() |
|||
{ |
|||
return _latestStatsByClient.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 清空统计数据
|
|||
/// </summary>
|
|||
public void ClearStats() |
|||
{ |
|||
_statsQueue.Clear(); |
|||
_latestStatsByClient.Clear(); |
|||
_statsHistoryByClient.Clear(); |
|||
_logger.LogInformation("统计数据已清空"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取统计摘要信息
|
|||
/// </summary>
|
|||
public object GetStatsSummary() |
|||
{ |
|||
var summary = new |
|||
{ |
|||
TotalStatsCount = _statsQueue.Count, |
|||
ClientCount = _latestStatsByClient.Count, |
|||
Clients = _latestStatsByClient.Keys.ToList(), |
|||
LastUpdateTime = _latestStatsByClient.Values |
|||
.OrderByDescending(s => s.ReceivedAt) |
|||
.FirstOrDefault()?.ReceivedAt |
|||
}; |
|||
|
|||
return summary; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取SSE格式的统计数据
|
|||
/// </summary>
|
|||
public string GetStatsAsSSE() |
|||
{ |
|||
var latestStats = GetAllLatestStats(); |
|||
var sseData = new |
|||
{ |
|||
type = "stats_update", |
|||
timestamp = DateTime.UtcNow, |
|||
data = latestStats |
|||
}; |
|||
|
|||
var json = System.Text.Json.JsonSerializer.Serialize(sseData, new System.Text.Json.JsonSerializerOptions |
|||
{ |
|||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase |
|||
}); |
|||
|
|||
return $"data: {json}\n\n"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取指定客户端的SSE格式统计数据
|
|||
/// </summary>
|
|||
public string GetClientStatsAsSSE(string clientName) |
|||
{ |
|||
var stats = GetLatestStatsByClient(clientName); |
|||
if (stats == null) |
|||
{ |
|||
return $"data: {System.Text.Json.JsonSerializer.Serialize(new { type = "error", message = "Client not found" })}\n\n"; |
|||
} |
|||
|
|||
var sseData = new |
|||
{ |
|||
type = "client_stats_update", |
|||
clientName = clientName, |
|||
timestamp = DateTime.UtcNow, |
|||
data = stats |
|||
}; |
|||
|
|||
var json = System.Text.Json.JsonSerializer.Serialize(sseData, new System.Text.Json.JsonSerializerOptions |
|||
{ |
|||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase |
|||
}); |
|||
|
|||
return $"data: {json}\n\n"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取队列大小
|
|||
/// </summary>
|
|||
public int GetQueueCount() |
|||
{ |
|||
return _statsQueue.Count; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取客户端数量
|
|||
/// </summary>
|
|||
public int GetClientCount() |
|||
{ |
|||
return _latestStatsByClient.Count; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,122 @@ |
|||
@{ |
|||
ViewData["Title"] = "统计测试"; |
|||
} |
|||
|
|||
<div class="container-fluid"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h3 class="card-title">统计功能测试</h3> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<h5>API测试</h5> |
|||
<button class="btn btn-primary mb-2" onclick="testGetAllStats()">获取所有统计数据</button> |
|||
<button class="btn btn-info mb-2" onclick="testGetLatestStats()">获取最新统计数据</button> |
|||
<button class="btn btn-success mb-2" onclick="testGetSummary()">获取统计摘要</button> |
|||
<button class="btn btn-warning mb-2" onclick="testClearStats()">清空统计数据</button> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<h5>SSE测试</h5> |
|||
<button class="btn btn-primary mb-2" onclick="testSSE()">测试SSE连接</button> |
|||
<button class="btn btn-danger mb-2" onclick="stopSSE()">停止SSE</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row mt-3"> |
|||
<div class="col-12"> |
|||
<h5>测试结果</h5> |
|||
<pre id="testResult" style="background-color: #f8f9fa; padding: 10px; border-radius: 5px; max-height: 400px; overflow-y: auto;"></pre> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
@section Scripts { |
|||
<script> |
|||
let sseConnection = null; |
|||
|
|||
function logResult(message) { |
|||
const result = document.getElementById('testResult'); |
|||
const timestamp = new Date().toLocaleTimeString(); |
|||
result.textContent += `[${timestamp}] ${message}\n`; |
|||
result.scrollTop = result.scrollHeight; |
|||
} |
|||
|
|||
function testGetAllStats() { |
|||
logResult('测试获取所有统计数据...'); |
|||
$.get('/Statistics/GetAllStats', function(response) { |
|||
logResult('响应: ' + JSON.stringify(response, null, 2)); |
|||
}).fail(function(xhr, status, error) { |
|||
logResult('错误: ' + error); |
|||
}); |
|||
} |
|||
|
|||
function testGetLatestStats() { |
|||
logResult('测试获取最新统计数据...'); |
|||
$.get('/Statistics/GetLatestStats', function(response) { |
|||
logResult('响应: ' + JSON.stringify(response, null, 2)); |
|||
}).fail(function(xhr, status, error) { |
|||
logResult('错误: ' + error); |
|||
}); |
|||
} |
|||
|
|||
function testGetSummary() { |
|||
logResult('测试获取统计摘要...'); |
|||
$.get('/Statistics/GetSummary', function(response) { |
|||
logResult('响应: ' + JSON.stringify(response, null, 2)); |
|||
}).fail(function(xhr, status, error) { |
|||
logResult('错误: ' + error); |
|||
}); |
|||
} |
|||
|
|||
function testClearStats() { |
|||
logResult('测试清空统计数据...'); |
|||
$.post('/Statistics/ClearStats', function(response) { |
|||
logResult('响应: ' + JSON.stringify(response, null, 2)); |
|||
}).fail(function(xhr, status, error) { |
|||
logResult('错误: ' + error); |
|||
}); |
|||
} |
|||
|
|||
function testSSE() { |
|||
logResult('测试SSE连接...'); |
|||
if (sseConnection) { |
|||
sseConnection.close(); |
|||
} |
|||
|
|||
sseConnection = new EventSource('/Statistics/SSEStats'); |
|||
|
|||
sseConnection.onopen = function(event) { |
|||
logResult('SSE连接已建立'); |
|||
}; |
|||
|
|||
sseConnection.onmessage = function(event) { |
|||
logResult('SSE消息: ' + event.data); |
|||
}; |
|||
|
|||
sseConnection.onerror = function(event) { |
|||
logResult('SSE错误: ' + JSON.stringify(event)); |
|||
}; |
|||
} |
|||
|
|||
function stopSSE() { |
|||
logResult('停止SSE连接...'); |
|||
if (sseConnection) { |
|||
sseConnection.close(); |
|||
sseConnection = null; |
|||
logResult('SSE连接已关闭'); |
|||
} |
|||
} |
|||
|
|||
// 页面加载时清空结果 |
|||
$(document).ready(function() { |
|||
document.getElementById('testResult').textContent = ''; |
|||
}); |
|||
</script> |
|||
} |
@ -0,0 +1,344 @@ |
|||
@{ |
|||
ViewData["Title"] = "统计数据"; |
|||
} |
|||
|
|||
<div class="container-fluid"> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h3 class="card-title">LTE统计数据监控</h3> |
|||
<div class="card-tools"> |
|||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshStats()"> |
|||
<i class="fas fa-sync-alt"></i> 刷新 |
|||
</button> |
|||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="clearStats()"> |
|||
<i class="fas fa-trash"></i> 清空 |
|||
</button> |
|||
<button type="button" class="btn btn-sm btn-outline-info" onclick="toggleSSE()"> |
|||
<i class="fas fa-broadcast-tower"></i> <span id="sseToggleText">启动SSE</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<!-- 统计摘要 --> |
|||
<div class="row mb-3"> |
|||
<div class="col-md-3"> |
|||
<div class="info-box"> |
|||
<span class="info-box-icon bg-info"><i class="fas fa-server"></i></span> |
|||
<div class="info-box-content"> |
|||
<span class="info-box-text">客户端数量</span> |
|||
<span class="info-box-number" id="clientCount">0</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3"> |
|||
<div class="info-box"> |
|||
<span class="info-box-icon bg-success"><i class="fas fa-database"></i></span> |
|||
<div class="info-box-content"> |
|||
<span class="info-box-text">队列大小</span> |
|||
<span class="info-box-number" id="queueCount">0</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3"> |
|||
<div class="info-box"> |
|||
<span class="info-box-icon bg-warning"><i class="fas fa-clock"></i></span> |
|||
<div class="info-box-content"> |
|||
<span class="info-box-text">最后更新</span> |
|||
<span class="info-box-number" id="lastUpdate">-</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3"> |
|||
<div class="info-box"> |
|||
<span class="info-box-icon bg-primary"><i class="fas fa-signal"></i></span> |
|||
<div class="info-box-content"> |
|||
<span class="info-box-text">SSE状态</span> |
|||
<span class="info-box-number" id="sseStatus">未连接</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 客户端选择 --> |
|||
<div class="row mb-3"> |
|||
<div class="col-md-6"> |
|||
<div class="form-group"> |
|||
<label for="clientSelect">选择客户端:</label> |
|||
<select class="form-control" id="clientSelect" onchange="loadClientStats()"> |
|||
<option value="">所有客户端</option> |
|||
</select> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<div class="form-group"> |
|||
<label for="refreshInterval">刷新间隔 (秒):</label> |
|||
<input type="number" class="form-control" id="refreshInterval" value="5" min="1" max="60" onchange="updateRefreshInterval()"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 统计数据表格 --> |
|||
<div class="table-responsive"> |
|||
<table class="table table-bordered table-striped" id="statsTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>客户端</th> |
|||
<th>实例ID</th> |
|||
<th>CPU使用率</th> |
|||
<th>小区数量</th> |
|||
<th>RF端口</th> |
|||
<th>消息ID</th> |
|||
<th>持续时间</th> |
|||
<th>接收时间</th> |
|||
<th>操作</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="statsTableBody"> |
|||
<!-- 数据将通过JavaScript动态填充 --> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
<!-- 小区详细信息 --> |
|||
<div class="row mt-4" id="cellDetails" style="display: none;"> |
|||
<div class="col-12"> |
|||
<div class="card"> |
|||
<div class="card-header"> |
|||
<h4 class="card-title">小区详细信息 - <span id="selectedClientName"></span></h4> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-bordered table-striped" id="cellTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>小区ID</th> |
|||
<th>下行比特率</th> |
|||
<th>上行比特率</th> |
|||
<th>下行使用率(平均)</th> |
|||
<th>上行使用率(平均)</th> |
|||
<th>UE数量(平均)</th> |
|||
<th>ERAB数量(平均)</th> |
|||
<th>调度用户(下行)</th> |
|||
<th>调度用户(上行)</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="cellTableBody"> |
|||
<!-- 小区数据将通过JavaScript动态填充 --> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
@section Scripts { |
|||
<script> |
|||
let sseConnection = null; |
|||
let refreshTimer = null; |
|||
let currentStats = {}; |
|||
|
|||
// 页面加载完成后初始化 |
|||
$(document).ready(function() { |
|||
loadSummary(); |
|||
loadAllStats(); |
|||
startRefreshTimer(); |
|||
}); |
|||
|
|||
// 加载统计摘要 |
|||
function loadSummary() { |
|||
$.get('/Statistics/GetSummary', function(response) { |
|||
if (response.success) { |
|||
$('#clientCount').text(response.data.clientCount || 0); |
|||
$('#queueCount').text(response.data.totalStatsCount || 0); |
|||
if (response.data.lastUpdateTime) { |
|||
$('#lastUpdate').text(new Date(response.data.lastUpdateTime).toLocaleString()); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// 加载所有统计数据 |
|||
function loadAllStats() { |
|||
$.get('/Statistics/GetAllClientStats', function(response) { |
|||
if (response.success) { |
|||
currentStats = response.data; |
|||
updateStatsTable(); |
|||
updateClientSelect(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// 更新统计表格 |
|||
function updateStatsTable() { |
|||
const tbody = $('#statsTableBody'); |
|||
tbody.empty(); |
|||
|
|||
Object.keys(currentStats).forEach(clientName => { |
|||
const stats = currentStats[clientName]; |
|||
const row = ` |
|||
<tr> |
|||
<td>${clientName}</td> |
|||
<td>${stats.instanceId || '-'}</td> |
|||
<td>${(stats.cpu?.global || 0).toFixed(2)}%</td> |
|||
<td>${Object.keys(stats.cells || {}).length}</td> |
|||
<td>${Object.keys(stats.rfPorts || {}).length}</td> |
|||
<td>${stats.messageId || '-'}</td> |
|||
<td>${(stats.duration || 0).toFixed(2)}s</td> |
|||
<td>${new Date(stats.receivedAt).toLocaleString()}</td> |
|||
<td> |
|||
<button class="btn btn-sm btn-info" onclick="showCellDetails('${clientName}')"> |
|||
<i class="fas fa-eye"></i> 查看小区 |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
`; |
|||
tbody.append(row); |
|||
}); |
|||
} |
|||
|
|||
// 更新客户端选择下拉框 |
|||
function updateClientSelect() { |
|||
const select = $('#clientSelect'); |
|||
select.find('option:not(:first)').remove(); |
|||
|
|||
Object.keys(currentStats).forEach(clientName => { |
|||
select.append(`<option value="${clientName}">${clientName}</option>`); |
|||
}); |
|||
} |
|||
|
|||
// 显示小区详细信息 |
|||
function showCellDetails(clientName) { |
|||
const stats = currentStats[clientName]; |
|||
if (!stats || !stats.cells) return; |
|||
|
|||
$('#selectedClientName').text(clientName); |
|||
const tbody = $('#cellTableBody'); |
|||
tbody.empty(); |
|||
|
|||
Object.keys(stats.cells).forEach(cellId => { |
|||
const cell = stats.cells[cellId]; |
|||
const row = ` |
|||
<tr> |
|||
<td>${cellId}</td> |
|||
<td>${(cell.dlBitrate || 0).toFixed(2)}</td> |
|||
<td>${(cell.ulBitrate || 0).toFixed(2)}</td> |
|||
<td>${(cell.dlUseAvg || 0).toFixed(2)}%</td> |
|||
<td>${(cell.ulUseAvg || 0).toFixed(2)}%</td> |
|||
<td>${(cell.ueCountAvg || 0).toFixed(0)}</td> |
|||
<td>${(cell.erabCountAvg || 0).toFixed(0)}</td> |
|||
<td>${(cell.dlSchedUsersAvg || 0).toFixed(0)}</td> |
|||
<td>${(cell.ulSchedUsersAvg || 0).toFixed(0)}</td> |
|||
</tr> |
|||
`; |
|||
tbody.append(row); |
|||
}); |
|||
|
|||
$('#cellDetails').show(); |
|||
} |
|||
|
|||
// 加载指定客户端的统计数据 |
|||
function loadClientStats() { |
|||
const clientName = $('#clientSelect').val(); |
|||
if (!clientName) { |
|||
loadAllStats(); |
|||
return; |
|||
} |
|||
|
|||
$.get(`/Statistics/GetClientStats?clientName=${encodeURIComponent(clientName)}`, function(response) { |
|||
if (response.success) { |
|||
currentStats = { [clientName]: response.data }; |
|||
updateStatsTable(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// 刷新统计数据 |
|||
function refreshStats() { |
|||
loadSummary(); |
|||
loadAllStats(); |
|||
} |
|||
|
|||
// 清空统计数据 |
|||
function clearStats() { |
|||
if (confirm('确定要清空所有统计数据吗?')) { |
|||
$.post('/Statistics/ClearStats', function(response) { |
|||
if (response.success) { |
|||
alert('统计数据已清空'); |
|||
refreshStats(); |
|||
} else { |
|||
alert('清空失败: ' + response.message); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// 切换SSE连接 |
|||
function toggleSSE() { |
|||
if (sseConnection) { |
|||
sseConnection.close(); |
|||
sseConnection = null; |
|||
$('#sseToggleText').text('启动SSE'); |
|||
$('#sseStatus').text('未连接').removeClass('text-success').addClass('text-danger'); |
|||
} else { |
|||
startSSE(); |
|||
$('#sseToggleText').text('停止SSE'); |
|||
$('#sseStatus').text('已连接').removeClass('text-danger').addClass('text-success'); |
|||
} |
|||
} |
|||
|
|||
// 启动SSE连接 |
|||
function startSSE() { |
|||
sseConnection = new EventSource('/Statistics/SSEStats'); |
|||
|
|||
sseConnection.onmessage = function(event) { |
|||
try { |
|||
const data = JSON.parse(event.data); |
|||
if (data.type === 'stats_update') { |
|||
currentStats = data.data; |
|||
updateStatsTable(); |
|||
loadSummary(); |
|||
} |
|||
} catch (e) { |
|||
console.error('解析SSE数据失败:', e); |
|||
} |
|||
}; |
|||
|
|||
sseConnection.onerror = function(event) { |
|||
console.error('SSE连接错误:', event); |
|||
$('#sseStatus').text('连接错误').removeClass('text-success').addClass('text-danger'); |
|||
}; |
|||
} |
|||
|
|||
// 启动刷新定时器 |
|||
function startRefreshTimer() { |
|||
const interval = $('#refreshInterval').val() * 1000; |
|||
if (refreshTimer) { |
|||
clearInterval(refreshTimer); |
|||
} |
|||
refreshTimer = setInterval(refreshStats, interval); |
|||
} |
|||
|
|||
// 更新刷新间隔 |
|||
function updateRefreshInterval() { |
|||
startRefreshTimer(); |
|||
} |
|||
|
|||
// 页面卸载时清理资源 |
|||
$(window).on('beforeunload', function() { |
|||
if (sseConnection) { |
|||
sseConnection.close(); |
|||
} |
|||
if (refreshTimer) { |
|||
clearInterval(refreshTimer); |
|||
} |
|||
}); |
|||
</script> |
|||
} |
Loading…
Reference in new issue