Browse Source

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

- 新增 ClientLogsConfig 类,封装日志配置
- 重构 ClientConfig 类,使用强类型替代弱类型字典
- 更新 LTEClientWebSocket 服务,简化配置处理逻辑
- 修复 WebSocketManagerService 中的类型安全问题
- 更新控制器和视图,适配新的强类型模型
- 消除所有 JsonElement 和 Dictionary<string,object> 的混乱使用
- 提升代码可读性、可维护性和类型安全性
feature/strong-typing-refactor
root 1 month 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,
ReconnectDelay = 5000,
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>
{
["level"] = "debug",
["filter"] = "debug",
["max_size"] = 1000,
["payload"] = false
},
["RRC"] = new Dictionary<string, object>
{
["level"] = "debug",
["filter"] = "debug",
["max_size"] = 1000,
["payload"] = false
}
["PHY"] = new LogLayerConfig { Level = "debug", Filter = "debug", MaxSize = 1000, Payload = false },
["RRC"] = new LogLayerConfig { Level = "debug", Filter = "debug", MaxSize = 1000, Payload = false }
},
["signal"] = true,
["cch"] = true
Signal = true,
Cch = true
}
};

15
LTEMvcApp/Controllers/WebSocketController.cs

@ -170,16 +170,19 @@ namespace LTEMvcApp.Controllers
/// 设置客户端日志配置
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="request">日志配置请求</param>
/// <returns>操作结果</returns>
/// <param name="request">请求体</param>
[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)
return Ok(new { message = $"客户端 '{clientName}' 日志配置已更新" });
{
return Ok(new { message = "日志配置已更新" });
}
else
return BadRequest($"更新客户端 '{clientName}' 日志配置失败");
{
return NotFound(new { message = $"客户端 '{clientName}' 未找到或更新失败" });
}
}
/// <summary>

99
LTEMvcApp/Models/LTEClient.cs

@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using System.Text.Json.Serialization;
namespace LTEMvcApp.Models;
@ -45,6 +46,11 @@ public class LTEClient
/// </summary>
public string? Model { get; set; }
/// <summary>
/// 是否有信号记录
/// </summary>
public bool HasSignalRecord { get; set; }
#endregion
#region 日志相关属性
@ -147,11 +153,6 @@ public class LTEClient
/// </summary>
public bool HasRb { get; set; }
/// <summary>
/// 是否有信号记录
/// </summary>
public bool HasSignalRecord { get; set; }
#endregion
#region 参数和组件
@ -366,24 +367,100 @@ public class LTEClient
#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>
public class ClientConfig
{
/// <summary>
/// 客户端名称
/// </summary>
public string Name { get; set; } = string.Empty;
public bool Enabled { get; set; } = true;
public string? Model { get; set; }
public string? Address { get; set; }
/// <summary>
/// 服务器地址
/// </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 int ReconnectDelay { get; set; } = 15000;
/// <summary>
/// 日志配置
/// </summary>
public ClientLogsConfig Logs { get; set; } = new();
/// <summary>
/// 是否暂停
/// </summary>
public bool Pause { get; set; }
/// <summary>
/// 是否只读
/// </summary>
public bool Readonly { get; set; }
/// <summary>
/// 是否跳过日志菜单
/// </summary>
public bool SkipLogMenu { get; set; }
/// <summary>
/// 是否锁定
/// </summary>
public bool Locked { get; set; }
/// <summary>
/// 是否激活
/// </summary>
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>

145
LTEMvcApp/Services/LTEClientWebSocket.cs

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

40
LTEMvcApp/Services/WebSocketManagerService.cs

@ -120,6 +120,19 @@ namespace LTEMvcApp.Services
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
{
Name = "TestClient",
@ -128,25 +141,11 @@ namespace LTEMvcApp.Services
Ssl = false,
ReconnectDelay = 15000,
Password = "test123",
Logs = new Dictionary<string, object>
Logs = new ClientLogsConfig
{
["layers"] = new Dictionary<string, object>
{
["PHY"] = new LogLayerConfig { Level = "info", Filter = "warn", MaxSize = 1000, Payload = 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
Layers = layers,
Signal = true,
Cch = true
}
};
}
@ -335,13 +334,12 @@ namespace LTEMvcApp.Services
/// </summary>
/// <param name="clientName">客户端名称</param>
/// <param name="logsConfig">日志配置</param>
/// <param name="save">是否保存</param>
/// <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))
{
client.SetLogsConfig(logsConfig, save);
client.SetLogsConfig(logsConfig, true);
return true;
}
return false;

8
LTEMvcApp/Views/Home/Index.cshtml

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

32
LTEMvcApp/Views/Home/TestClientConfig.cshtml

@ -3,32 +3,7 @@
var testConfig = ViewBag.TestConfig as LTEMvcApp.Models.ClientConfig;
// 只保留不含 EVENT 的日志层
var allLayers = LTEMvcApp.Models.LogLayerTypes.AllLayers.Where(l => l != "EVENT").ToArray();
var layerConfigs = 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) };
}
}
var layerConfigs = testConfig?.Logs?.Layers ?? new Dictionary<string, LTEMvcApp.Models.LogLayerConfig>();
}
<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) {
var level = $(`select[name="layers[${layer}][level]"]`).val();
var filter = $(`select[name="layers[${layer}][filter]"]`).val();
@ -231,6 +206,9 @@
};
});
// 添加其他日志配置
// formData.logs.signal = ...; // 如果需要的话
$.ajax({
url: '/api/websocket/test-client-config',
type: 'POST',

Loading…
Cancel
Save