Files
SecMPS/doc/代码审核/网关代码审核20260604.md
T

536 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 网关项目深度代码审核报告
> 日期: 2026-06-04 | 项目: gateway/ | 扫描: 38 源文件, ~2800 行 | 问题: 30 项
---
## 1. Core/Infrastructure/AdapterRegistry.cs — 2 项
### AR1 [🟡] `GetOnlineAdapters()` 方法名误导
**位置**: 第 49-50 行
```csharp
public IReadOnlyList<IGatewayAdapter> GetOnlineAdapters()
=> _adapters.AsReadOnly();
```
返回所有已注册适配器,**不做任何在线/离线判断**。调用者会误以为返回的是在线适配器。
**修复**:
```csharp
public IReadOnlyList<IGatewayAdapter> GetAllAdapters()
=> _adapters.AsReadOnly();
```
同时 grep 全项目无任何代码调用此方法,可直接删除(或保留为兼容性方法并标注 `[Obsolete]`)。
### AR2 [🟡] `FindByCode<T>` O(n) 查找无缓存
每次 B 路由请求都执行 `_adapters.FirstOrDefault(...)`,适配器数量少时影响可忽略(<10 个),但如果未来扩展到 50+ 适配器会有性能问题。
**修复** — 加字典缓存:
```csharp
private readonly Dictionary<string, IGatewayAdapter> _byCode = new();
public void Register(IGatewayAdapter adapter)
{
_adapters.Add(adapter);
_byCode[adapter.AdapterCode] = adapter;
}
public T? FindByCode<T>(string adapterCode) where T : class, IGatewayAdapter
=> _byCode.TryGetValue(adapterCode, out var a) ? a as T : null;
```
---
## 2. Core/Infrastructure/RateLimiter.cs — 1 项
### RL1 [🟠] `Task.Run` 无限制创建后台任务
**位置**: 第 30-32 行
```csharp
public async Task WaitAsync(CancellationToken ct = default)
{
await _semaphore.WaitAsync(ct);
_ = Task.Run(async () => { await Task.Delay(_intervalMs, ct); try { _semaphore.Release(); } catch { } }, ct);
}
```
每次等待都创建一个新的 `Task.Run`。在 KMS 5 QPS 配置下每秒创建 5 个 Task5 个适配器 = 25 task/s。虽然 Task 本身轻量,但 `Task.Delay` + `Release` 的火焰纹章在极端高并发下可能导致线程池饥饿。
**修复** — 使用 `PeriodicTimer` 或固定数量的后台任务:
```csharp
private readonly SemaphoreSlim _semaphore;
private readonly int _maxTokens;
public RateLimiter(int tokensPerSecond)
{
_maxTokens = tokensPerSecond;
_semaphore = new SemaphoreSlim(_maxTokens, _maxTokens);
// 单后台任务持续补充令牌
_ = Task.Run(async () =>
{
var interval = 1000 / _maxTokens;
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(interval));
while (await timer.WaitForNextTickAsync())
{
try { if (_semaphore.CurrentCount < _maxTokens) _semaphore.Release(); }
catch (ObjectDisposedException) { break; }
}
});
}
public async Task WaitAsync(CancellationToken ct = default)
=> await _semaphore.WaitAsync(ct);
```
---
## 3. Core/Models/AdapterCapabilities.cs — 1 项
### AC1 [🟡] `HasPtz` 与 `AcceptsControl` 字段冗余
**位置**: 第 10-13 行
```csharp
public bool HasPtz { get; set; }
public bool AcceptsControl { get; set; }
```
`HasPtz``HasStreams` 的子能力,`AcceptsControl``IAcceptsControl` 接口的声明。这两个字段在 `AdapterCapabilities` 中冗余——`IAcceptsControl` 接口本身已经声明了控制能力。
**修复** — 删除 `AcceptsControl` 字段(能力声明应直接检查接口实现):
```csharp
// 删除 AcceptsControl、HasPtz
// 网关 B 路由中改为: a is IAcceptsControl
```
**跨文件影响**: `gateway/src/IntegrationGateway.Host/Program.cs` — B10 路由已用 `FindByCode<IAcceptsControl>` 不再需要此字段。OwlAdapter/Mc4Adapter 的 Capabilities 声明中删除对应行。
---
## 4. Core/Models/StandardDevice.cs — 1 项
### SD1 [🟡] `DeviceId` 字段职责混淆
**位置**: 第 9 行
```csharp
public int DeviceId { get; set; }
```
`DeviceId` 是 VolPro 侧的主键,由 A3 同步后由 VolPro 回填。但网关在 `GetDevicesAsync` 返回时不设置此字段(始终为 0)。文档注释说"同步后由 VolPro 回填",但调用者可能在未同步时就读取此字段。
**修复** — 改为 `int?` 并初始化为 null:
```csharp
public int? DeviceId { get; set; }
// VolPro 回填后才有值
```
---
## 5. Core/Abstractions/IHasRecordings.cs — 1 项
### IR1 [🟡] 方法参数过多
**位置**: 第 12-13 行
```csharp
Task<PagedResult<StandardRecording>> GetRecordingsAsync(
string channelId, DateTime start, DateTime end, int page, int size);
```
5 个位置参数,调用时易错序。
**修复** — 使用请求对象:
```csharp
public class RecordingQuery
{
public string ChannelId { get; set; } = "";
public DateTime Start { get; set; }
public DateTime End { get; set; }
public int Page { get; set; } = 1;
public int Size { get; set; } = 20;
}
Task<PagedResult<StandardRecording>> GetRecordingsAsync(RecordingQuery query);
```
**跨文件影响**: `OwlAdapter.cs` GetRecordingsAsync 实现 + `Program.cs` B 路由。
---
## 6. Core/Infrastructure/GatewayClientFactory.cs — 2 项
### GF1 [🟡] `JsonDocument?` 返回类型不透明
**位置**: 第 33-42 行
```csharp
public async Task<JsonDocument?> RegisterAsync(GatewayRegisterRequest req) { ... }
public async Task<JsonDocument?> SyncDevicesAsync(...) { ... }
```
返回 `JsonDocument?` 使调用者必须知道 VolPro 响应的 JSON 结构。
**修复** — 定义响应 DTO:
```csharp
public class RegisterResponse { public int NodeId { get; set; } public List<DeviceSummary> Devices { get; set; } = new(); }
public Task<RegisterResponse?> RegisterAsync(GatewayRegisterRequest req) { ... }
```
### GF2 [🟡] `CreateClient()` 每次创建新 HttpClient
```csharp
private HttpClient CreateClient() => _httpFactory.CreateClient("VolPro");
```
`IHttpClientFactory.CreateClient("VolPro")` 从连接池返回,每次调用都会创建新的 `HttpClient` 包装实例。虽然底层 SocketsHttpHandler 复用连接,但 `HttpClient` 上设置的 Timeout/Headers 不会被保留。
**修复** — 直接使用工厂客户端(已配置好 Timeout/Headers:
```csharp
// Program.cs 中注册时已设置 Timeout=30s + Accept: application/json
// 直接使用 factory 创建的客户端
private HttpClient GetClient() => _httpFactory.CreateClient("VolPro");
```
---
## 7. Adapters.Owl/OwlAdapter.cs — 5 项
### OW1 [🟠] `GetDevicesAsync` 忽略 `page`/`size` 参数
**位置**: 第 68 行
```csharp
var url = $"/devices/channels?page={page}&size=1000";
```
硬编码 `size=1000`,无论调用者传的 `size` 值是多少。前端分页完全失效。
**修复**:
```csharp
var url = $"/devices/channels?page={page}&size={size}";
```
### OW2 [🟡] `GetPlaybackUrlAsync` 手工拼 URL 脆弱
**位置**: 第 149-153 行
```csharp
return new StreamUrls
{
Hls = $"{baseUrl}/recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}"
};
```
直接拼接 URL,依赖 Owl 内部路径约定。Owl API 版本升级可能改变路径格式。
**修复** — 调 Owl API 获取回放地址(如果有对应接口)或至少将 base URL 路径前缀提取为常量:
```csharp
private const string PlaybackPathFormat = "/recordings/channels/{0}/index.m3u8?start_ms={1}&end_ms={2}&token={3}";
var hls = string.Format(PlaybackPathFormat, channelId, startMs, endMs, token);
```
### OW3 [🟡] `MapChannel` IsOnline 判断脆弱
**位置**: 第 116 行
```csharp
IsOnline = ch.IsOnline?.ToLower() == "true" || ch.IsOnline == "1",
```
Owl 的 `IsOnline` 字段在不同接口中返回类型不同:设备接口返回 `"0"/"1"`,通道接口可能返回 `true/false` 字符串。这种脆弱的兼容方式在 Owl 版本升级后可能失效。
**修复** — 统一为 bool 解析:
```csharp
private static bool ParseOwlOnline(string? val) =>
bool.TryParse(val, out var b) ? b : val == "1";
```
### OW4 [🟡] `MapDevice` + `MapChannel` 硬编码设备名
**位置**: 第 91, 113 行
```csharp
Category = "硬盘录像机", Group = "视频设备"
Category = "摄像机", Group = "视频设备"
```
**修复** — 提取为常量:
```csharp
private const string DEVICE_CATEGORY_NVR = "硬盘录像机";
private const string DEVICE_CATEGORY_CAMERA = "摄像机";
private const string DEVICE_GROUP_VIDEO = "视频设备";
```
### OW5 [⚪] 文件 308 行过长
**修复** — 拆分为:
- `OwlAdapter.cs` — 类声明 + 构造函数 + Capabilities (40 行)
- `OwlAdapter.FlatDevices.cs` — IHasFlatDevices 实现 (70 行)
- `OwlAdapter.Streams.cs` — IHasStreams 实现 (80 行)
- `OwlAdapter.Recordings.cs` — IHasRecordings (30 行)
- `OwlAdapter.Alarms.cs` — IHasAlarms (60 行)
---
## 8. Adapters.Owl/OwlAuthHelper.cs — 0 项
代码质量良好的 RSA 加密认证实现。Token 缓存策略合理(2.5 天/3 天)。无可优化项。
---
## 9. Adapters.Owl/OwlModels.cs — 1 项
### OM1 [🟡] `OwlDeviceChannel` 字段注释缺失
**位置**: 第 17-38 行
`Type``Did``Ptztype``App``StreamId` 等字段无注释说明其含义和取值范围。
**修复** — 添加 XML 注释:
```csharp
/// <summary>类型: "DEVICE"(NVR) | "CHANNEL"(摄像头)</summary>
public string? Type { get; set; }
/// <summary>设备 ID(通道记录指向其所属设备)</summary>
public string? Did { get; set; }
/// <summary>云台类型: 0=无, 1=方向, 2=预置位</summary>
public int? Ptztype { get; set; }
```
---
## 10. Adapters.MC4/Mc4Adapter.cs — 4 项
### MC1 [🟠] `GetAlarmsAsync` DateTime.MinValue 仍发送
**位置**: 第 145-146 行
```csharp
From = from.ToString("yyyy-MM-dd HH:mm:ss"),
To = to.ToString("yyyy-MM-dd HH:mm:ss"),
```
`from`/`to``DateTime.MinValue`B8 路由默认值)时,发送 `"0001-01-01 00:00:00"` 给 MC4——MC4 将认为这是有效日期过滤。
**修复**:
```csharp
From = from == DateTime.MinValue ? "" : from.ToString("yyyy-MM-dd HH:mm:ss"),
To = to == DateTime.MinValue ? "" : to.ToString("yyyy-MM-dd HH:mm:ss"),
```
### MC2 [🟡] MC4 模型应独立文件
**位置**: 第 266-350 行
`Mc4TreeNode``Mc4PointValue``Mc4AlarmQuery` 等 8 个类全部定义在 `Mc4Adapter.cs` 底部。
**修复** — 创建 `Mc4Models.cs`(参照 KMS/Owl 的模式),将 266 行之后的模型移出。
### MC3 [🟡] `GetMultiRealtimeValuesAsync` + `GetHisAlarmsAsync` 未暴露到 B 路由
**位置**: 第 211, 228 行
这两个方法是 MC4.0 原生批量接口,但 Program.cs 中没有对应的 B 路由暴露它们。B4-batch 路由直接用 `IHasPoints.GetRealtimeValuesAsync` 逐设备调用。
**修复** — B4-batch 路由已检查 `Mc4Adapter` 类型并优先调用 `GetMultiRealtimeValuesAsync`(已实现)。确认编译通过即可。
### MC4 [🟡] `ConfirmAlarmAsync` / `EndAlarmAsync` 不检查响应
**位置**: 第 175-192 行
```csharp
await client.PostAsync("/api/central/alarm/confirm", ...); // 无 resp.EnsureSuccessStatusCode()
```
MC4 返回非 200 时静默失败,调用者认为确认成功。
**修复**:
```csharp
var resp = await client.PostAsync(...);
resp.EnsureSuccessStatusCode();
```
---
## 11. Adapters.MC4/Mc4AuthHelper.cs — 1 项
### MA1 [🟡] `_needMd5` 只获取一次
**位置**: 第 46-57 行
```csharp
if (!_needMd5.HasValue) { /* 仅首次调用时查 conf/get */ }
```
如果 MC4 服务重启并更改了加密配置(encrypt true↔false),适配器不会重新检测。但实际场景中 MC4 不会在运行时切换加密模式,风险极低。
**修复** — 加一个 Token 刷新计数器,每 N 次重新获取 conf/getN=10 即可)。
---
## 12. Adapters.Kms/KmsAdapter.cs — 1 项
### KM1 [⚪] `OpenerIds` 缩进不齐
**位置**: 第 296 行
```csharp
OpenerIds = parameters.TryGetValue(...)
```
前面的注释和代码使用 8 空格缩进,`OpenerIds` 使用 0 空格缩进。视觉效果不一致但不影响编译。
**修复** — 统一缩进为 8 空格。
---
## 13. Adapters.Kms/KmsAuthHelper.cs — 0 项
Token 缓存 25min(30-5)、Bearer 头、Invalidate 均实现正确。无可优化项。
---
## 14. Adapters.Kms/KmsModels.cs — 0 项
15 个 DTO 覆盖全部 KMS 接口,字段完整。标准接口 DTO 虽暂未使用,但注释说明 Phase 2 用途——保留合理。
---
## 15. Host/Program.cs — 6 项
### PR1 [🟠] `SyncAllDevicesAsync` 和 `FlattenTree` 在两个层次定义
**位置**: 第 152-184 行
这两个函数在 `InitializeAllAsync` 回调的闭包作用域中定义为局部函数。如果未来需要在其他地方调用(如 A3 手动触发),将不可用。
**修复** — 提取为私有静态方法或迁移到 Core:
```csharp
// 新建 Core/Infrastructure/DeviceSyncHelper.cs
public static class DeviceSyncHelper
{
public static async Task SyncAllAsync(AdapterRegistry reg, GatewayClientFactory factory, string nodeCode, string token) { ... }
private static void FlattenTree(...) { ... }
}
```
### PR2 [🟡] Swagger 无 XML 注释
**位置**: 第 17-19 行
```csharp
builder.Services.AddSwaggerGen(); // 无 XML 注释选项
```
所有 Minimal API 端点没有 Swagger 描述,调用者必须参考外部文档。
**修复**:
```csharp
builder.Services.AddSwaggerGen(c =>
{
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFile));
});
```
并在 `.csproj` 中添加 `<GenerateDocumentationFile>true</GenerateDocumentationFile>`
### PR3 [🟡] 路由注册代码可读性差
**位置**: 第 186-377 行
19 条 B 路由全部内联在 `Program.cs` 中,每条路由 5-10 行,总计 ~200 行路由注册代码。
**修复** — 按模块拆分为扩展方法:
```csharp
// Host/Routes/HealthRoutes.cs
public static class HealthRoutes
{
public static void MapHealthEndpoints(this WebApplication app, AdapterRegistry registry) { ... }
}
// Host/Routes/DeviceRoutes.cs — B2, B3, B3-sync
// Host/Routes/StreamRoutes.cs — B6a, B6b, B7, snapshot
// Host/Routes/RealtimeRoutes.cs — B4, B4-batch, B5
// Host/Routes/AlarmRoutes.cs — B8, B9-confirm, B9-end
// Host/Routes/ControlRoutes.cs — B10
// Host/Routes/LogRoutes.cs — B11
// Host/Routes/SyncRoutes.cs — B12, B13
```
Program.cs 改为:
```csharp
app.MapHealthEndpoints(registry);
app.MapDeviceEndpoints(registry);
// ...
```
### PR4 [🟡] 配置缺少验证
**位置**: 第 53-83 行
`app.Configuration.GetSection("Owl").Get<List<OwlConfig>>()` 可能在配置格式错误时返回 null,直接 `foreach` 正常运作但无错误提示。
**修复** — 加空值检查和警告:
```csharp
var owlList = app.Configuration.GetSection("Owl").Get<List<OwlConfig>>();
if (owlList == null || !owlList.Any()) Console.WriteLine("[Gateway] WARNING: 未配置 Owl 适配器");
```
### PR5 [🟡] `appsettings.json` 凭证明文
**位置**: appsettings.json
```json
"NodeToken": "changeme",
"Password": "your_owl_password",
"ClientSecret": "your_client_secret"
```
**修复** — 生产环境注入:
```json
"NodeToken": null, // 生产环境由 SECMPS_GATEWAY_TOKEN 环境变量注入
"Password": null, // 生产环境由 OWL_PASSWORD 注入
"ClientSecret": null // 生产环境由 KMS_CLIENT_SECRET 注入
```
Program.cs:
```csharp
var nodeToken = Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"];
```
### PR6 [⚪] dotnet-tools.json 未使用
`gateway/src/IntegrationGateway.Host/dotnet-tools.json` 存在但可能无声明工具。确认是否被使用。
---
## 16. 跨切面问题 — 4 项
### X1 [🟡] 无统一日志抽象
所有适配器使用 `Console.Error.WriteLine` 直接写控制台。无日志级别、无结构化输出、无法对接日志收集系统。
**修复** — 注入 `ILogger<T>`:
```csharp
public class OwlAdapter : ...
{
private readonly ILogger<OwlAdapter> _logger;
public OwlAdapter(..., ILogger<OwlAdapter> logger) { _logger = logger; }
public async Task<bool> HealthCheckAsync()
{
try { ... }
catch (Exception ex) { _logger.LogWarning(ex, "[{Code}] HealthCheck 失败", AdapterCode); return false; }
}
}
```
### X2 [🟡] `GetAuthenticatedClientAsync` 每次 new HttpClient
三个适配器(Owl/KMS/MC4)的 AuthHelper 都在 `GetAuthenticatedClientAsync``new HttpClient { BaseAddress = new Uri(_baseUrl) }`。每个请求创建一个新的 HttpClient 实例,无法复用连接池。
**修复** — 传入 `IHttpClientFactory`:
```csharp
public class KmsAuthHelper
{
private readonly IHttpClientFactory _httpFactory;
public async Task<HttpClient> GetAuthenticatedClientAsync()
{
var token = await GetTokenAsync();
var client = _httpFactory.CreateClient(); // 连接池复用
client.BaseAddress = new Uri(_baseUrl);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return client;
}
}
```
**跨文件影响**: 三个 AuthHelper 构造函数全部需加 `IHttpClientFactory` 参数。三个 Adapter 构造函数也需传递。
### X3 [🟡] 无 request/response DTO 版本策略
当前所有接口模型无版本号字段。未来接口升级时无法区分新旧格式。
**修复** — 在 B 路由响应中加 `version` 字段:
```csharp
return Results.Ok(new { version = "1.0", items = result.Items, total = result.Total });
```
### X4 [⚪] `csproj` 无 `<GenerateDocumentationFile>` 配置
三个适配器项目均无 XML 文档生成配置。
**修复** — 在各 `.csproj` 中添加 `<GenerateDocumentationFile>true</GenerateDocumentationFile>`
---
## 统计
| 级别 | 数量 | 位置 |
|:--:|:--:|------|
| 🟠 严重 | 4 | RateLimiter(Owl.GetDevices size,MC4 MinValue,Program SyncAll) |
| 🟡 改善 | 21 | 命名/拆分/日志/HttpClient/配置 |
| ⚪ 低优 | 5 | 缩进/tools.json/注释 |
## 总评
网关架构设计优秀——适配器模式隔离清晰、限流/认证/错误处理一致、接口粒度过细(19 条 B 路由)而非不足。最大改进空间在于:HttpClient 连接池复用、日志抽象统一、Program.cs 路由拆分。