Browse Source

feat: 全面重构为强类型模型,提升代码健壮性

- 新增 ClientLogsConfig 类,封装日志配置
- 重构 ClientConfig 类,使用强类型替代弱类型字典
- 更新 LTEClientWebSocket 服务,简化配置处理逻辑
- 修复 WebSocketManagerService 中的类型安全问题
- 更新控制器和视图,适配新的强类型模型
- 消除所有 JsonElement 和 Dictionary<string,object> 的混乱使用
- 提升代码可读性、可维护性和类型安全性
feature/strong-typing-refactor
root 2 months ago
parent
commit
b04d17c01d
  1. 24
      LTEMvcApp/Controllers/HomeController.cs
  2. 15
      LTEMvcApp/Controllers/WebSocketController.cs
  3. 99
      LTEMvcApp/Models/LTEClient.cs
  4. 145
      LTEMvcApp/Services/LTEClientWebSocket.cs
  5. 40
      LTEMvcApp/Services/WebSocketManagerService.cs
  6. 8
      LTEMvcApp/Views/Home/Index.cshtml
  7. 32
      LTEMvcApp/Views/Home/TestClientConfig.cshtml

24
LTEMvcApp/Controllers/HomeController.cs

@ -77,27 +77,15 @@ public class HomeController : Controller
Enabled = true, Enabled = true,
ReconnectDelay = 5000, ReconnectDelay = 5000,
Password = "test123", Password = "test123",
Logs = new Dictionary<string, object> Logs = new ClientLogsConfig
{ {
["layers"] = new Dictionary<string, object> Layers = new Dictionary<string, LogLayerConfig>
{ {
["PHY"] = new Dictionary<string, object> ["PHY"] = new LogLayerConfig { Level = "debug", Filter = "debug", MaxSize = 1000, Payload = false },
{ ["RRC"] = new LogLayerConfig { Level = "debug", Filter = "debug", MaxSize = 1000, Payload = false }
["level"] = "debug",
["filter"] = "debug",
["max_size"] = 1000,
["payload"] = false
},
["RRC"] = new Dictionary<string, object>
{
["level"] = "debug",
["filter"] = "debug",
["max_size"] = 1000,
["payload"] = false
}
}, },
["signal"] = true, Signal = true,
["cch"] = true Cch = true
} }
}; };

15
LTEMvcApp/Controllers/WebSocketController.cs

@ -170,16 +170,19 @@ namespace LTEMvcApp.Controllers
/// 设置客户端日志配置 /// 设置客户端日志配置
/// </summary> /// </summary>
/// <param name="clientName">客户端名称</param> /// <param name="clientName">客户端名称</param>
/// <param name="request">日志配置请求</param> /// <param name="request">请求体</param>
/// <returns>操作结果</returns>
[HttpPost("clients/{clientName}/logs-config")] [HttpPost("clients/{clientName}/logs-config")]
public ActionResult SetClientLogsConfig(string clientName, [FromBody] LogsConfigRequest request) public ActionResult SetClientLogsConfig(string clientName, [FromBody] ClientLogsConfig request)
{ {
var success = _webSocketManager.SetClientLogsConfig(clientName, request.Config, request.Save); var success = _webSocketManager.SetClientLogsConfig(clientName, request);
if (success) if (success)
return Ok(new { message = $"客户端 '{clientName}' 日志配置已更新" }); {
return Ok(new { message = "日志配置已更新" });
}
else else
return BadRequest($"更新客户端 '{clientName}' 日志配置失败"); {
return NotFound(new { message = $"客户端 '{clientName}' 未找到或更新失败" });
}
} }
/// <summary> /// <summary>

99
LTEMvcApp/Models/LTEClient.cs

