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

18 KiB
Raw Blame History

网关项目深度代码审核报告

日期: 2026-06-04 | 项目: gateway/ | 扫描: 38 源文件, ~2800 行 | 问题: 30 项


1. Core/Infrastructure/AdapterRegistry.cs — 2 项

AR1 [🟡] GetOnlineAdapters() 方法名误导

位置: 第 49-50 行

public IReadOnlyList<IGatewayAdapter> GetOnlineAdapters()
    => _adapters.AsReadOnly();

返回所有已注册适配器,不做任何在线/离线判断。调用者会误以为返回的是在线适配器。

修复:

public IReadOnlyList<IGatewayAdapter> GetAllAdapters()
    => _adapters.AsReadOnly();

同时 grep 全项目无任何代码调用此方法,可直接删除(或保留为兼容性方法并标注 [Obsolete])。

AR2 [🟡] FindByCode<T> O(n) 查找无缓存

每次 B 路由请求都执行 _adapters.FirstOrDefault(...),适配器数量少时影响可忽略(<10 个),但如果未来扩展到 50+ 适配器会有性能问题。

修复 — 加字典缓存:

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 行

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 或固定数量的后台任务:

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 [🟡] HasPtzAcceptsControl 字段冗余

位置: 第 10-13 行

public bool HasPtz { get; set; }
public bool AcceptsControl { get; set; }

HasPtzHasStreams 的子能力,AcceptsControlIAcceptsControl 接口的声明。这两个字段在 AdapterCapabilities 中冗余——IAcceptsControl 接口本身已经声明了控制能力。

修复 — 删除 AcceptsControl 字段(能力声明应直接检查接口实现):

// 删除 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 行

public int DeviceId { get; set; }

DeviceId 是 VolPro 侧的主键,由 A3 同步后由 VolPro 回填。但网关在 GetDevicesAsync 返回时不设置此字段(始终为 0)。文档注释说"同步后由 VolPro 回填",但调用者可能在未同步时就读取此字段。

修复 — 改为 int? 并初始化为 null:

public int? DeviceId { get; set; }
// VolPro 回填后才有值

5. Core/Abstractions/IHasRecordings.cs — 1 项

IR1 [🟡] 方法参数过多

位置: 第 12-13 行

Task<PagedResult<StandardRecording>> GetRecordingsAsync(
    string channelId, DateTime start, DateTime end, int page, int size);

5 个位置参数,调用时易错序。

修复 — 使用请求对象:

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 行

public async Task<JsonDocument?> RegisterAsync(GatewayRegisterRequest req) { ... }
public async Task<JsonDocument?> SyncDevicesAsync(...) { ... }

返回 JsonDocument? 使调用者必须知道 VolPro 响应的 JSON 结构。

修复 — 定义响应 DTO:

public class RegisterResponse { public int NodeId { get; set; } public List<DeviceSummary> Devices { get; set; } = new(); }
public Task<RegisterResponse?> RegisterAsync(GatewayRegisterRequest req) { ... }

GF2 [🟡] CreateClient() 每次创建新 HttpClient

private HttpClient CreateClient() => _httpFactory.CreateClient("VolPro");

IHttpClientFactory.CreateClient("VolPro") 从连接池返回,每次调用都会创建新的 HttpClient 包装实例。虽然底层 SocketsHttpHandler 复用连接,但 HttpClient 上设置的 Timeout/Headers 不会被保留。

