diff --git a/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesQuery.cs b/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesQuery.cs
new file mode 100644
index 0000000..8a444a5
--- /dev/null
+++ b/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesQuery.cs
@@ -0,0 +1,12 @@
+using CellularManagement.Domain.Common;
+using MediatR;
+
+namespace CellularManagement.Application.Features.ProtocolLogs.Queries.GetRuntimeDeviceCodesNotInActiveRuntimes;
+
+///
+/// 获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode查询
+///
+public class GetRuntimeDeviceCodesNotInActiveRuntimesQuery : IRequest>
+{
+ // 此查询不需要参数,直接获取所有符合条件的记录
+}
\ No newline at end of file
diff --git a/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesQueryHandler.cs b/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesQueryHandler.cs
new file mode 100644
index 0000000..e5f718a
--- /dev/null
+++ b/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesQueryHandler.cs
@@ -0,0 +1,92 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using CellularManagement.Domain.Common;
+using CellularManagement.Domain.Repositories.Logging;
+using CellularManagement.Domain.Models;
+
+namespace CellularManagement.Application.Features.ProtocolLogs.Queries.GetRuntimeDeviceCodesNotInActiveRuntimes;
+
+///
+/// 获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode查询处理器
+///
+public class GetRuntimeDeviceCodesNotInActiveRuntimesQueryHandler : IRequestHandler>
+{
+ private readonly IProtocolLogRepository _protocolLogRepository;
+ private readonly ILogger _logger;
+
+ ///
+ /// 初始化查询处理器
+ ///
+ public GetRuntimeDeviceCodesNotInActiveRuntimesQueryHandler(
+ IProtocolLogRepository protocolLogRepository,
+ ILogger logger)
+ {
+ _protocolLogRepository = protocolLogRepository;
+ _logger = logger;
+ }
+
+ ///
+ /// 处理获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode查询
+ ///
+ public async Task> Handle(GetRuntimeDeviceCodesNotInActiveRuntimesQuery request, CancellationToken cancellationToken)
+ {
+ try
+ {
+ _logger.LogInformation("开始获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode");
+
+ // 调用仓储方法获取数据
+ var items = await _protocolLogRepository.GetRuntimeDeviceCodesNotInActiveRuntimesAsync(cancellationToken);
+ var itemsList = items.ToList();
+
+ // 构建树形结构数据
+ var treeItems = BuildTreeStructure(itemsList);
+
+ // 构建响应
+ var response = new GetRuntimeDeviceCodesNotInActiveRuntimesResponse
+ {
+ TreeItems = treeItems,
+ TotalCount = itemsList.Count,
+ DeviceCount = treeItems.Count()
+ };
+
+ _logger.LogInformation("成功获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode,共 {TotalCount} 条记录,涉及 {DeviceCount} 个设备",
+ response.TotalCount, response.DeviceCount);
+
+ return OperationResult.CreateSuccess(response);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode时发生错误");
+ return OperationResult.CreateFailure($"获取协议日志运行时设备代码时发生错误: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 构建树形结构数据
+ /// 将平面数据按DeviceCode分组,每个设备下包含其对应的RuntimeCode列表
+ ///
+ /// 平面数据列表
+ /// 树形结构数据
+ private IEnumerable BuildTreeStructure(IEnumerable items)
+ {
+ // 按DeviceCode分组
+ var groupedData = items
+ .GroupBy(item => item.DeviceCode)
+ .Select(group => new ProtocolLogRuntimeDeviceTreeDto
+ {
+ DeviceCode = group.Key,
+ DeviceName = group.Key, // 可以根据需要从设备服务获取设备名称
+ Runtimes = group.Select(item => new ProtocolLogRuntimeInfo
+ {
+ RuntimeCode = item.RuntimeCode,
+ Timestamp = item.Timestamp
+ }).ToList()
+ })
+ .OrderBy(item => item.DeviceCode) // 按设备代码排序
+ .ToList();
+
+ _logger.LogDebug("构建树形结构完成,共 {DeviceCount} 个设备", groupedData.Count);
+
+ return groupedData;
+ }
+}
\ No newline at end of file
diff --git a/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesResponse.cs b/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesResponse.cs
new file mode 100644
index 0000000..3a919fa
--- /dev/null
+++ b/src/X1.Application/Features/ProtocolLogs/Queries/GetRuntimeDeviceCodesNotInActiveRuntimes/GetRuntimeDeviceCodesNotInActiveRuntimesResponse.cs
@@ -0,0 +1,29 @@
+using CellularManagement.Domain.Models;
+
+namespace CellularManagement.Application.Features.ProtocolLogs.Queries.GetRuntimeDeviceCodesNotInActiveRuntimes;
+
+///
+/// 获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode响应
+///
+public class GetRuntimeDeviceCodesNotInActiveRuntimesResponse
+{
+ ///
+ /// 协议日志运行时设备树形结构(按设备分组)
+ ///
+ public IEnumerable TreeItems { get; set; } = Enumerable.Empty();
+
+ ///
+ /// 总数量
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 设备数量
+ ///
+ public int DeviceCount { get; set; }
+
+ ///
+ /// 是否有树形数据
+ ///
+ public bool HasData => TreeItems.Any();
+}
\ No newline at end of file
diff --git a/src/X1.Domain/Models/ProtocolLogRuntimeDeviceDto.cs b/src/X1.Domain/Models/ProtocolLogRuntimeDeviceDto.cs
new file mode 100644
index 0000000..85adc41
--- /dev/null
+++ b/src/X1.Domain/Models/ProtocolLogRuntimeDeviceDto.cs
@@ -0,0 +1,23 @@
+namespace CellularManagement.Domain.Models;
+
+///
+/// 协议日志运行时设备DTO
+/// 用于返回协议日志的RuntimeCode和DeviceCode
+///
+public class ProtocolLogRuntimeDeviceDto
+{
+ ///
+ /// 运行时代码
+ ///
+ public string RuntimeCode { get; set; } = string.Empty;
+
+ ///
+ /// 设备代码
+ ///
+ public string DeviceCode { get; set; } = string.Empty;
+
+ ///
+ /// 时间戳
+ ///
+ public long Timestamp { get; set; }
+}
\ No newline at end of file
diff --git a/src/X1.Domain/Models/ProtocolLogRuntimeDeviceTreeDto.cs b/src/X1.Domain/Models/ProtocolLogRuntimeDeviceTreeDto.cs
new file mode 100644
index 0000000..f1c3cb3
--- /dev/null
+++ b/src/X1.Domain/Models/ProtocolLogRuntimeDeviceTreeDto.cs
@@ -0,0 +1,49 @@
+namespace CellularManagement.Domain.Models;
+
+///
+/// 协议日志运行时设备树形结构DTO
+/// 用于表示按设备分组的运行时数据,便于前端渲染树形结构
+///
+public class ProtocolLogRuntimeDeviceTreeDto
+{
+ ///
+ /// 设备代码
+ ///
+ public string DeviceCode { get; set; } = string.Empty;
+
+ ///
+ /// 设备名称(可选,用于显示)
+ ///
+ public string? DeviceName { get; set; }
+
+ ///
+ /// 该设备下的运行时列表
+ ///
+ public List Runtimes { get; set; } = new();
+
+ ///
+ /// 该设备下的运行时数量
+ ///
+ public int RuntimeCount => Runtimes.Count;
+}
+
+///
+/// 协议日志运行时信息
+///
+public class ProtocolLogRuntimeInfo
+{
+ ///
+ /// 运行时代码
+ ///
+ public string RuntimeCode { get; set; } = string.Empty;
+
+ ///
+ /// 时间戳
+ ///
+ public long Timestamp { get; set; }
+
+ ///
+ /// 格式化后的时间显示
+ ///
+ public string FormattedTime => DateTimeOffset.FromUnixTimeMilliseconds(Timestamp).ToString("yyyy-MM-dd HH:mm:ss.fff");
+}
\ No newline at end of file
diff --git a/src/X1.Domain/Repositories/Logging/IProtocolLogRepository.cs b/src/X1.Domain/Repositories/Logging/IProtocolLogRepository.cs
index 3092285..e4c087b 100644
--- a/src/X1.Domain/Repositories/Logging/IProtocolLogRepository.cs
+++ b/src/X1.Domain/Repositories/Logging/IProtocolLogRepository.cs
@@ -84,4 +84,11 @@ public interface IProtocolLogRepository : IBaseRepository
/// MessageDetailJson内容
Task GetMessageDetailJsonByIdAsync(string id, CancellationToken cancellationToken = default);
+ ///
+ /// 获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode
+ ///
+ /// 取消令牌
+ /// RuntimeCode和DeviceCode列表
+ Task> GetRuntimeDeviceCodesNotInActiveRuntimesAsync(CancellationToken cancellationToken = default);
+
}
\ No newline at end of file
diff --git a/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs b/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs
index 1ac1c8d..4ef2015 100644
--- a/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs
+++ b/src/X1.Infrastructure/Repositories/Logging/ProtocolLogRepository.cs
@@ -320,4 +320,40 @@ public class ProtocolLogRepository : BaseRepository, IProtocolLogRe
throw;
}
}
+
+ ///
+ /// 获取不在活跃运行时状态中的协议日志RuntimeCode和DeviceCode
+ ///
+ /// 取消令牌
+ /// RuntimeCode和DeviceCode列表
+ public async Task> GetRuntimeDeviceCodesNotInActiveRuntimesAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ // 使用DISTINCT ON和NOT EXISTS子查询,性能优化
+ var sql = @"
+ SELECT DISTINCT ON (tpl.""DeviceCode"", tpl.""RuntimeCode"")
+ tpl.""RuntimeCode"", tpl.""DeviceCode"", tpl.""Timestamp""
+ FROM ""tb_protocol_logs"" tpl
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM ""tb_cellular_device_runtimes"" t
+ WHERE t.""RuntimeStatus"" = 1
+ AND t.""RuntimeCode"" = tpl.""RuntimeCode""
+ )
+ ORDER BY tpl.""DeviceCode"", tpl.""RuntimeCode"", tpl.""Timestamp"" DESC";
+
+ // 执行SQL查询,直接映射到ProtocolLogRuntimeDeviceDto
+ var result = await QueryRepository.ExecuteSqlQueryAsync(sql, Array.Empty