@ -1,5 +1,6 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Text.Json.Serialization;
namespace LTEMvcApp.Models; namespace LTEMvcApp.Models;
@ -45,6 +46,11 @@ public class LTEClient
/// </summary> /// </summary>
public string? Model { get; set; } public string? Model { get; set; }
/// <summary>
/// 是否有信号记录
/// </summary>
public bool HasSignalRecord { get; set; }
#endregion #endregion
#region 日志相关属性 #region 日志相关属性
@ -147,11 +153,6 @@ public class LTEClient
/// </summary> /// </summary>
public bool HasRb { get; set; } public bool HasRb { get; set; }
/// <summary>
/// 是否有信号记录
/// </summary>
public bool HasSignalRecord { get; set; }
#endregion #endregion
#region 参数和组件 #region 参数和组件
@ -366,24 +367,100 @@ public class LTEClient
#endregion #endregion
} }
/// <summary>
/// 客户端日志配置
/// </summary>
public class ClientLogsConfig
{
/// <summary>
/// 日志层配置
/// </summary>
public Dictionary<string, LogLayerConfig> Layers { get; set; } = new();
/// <summary>
/// 是否启用信号日志
/// </summary>
public bool? Signal { get; set; }
/// <summary>
/// 是否启用控制信道日志
/// </summary>
public bool? Cch { get; set; }
// 允许其他未明确定义的属性
[JsonExtensionData]
public Dictionary<string, object>? ExtensionData { get; set; }
}
/// <summary> /// <summary>
/// 客户端配置 /// 客户端配置
/// </summary> /// </summary>
public class ClientConfig public class ClientConfig
{ {
/// <summary>
/// 客户端名称
/// </summary>
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public bool Enabled { get; set; } = true;
public string? Model { get; set; } /// <summary>
public string? Address { get; set; } /// 服务器地址
/// </summary>
public string Address { get; set; } = string.Empty;
/// <summary>
/// 是否启用
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; } = string.Empty;
/// <summary>
/// 重连延迟(毫秒)
/// </summary>
public int ReconnectDelay { get; set; } = 5000;
/// <summary>
/// 是否启用SSL
/// </summary>
public bool Ssl { get; set; } public bool Ssl { get; set; }
public int ReconnectDelay { get; set; } = 15000;
/// <summary>
/// 日志配置
/// </summary>
public ClientLogsConfig Logs { get; set; } = new();
/// <summary>
/// 是否暂停
/// </summary>
public bool Pause { get; set; } public bool Pause { get; set; }
/// <summary>
/// 是否只读
/// </summary>
public bool Readonly { get; set; } public bool Readonly { get; set; }
/// <summary>
/// 是否跳过日志菜单
/// </summary>
public bool SkipLogMenu { get; set; } public bool SkipLogMenu { get; set; }
/// <summary>
/// 是否锁定
/// </summary>
public bool Locked { get; set; } public bool Locked { get; set; }
/// <summary>
/// 是否激活
/// </summary>
public bool Active { get; set; } public bool Active { get; set; }
public string? Password { get; set; }
public Dictionary<string, object> Logs { get; set; } = new(); /// <summary>
/// 模型
/// </summary>
public string? Model { get; set; }
} }
/// <summary> /// <summary>

145
LTEMvcApp/Services/LTEClientWebSocket.cs