修复 — 直接使用工厂客户端(已配置好 Timeout/Headers:

// 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 行

var url = $"/devices/channels?page={page}&size=1000";

硬编码 size=1000,无论调用者传的 size 值是多少。前端分页完全失效。

修复:

var url = $"/devices/channels?page={page}&size={size}";

OW2 [🟡] GetPlaybackUrlAsync 手工拼 URL 脆弱

位置: 第 149-153 行

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 路径前缀提取为常量:

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 行

IsOnline = ch.IsOnline?.ToLower() == "true" || ch.IsOnline == "1",

Owl 的 IsOnline 字段在不同接口中返回类型不同:设备接口返回 "0"/"1",通道接口可能返回 true/false 字符串。这种脆弱的兼容方式在 Owl 版本升级后可能失效。

修复 — 统一为 bool 解析:

private static bool ParseOwlOnline(string? val) =>
    bool.TryParse(val, out var b) ? b : val == "1";

OW4 [🟡] MapDevice + MapChannel 硬编码设备名

位置: 第 91, 113 行

Category = "硬盘录像机", Group = "视频设备"
Category = "摄像机", Group = "视频设备"

修复 — 提取为常量:

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 行 TypeDidPtztypeAppStreamId 等字段无注释说明其含义和取值范围。

修复 — 添加 XML 注释:

/// <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 行

From = from.ToString("yyyy-MM-dd HH:mm:ss"),
To = to.ToString("yyyy-MM-dd HH:mm:ss"),

from/toDateTime.MinValueB8 路由默认值)时,发送 "0001-01-01 00:00:00" 给 MC4——MC4 将认为这是有效日期过滤。

修复:

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 行 Mc4TreeNodeMc4PointValueMc4AlarmQuery 等 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 行

await client.PostAsync("/api/central/alarm/confirm", ...);  // 无 resp.EnsureSuccessStatusCode()

MC4 返回非 200 时静默失败,调用者认为确认成功。

修复:

var resp = await client.PostAsync(...);
resp.EnsureSuccessStatusCode();

11. Adapters.MC4/Mc4AuthHelper.cs — 1 项

MA1 [🟡] _needMd5 只获取一次

位置: 第 46-57 行

if (!_needMd5.HasValue) { /* 仅首次调用时查 conf/get */ }

如果 MC4 服务重启并更改了加密配置(encrypt true↔false),适配器不会重新检测。但实际场景中 MC4 不会在运行时切换加密模式,风险极低。

修复 — 加一个 Token 刷新计数器,每 N 次重新获取 conf/get(N=10 即可)。


12. Adapters.Kms/KmsAdapter.cs — 1 项

KM1 [] OpenerIds 缩进不齐

位置: 第 296 行

                    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 [🟠] SyncAllDevicesAsyncFlattenTree 在两个层次定义

位置: 第 152-184 行 这两个函数在 InitializeAllAsync 回调的闭包作用域中定义为局部函数。如果未来需要在其他地方调用(如 A3 手动触发),将不可用。

修复 — 提取为私有静态方法或迁移到 Core:

// 新建 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 行

builder.Services.AddSwaggerGen();  // 无 XML 注释选项

所有 Minimal API 端点没有 Swagger 描述,调用者必须参考外部文档。

修复:

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 行路由注册代码。

修复 — 按模块拆分为扩展方法:

// 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 改为:

app.MapHealthEndpoints(registry);
app.MapDeviceEndpoints(registry);
// ...

PR4 [🟡] 配置缺少验证

位置: 第 53-83 行 app.Configuration.GetSection("Owl").Get<List<OwlConfig>>() 可能在配置格式错误时返回 null,直接 foreach 正常运作但无错误提示。

修复 — 加空值检查和警告:

var owlList = app.Configuration.GetSection("Owl").Get<List<OwlConfig>>();
if (owlList == null || !owlList.Any()) Console.WriteLine("[Gateway] WARNING: 未配置 Owl 适配器");

PR5 [🟡] appsettings.json 凭证明文

位置: appsettings.json

"NodeToken": "changeme",
"Password": "your_owl_password",
"ClientSecret": "your_client_secret"

修复 — 生产环境注入:

"NodeToken": null,  // 生产环境由 SECMPS_GATEWAY_TOKEN 环境变量注入
"Password": null,   // 生产环境由 OWL_PASSWORD 注入
"ClientSecret": null // 生产环境由 KMS_CLIENT_SECRET 注入

Program.cs:

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>:

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 都在 GetAuthenticatedClientAsyncnew HttpClient { BaseAddress = new Uri(_baseUrl) }。每个请求创建一个新的 HttpClient 实例,无法复用连接池。

修复 — 传入 IHttpClientFactory:

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 字段:

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 路由拆分。