@ -10,6 +10,7 @@ using WebSocket4Net;
using LTEMvcApp.Models; using LTEMvcApp.Models;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace LTEMvcApp.Services namespace LTEMvcApp.Services
{ {
@ -233,66 +234,35 @@ namespace LTEMvcApp.Services
/// <summary> /// <summary>
/// 设置日志配置 /// 设置日志配置
/// </summary> /// </summary>
/// <param name="config">日志配置</param> public void SetLogsConfig(ClientLogsConfig logsConfig, bool save = false)
/// <param name="save">是否保存</param>
public void SetLogsConfig(Dictionary<string, object> config, bool save = false)
{ {
// 重新格式化配置 var logsPayload = new JObject();
var logs = new JObject(); var layersPayload = new JObject();
foreach (var kvp in config)
foreach (var layerKvp in logsConfig.Layers)
{ {
switch (kvp.Key) layersPayload[layerKvp.Key] = JObject.FromObject(new
{ {
case "layers": level = layerKvp.Value.Level,
var layers = new JObject(); maxSize = layerKvp.Value.MaxSize,
var layersDict = kvp.Value as Dictionary<string, object>; payload = layerKvp.Value.Payload,
if (layersDict != null) filter = layerKvp.Value.Filter
{ });
foreach (var layer in layersDict)
{
var layerConfig = layer.Value as Dictionary<string, object>;
if (layerConfig != null)
{
layers[layer.Key] = new JObject
{
["level"] = layerConfig.GetValueOrDefault("level", "warn").ToString(),
["max_size"] = Convert.ToInt32(layerConfig.GetValueOrDefault("max_size", 0)),
["payload"] = Convert.ToBoolean(layerConfig.GetValueOrDefault("payload", false))
};
}
}
}
logs["layers"] = layers;
break;
default:
if (IsLogParameter(kvp.Key))
{
logs[kvp.Key] = JToken.FromObject(kvp.Value);
}
break;
}
} }
logsPayload["layers"] = layersPayload;
if (logsConfig.Signal.HasValue) logsPayload["signal"] = logsConfig.Signal.Value;
if (logsConfig.Cch.HasValue) logsPayload["cch"] = logsConfig.Cch.Value;
SendMessage(new JObject SendMessage(new JObject
{ {
["message"] = "config_set", ["message"] = "config_set",
["logs"] = logs ["logs"] = logsPayload
}, response => }, response =>
{ {
if (save) if (save)
{ {
// 保存配置 _config.Logs = logsConfig;
foreach (var kvp in config)
{
if (_config.Logs.ContainsKey(kvp.Key))
{
_config.Logs[kvp.Key] = kvp.Value;
}
else
{
_config.Logs.Add(kvp.Key, kvp.Value);
}
}
} }
LogGet(new Dictionary<string, object> { ["timeout"] = 0 }); LogGet(new Dictionary<string, object> { ["timeout"] = 0 });
}, true); }, true);
@ -392,36 +362,15 @@ namespace LTEMvcApp.Services
/// <param name="parameters">参数</param> /// <param name="parameters">参数</param>
public void LogGet(Dictionary<string, object>? parameters = null) public void LogGet(Dictionary<string, object>? parameters = null)
{ {
var config = _config; if (_config.Pause)
if (config.Pause)
return; return;
var layers = new JObject(); var layers = new JObject();
if (config.Logs.ContainsKey("layers")) if (_config.Logs?.Layers != null)
{ {
// 处理layers配置,确保键是字符串类型 foreach (var layerKvp in _config.Logs.Layers)
if (config.Logs["layers"] is JObject layersJObject)
{
foreach (var layer in layersJObject)
{
var layerName = layer.Key; // 这里layer.Key已经是字符串
var layerConfig = layer.Value as JObject;
if (layerConfig != null && layerConfig.ContainsKey("filter"))
{
layers[layerName] = layerConfig["filter"].ToString();
}
}
}
else if (config.Logs["layers"] is Dictionary<string, object> layersDict)
{ {
foreach (var layer in layersDict) layers[layerKvp.Key] = layerKvp.Value.Filter;
{
var layerConfig = layer.Value as Dictionary<string, object>;
if (layerConfig != null && layerConfig.ContainsKey("filter"))
{
layers[layer.Key] = layerConfig["filter"].ToString();
}
}
} }
} }
@ -565,7 +514,7 @@ namespace LTEMvcApp.Services
_messageFifo.Clear(); _messageFifo.Clear();
// 检查当前配置 // 检查当前配置
var firstCon = !_config.Logs.ContainsKey("layers"); var firstCon = _config.Logs.Layers.Count == 0;
// 获取配置 // 获取配置
SendMessage(new JObject { ["message"] = "config_get" }, config => SendMessage(new JObject { ["message"] = "config_get" }, config =>
@ -585,54 +534,16 @@ namespace LTEMvcApp.Services
} }
var ro = _config.Readonly; var ro = _config.Readonly;
if (ro || firstCon) var serverLogsConfig = config["logs"]?.ToObject<ClientLogsConfig>(Newtonsoft.Json.JsonSerializer.CreateDefault(new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
{
_config.Logs = config["logs"]?.ToObject<Dictionary<string, object>>() ?? new Dictionary<string, object>();
}
else
{
var serverLogs = config["logs"]?.ToObject<Dictionary<string, object>>() ?? new Dictionary<string, object>();
foreach (var kvp in serverLogs)
{
if (!_config.Logs.ContainsKey(kvp.Key))
{
_config.Logs[kvp.Key] = kvp.Value;
}
}
}
// 清理和设置层配置 if (ro || firstCon)
if (_config.Logs.ContainsKey("layers"))
{ {
var layers = _config.Logs["layers"] as Dictionary<string, object>; if (serverLogsConfig != null)
var configLayers = config["logs"]?["layers"]?.ToObject<Dictionary<string, object>>();
if (layers != null && configLayers != null)
{ {
var keysToRemove = layers.Keys.Where(k => !configLayers.ContainsKey(k)).ToList(); _config.Logs = serverLogsConfig;
foreach (var key in keysToRemove)
{
layers.Remove(key);
}
foreach (var layer in layers)
{
var layerConfig = layer.Value as Dictionary<string, object>;
if (layerConfig != null && !layerConfig.ContainsKey("filter"))
{
if (ro)
{
layerConfig["filter"] = layerConfig.GetValueOrDefault("level", "warn").ToString();
}
else
{
layerConfig["filter"] = GetDefaultFilter(layer.Key);
}
}
}
} }
} }
// 添加小区信息 // 添加小区信息
if (config["cells"] != null) if (config["cells"] != null)
{ {

40
LTEMvcApp/Services/WebSocketManagerService.cs

@ -120,6 +120,19 @@ namespace LTEMvcApp.Services
private ClientConfig GetDefaultTestConfig() private ClientConfig GetDefaultTestConfig()
{ {
var layers = new Dictionary<string, LogLayerConfig>();
foreach(var layerName in LogLayerTypes.AllLayers.Where(l => l != "EVENT"))
{
layers[layerName] = new LogLayerConfig { Level = LogLayerTypes.GetDefaultLevel(layerName), Filter = "warn", MaxSize = 1000, Payload = false };
}
// Set some specific payloads to true
if(layers.ContainsKey("PHY")) layers["PHY"].Payload = true;
if(layers.ContainsKey("MAC")) layers["MAC"].Payload = true;
if(layers.ContainsKey("RRC")) layers["RRC"].Payload = true;
if(layers.ContainsKey("NAS")) layers["NAS"].Payload = true;
return new ClientConfig return new ClientConfig
{ {
Name = "TestClient", Name = "TestClient",
@ -128,25 +141,11 @@ namespace LTEMvcApp.Services
Ssl = false, Ssl = false,
ReconnectDelay = 15000, ReconnectDelay = 15000,
Password = "test123", Password = "test123",
Logs = new Dictionary<string, object> Logs = new ClientLogsConfig
{ {
["layers"] = new Dictionary<string, object> Layers = layers,
{ Signal = true,
["PHY"] = new LogLayerConfig { Level = "info", Filter = "warn", MaxSize = 1000, Payload = true }, Cch = true
["MAC"] = new LogLayerConfig { Level = "info", Filter = "warn", MaxSize = 1000, Payload = true },
["RLC"] = new LogLayerConfig { Level = "info", Filter = "warn", MaxSize = 1000, Payload = false },
["PDCP"] = new LogLayerConfig { Level = "warn", Filter = "warn", MaxSize = 1000, Payload = false },
["RRC"] = new LogLayerConfig { Level = "debug", Filter = "warn", MaxSize = 1000, Payload = true },
["NAS"] = new LogLayerConfig { Level = "debug", Filter = "warn", MaxSize = 1000, Payload = true },
["S1AP"] = new LogLayerConfig { Level = "debug", Filter = "warn", MaxSize = 1000, Payload = false },
["NGAP"] = new LogLayerConfig { Level = "debug", Filter = "warn", MaxSize = 1000, Payload = false },
["GTPU"] = new LogLayerConfig { Level = "info", Filter = "warn", MaxSize = 1000, Payload = false },
["X2AP"] = new LogLayerConfig { Level = "debug", Filter = "warn", MaxSize = 1000, Payload = false },
["XnAP"] = new LogLayerConfig { Level = "info", Filter = "warn", MaxSize = 1000, Payload = false },
["M2AP"] = new LogLayerConfig { Level = "info", Filter = "warn", MaxSize = 1000, Payload = false }
},
["signal"] = true,
["cch"] = true
} }
}; };
} }
@ -335,13 +334,12 @@ namespace LTEMvcApp.Services
/// </summary> /// </summary>
/// <param name="clientName">客户端名称</param> /// <param name="clientName">客户端名称</param>
/// <param name="logsConfig">日志配置</param> /// <param name="logsConfig">日志配置</param>
/// <param name="save">是否保存</param>
/// <returns>是否成功设置</returns> /// <returns>是否成功设置</returns>
public bool SetClientLogsConfig(string clientName, Dictionary<string, object> logsConfig, bool save = false) public bool SetClientLogsConfig(string clientName, ClientLogsConfig logsConfig)
{ {
if (_clients.TryGetValue(clientName, out var client)) if (_clients.TryGetValue(clientName, out var client))
{ {
client.SetLogsConfig(logsConfig, save); client.SetLogsConfig(logsConfig, true);
return true; return true;
} }
return false; return false;

8
LTEMvcApp/Views/Home/Index.cshtml

@ -34,8 +34,8 @@
var state = (LTEMvcApp.Models.ClientState)client.State; var state = (LTEMvcApp.Models.ClientState)client.State;
<tr> <tr>
<td><a>@config.Name</a></td> <td>@client.Config.Name</td>
<td><small>@((config.Ssl ? "wss://" : "ws://") + config.Address)</small></td> <td>@client.Config.Address</td>
<td> <td>
@if (state == LTEMvcApp.Models.ClientState.Connected) @if (state == LTEMvcApp.Models.ClientState.Connected)
{ {
@ -87,8 +87,8 @@
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
消息 消息
</a> </a>
<a class="btn btn-secondary btn-sm" href="@Url.Action("TestClientConfig", "Home")"> <a href="@Url.Action("TestClientConfig", "Home")" class="btn btn-sm btn-warning">
<i class="fas fa-cogs"></i> <i class="fas fa-cog"></i>
配置 配置
</a> </a>
</td> </td>

32
LTEMvcApp/Views/Home/TestClientConfig.cshtml

@ -3,32 +3,7 @@
var testConfig = ViewBag.TestConfig as LTEMvcApp.Models.ClientConfig; var testConfig = ViewBag.TestConfig as LTEMvcApp.Models.ClientConfig;
// 只保留不含 EVENT 的日志层 // 只保留不含 EVENT 的日志层
var allLayers = LTEMvcApp.Models.LogLayerTypes.AllLayers.Where(l => l != "EVENT").ToArray(); var allLayers = LTEMvcApp.Models.LogLayerTypes.AllLayers.Where(l => l != "EVENT").ToArray();
var layerConfigs = new Dictionary<string, LTEMvcApp.Models.LogLayerConfig>(); var layerConfigs = testConfig?.Logs?.Layers ?? new Dictionary<string, LTEMvcApp.Models.LogLayerConfig>();
if (testConfig?.Logs?.ContainsKey("layers") == true && testConfig.Logs["layers"] is System.Text.Json.JsonElement layersElement)
{
var layers = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, LTEMvcApp.Models.LogLayerConfig>>(layersElement.GetRawText(), new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? new Dictionary<string, LTEMvcApp.Models.LogLayerConfig>();
foreach (var layerName in allLayers)
{
if (layers.TryGetValue(layerName, out var config))
{
layerConfigs[layerName] = config;
}
else
{
layerConfigs[layerName] = new LTEMvcApp.Models.LogLayerConfig { Level = LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layerName) };
}
}
}
else
{
foreach (var layerName in allLayers)
{
layerConfigs[layerName] = new LTEMvcApp.Models.LogLayerConfig { Level = LTEMvcApp.Models.LogLayerTypes.GetDefaultLevel(layerName) };
}
}
} }
<div class="container-fluid"> <div class="container-fluid">
@ -216,7 +191,7 @@
}; };
// 构建日志层配置 // 构建日志层配置
var layers = @Html.Raw(Json.Serialize(allLayers)); var layers = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(allLayers));
layers.forEach(function(layer) { layers.forEach(function(layer) {
var level = $(`select[name="layers[${layer}][level]"]`).val(); var level = $(`select[name="layers[${layer}][level]"]`).val();
var filter = $(`select[name="layers[${layer}][filter]"]`).val(); var filter = $(`select[name="layers[${layer}][filter]"]`).val();
@ -231,6 +206,9 @@
}; };
}); });
// 添加其他日志配置
// formData.logs.signal = ...; // 如果需要的话
$.ajax({ $.ajax({
url: '/api/websocket/test-client-config', url: '/api/websocket/test-client-config',
type: 'POST', type: 'POST',

Loading…
Cancel
Save