From f0add6e0b911ae1953d15ea440c07b959b4c04af Mon Sep 17 00:00:00 2001 From: g82tt Date: Sun, 17 May 2026 02:48:18 +0800 Subject: [PATCH] =?UTF-8?q?IntegrationGateway=E8=AF=A6=E7=BB=86=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/设计文档/对接网关设计文档.md | 703 +++++++++++++++++++++++++++++++ 1 file changed, 703 insertions(+) create mode 100644 doc/设计文档/对接网关设计文档.md diff --git a/doc/设计文档/对接网关设计文档.md b/doc/设计文档/对接网关设计文档.md new file mode 100644 index 0000000..13e6cdb --- /dev/null +++ b/doc/设计文档/对接网关设计文档.md @@ -0,0 +1,703 @@ +# IntegrationGateway 对接网关详细设计文档 + +> **版本**: 1.0 +> **日期**: 2025-05-17 +> **基准**: SecMPS 整合方案 v3.0 +> **作者**: 架构组 + +--- + +## 1. 概述 + +IntegrationGateway 是 SecMPS 整合方案 v3.0 的核心组件,定位为 Vol.Pro 管理端与各物联子系统之间的**协议适配中间层**。网关对外提供统一 REST API,对内通过适配器模式对接异构子系统,实现"适配一次,多处复用"。 + +### 1.1 设计目标 + +| 目标 | 度量 | +|------|------| +| 适配器热插拔 | 新增子系统不改网关核心,仅加 Adapter 项目 | +| 故障隔离 | 任一适配器故障不影响其他适配器和网关注册 | +| 无状态部署 | 网关不存数据库,配置仅 NodeCode/Token/VolProUrl | +| 编译独立性 | `dotnet build` 0 错误,不依赖 Vol.Pro 运行时 | + +### 1.2 技术栈 + +| 层面 | 选型 | +|------|------| +| 运行时 | .NET 8 | +| Web 框架 | ASP.NET Core Minimal API | +| HTTP 客户端 | IHttpClientFactory + SocketsHttpHandler | +| 序列化 | System.Text.Json | +| 容器化 | Docker (可选) | + +--- + +## 2. 项目结构 + +``` +gateway/ +├── IntegrationGateway.sln +└── src/ + ├── IntegrationGateway.Core/ # 核心抽象(被所有项目引用) + │ ├── Abstractions/ # 7 个能力接口 + │ │ ├── IHasOwnDeviceTree.cs + │ │ ├── IHasFlatDevices.cs + │ │ ├── IHasPoints.cs + │ │ ├── IHasStreams.cs + │ │ ├── IHasAlarms.cs + │ │ ├── IHasRecordings.cs + │ │ └── IAcceptsMetadataPush.cs + │ ├── Models/ # 统一模型 + │ │ ├── StandardDevice.cs + │ │ ├── StandardAlarm.cs + │ │ ├── StandardRecording.cs + │ │ ├── DeviceTreeNode.cs + │ │ ├── PointValue.cs + │ │ ├── StreamUrls.cs + │ │ ├── PagedResult.cs + │ │ ├── AdapterCapabilities.cs + │ │ └── MetadataChangeSet.cs + │ └── Infrastructure/ # 基础设施 + │ ├── AdapterRegistry.cs # 适配器注册中心 + │ ├── RateLimiter.cs # 令牌桶限流器 + │ └── GatewayClientFactory.cs # HTTP 客户端工厂 + │ + ├── IntegrationGateway.Adapters.Owl/ # Owl 适配器 + │ ├── OwlAdapter.cs # 实现 IHasFlatDevices + IHasStreams + │ └── OwlAuthHelper.cs # RSA 加密登录 + │ + ├── IntegrationGateway.Adapters.MC4/ # MC4.0 适配器 + │ ├── Mc4Adapter.cs # 实现 IHasOwnDeviceTree + IHasPoints + IHasAlarms + │ └── Mc4AuthHelper.cs # Token 认证 + │ + └── IntegrationGateway.Host/ # 宿主(启动项目) + ├── Program.cs # 路由注册 + 适配器初始化 + └── appsettings.json # 适配器连接配置 +``` + +### 2.1 依赖关系 + +``` +Host → Adapters.Owl → Core +Host → Adapters.MC4 → Core +Host → Core +``` + +适配器项目不互相引用,Core 项目零外部依赖(仅 Microsoft.Extensions.*)。 + +--- + +## 3. 核心接口体系 + +### 3.1 接口总览 + +```csharp +namespace IntegrationGateway.Core.Abstractions +{ + /// 所有适配器必须实现的基础接口 + public interface IGatewayAdapter + { + string AdapterCode { get; } // "Owl:main" / "MC4:31ku" + string DisplayName { get; } // 人类可读名称 + AdapterCapabilities Capabilities { get; } // 能力声明 + Task InitializeAsync(); // 懒加载初始化 + Task HealthCheckAsync(); // 健康检查 + } + + /// 扁平设备列表(Owl/门禁/道闸) + public interface IHasFlatDevices : IGatewayAdapter + { + Task> GetDevicesAsync(int page, int size, string? keyword); + } + + /// 自有对象树(MC4.0) + public interface IHasOwnDeviceTree : IGatewayAdapter + { + Task> GetObjectTreeAsync(); + } + + /// 实时点位值(MC4.0 动环) + public interface IHasPoints : IGatewayAdapter + { + Task> GetRealtimeValuesAsync(string sourceDeviceId); + Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value); + } + + /// 视频流(Owl) + public interface IHasStreams : IGatewayAdapter + { + Task GetLiveUrlAsync(string channelId); + Task GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end); + Task PtzControlAsync(string channelId, string direction, float speed); + Task PtzStopAsync(string channelId); + Task GetSnapshotAsync(string channelId); + } + + /// 告警(MC4.0 + Owl AI可选) + public interface IHasAlarms : IGatewayAdapter + { + Task> GetAlarmsAsync( + int page, int size, DateTime from, DateTime to, + string? level = null, string? state = null); + Task ConfirmAlarmAsync(string alarmId); + Task EndAlarmAsync(string alarmId); + } + + /// 录像回放(Owl) + public interface IHasRecordings : IGatewayAdapter + { + Task> GetRecordingsAsync( + string channelId, DateTime start, DateTime end, int page, int size); + } + + /// 元数据回写(Owl 设备改名等) + public interface IAcceptsMetadataPush : IGatewayAdapter + { + Task PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes); + } +} +``` + +### 3.2 接口设计原则 + +- **显式优于隐式**:每个接口明确声明一种能力,适配器按需实现,网关通过 `is` 检查自动发现路由 +- **异步优先**:所有方法返回 `Task`/`Task`,避免阻塞线程 +- **统一分页**:`PagedResult` 统一 page/size 语义,适配器内部完成 skip/limit 转换 +- **弹性 Extra**:`Dictionary` 承载适配器特有属性,不污染核心模型 + +### 3.3 适配器能力矩阵 + +``` + Owl MC4.0 门禁(未来) +IGatewayAdapter ✅ ✅ ✅ +IHasOwnDeviceTree - ✅ - +IHasFlatDevices ✅ - ✅ +IHasPoints - ✅ - +IHasStreams ✅ - - +IHasAlarms ⚠️ ✅ ✅ +IHasRecordings ✅ - - +IAcceptsMetadata ✅ - ⚠️ +``` + +### 3.4 接口扩展规则 + +新增子系统时: +1. 如果现有接口能覆盖 → 直接实现对应接口,零网关改动 +2. 如果现有接口不覆盖 → Core 中新增接口(如 `IHasFaceRecognition`),不能改已有接口签名 +3. 新增能力接口后 → Controller 加一个 `if (adapter is INewFeature)` 分支即可 + +--- + +## 4. 统一模型设计 + +### 4.1 StandardDevice + +```csharp +public class StandardDevice +{ + public int DeviceId { get; set; } // Vol.Pro 侧主键(同步后回填) + public string AdapterCode { get; set; } = ""; // "Owl:main" + public string SourceId { get; set; } = ""; // 子系统原始ID (GB28181编码 / MC4 sid) + public string Name { get; set; } = ""; // 设备名称 + public string Category { get; set; } = ""; // 摄像机/温湿度变送器/... + public string Group { get; set; } = ""; // 视频设备/IoT设备/... + public bool IsParent { get; set; } // 是否有子设备 + public string? ParentSourceId { get; set; } // 父设备SourceId(层级关系) + public bool IsOnline { get; set; } // 在线状态 + public string? IpAddress { get; set; } // IP地址 + public int? Port { get; set; } // 端口 + public Dictionary? Extra { get; set; } // 适配器扩展JSON +} +``` + +### 4.2 字段映射规则(字段分治) + +| StandardDevice | base_device | 写入策略 | +|:---|---|:---:| +| Name | DeviceName | 仅首次 | +| Category | DeviceCategory | 仅首次 | +| Group | DeviceGroup | 仅首次 | +| IsOnline | IsOnline | 每次覆盖 | +| IsParent | IsParent | 每次覆盖 | +| ParentSourceId | ParentDeviceId | 每次覆盖(解析映射) | +| IpAddress | IpAddress | 每次覆盖 | +| Port | Port | 每次覆盖 | +| Extra | ExtraData | 每次覆盖 | +| AdapterCode + SourceId | (AdapterCode, SourceId) | 联合唯一键 | + +### 4.3 DeviceTreeNode(对象树) + +```csharp +public class DeviceTreeNode +{ + public int Id { get; set; } // MC4.0 原始ID + public string SourceId { get; set; } = ""; // 转换为 string 的源ID + public string Name { get; set; } = ""; // 节点名称 + public int Type { get; set; } // 1=区域, 2=设备 + public int ObjectType { get; set; } // MC4.0 对象类型 + public string? Tag { get; set; } // 标签(温湿度/烟雾/门磁...) + public Dictionary? Option { get; set; } // 扩展属性 + public List Children { get; set; } = new(); +} +``` + +### 4.4 PagedResult + +```csharp +public class PagedResult +{ + public List Items { get; set; } = new(); + public int Total { get; set; } + public int Page => 0; // 由调用方设置 + public int Size => 0; +} +``` + +### 4.5 其他模型 + +``` +StreamUrls → { WsFlv, HttpFlv, Hls, WebRtc, Rtmp, Rtsp } +StandardAlarm → { AlarmId, DeviceId, AdapterCode, Level, Title, Content, OccurTime, Status, ... } +StandardRecording → { Id, ChannelId, StartedAt, EndedAt, Duration, FilePath, Size } +PointValue → { SourceDeviceId, PointIndex, Value, UpdateTime, Interval } +MetadataChangeSet → { Name?, Category?, Group?, Extra? } +AdapterCapabilities → { HasObjectTree, HasPoints, HasStreams, HasAlarms, HasRecordings, AcceptsControl, AcceptsMetadataPush } +``` + +--- + +## 5. 基础设施设计 + +### 5.1 AdapterRegistry + +```csharp +public class AdapterRegistry +{ + private readonly List _adapters = new(); + + public void Register(IGatewayAdapter adapter) => _adapters.Add(adapter); + + public async Task InitializeAllAsync() + { + // 并行初始化,单个失败不影响其他 + await Task.WhenAll(_adapters.Select(a => Task.Run(async () => + { + try { await a.InitializeAsync(); } + catch (Exception ex) { Log.Error($"Adapter {a.AdapterCode} init failed: {ex.Message}"); } + }))); + } + + public IReadOnlyList All => _adapters.AsReadOnly(); + + public T? FindByCode(string adapterCode) where T : class, IGatewayAdapter + => _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode && a is T) as T; + + public IGatewayAdapter? FindByCode(string adapterCode) + => _adapters.FirstOrDefault(a => a.AdapterCode == adapterCode); +} +``` + +**设计要点**: +- 列表存储,O(1) 注册,O(n) 查找(n≤5,可接受) +- 初始化失败不回滚,适配器标记为离线 +- 网关启动时通过 `POST /register` 上报 `AdapterTypes` 给 Vol.Pro + +### 5.2 RateLimiter(令牌桶) + +```csharp +public class RateLimiter +{ + private readonly SemaphoreSlim _semaphore; + private readonly int _tokensPerSecond; + + public RateLimiter(int tokensPerSecond) + { + _tokensPerSecond = tokensPerSecond; + _semaphore = new SemaphoreSlim(tokensPerSecond, tokensPerSecond); + } + + public async Task WaitAsync(CancellationToken ct = default) + { + await _semaphore.WaitAsync(ct); + _ = Task.Run(async () => + { + await Task.Delay(1000 / _tokensPerSecond); + _semaphore.Release(); + }); + } +} +``` + +**配置策略**: +| 适配器 | QPS 限制 | 原因 | +|--------|:---:|------| +| Owl | 5 | 按文档推荐 | +| MC4.0 | 2 | MC4.0 API 限频 | + +### 5.3 HttpClient 工厂 + +```csharp +// Program.cs 注册 +builder.Services.AddHttpClient("VolPro", c => +{ + c.BaseAddress = new Uri("http://localhost:9100"); + c.DefaultRequestHeaders.Add("Accept", "application/json"); + c.Timeout = TimeSpan.FromSeconds(30); +}).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler +{ + PooledConnectionLifetime = TimeSpan.FromMinutes(5), + MaxConnectionsPerServer = 10 +}); +``` + +**策略**: +- 命名 HttpClient(`"VolPro"` 用于调 Vol.Pro,适配器内部自行创建 AuthenticatedClient) +- 连接池复用,5 分钟生命周期 +- 超时 30 秒,防止第三方 API 慢响应阻塞 + +--- + +## 6. 认证与安全 + +### 6.1 网关注册认证(A1-A4) + +``` +网关持有: NodeCode + NodeToken(管理端分配) +注册流程: + 1. 网关 → POST /api/gateway/register { nodeCode, token, ... } + 2. Vol.Pro 查询 gateway_nodes WHERE NodeCode = req.NodeCode + - 存在 → 比对 NodeToken + - 不存在 → NodeToken 验证通过后 Insert + 3. 认证失败 → 401 + 4. 成功后返回 NodeId + 设备列表 +``` + +### 6.2 Owl 认证(RSA 加密) + +``` +1. GET /login/key → 获取 RSA 公钥 (Base64) +2. 用公钥加密 { username, password } → Base64 +3. POST /login { data: } → 获取 JWT Token +4. Token 有效期: 3 天 +5. 后续请求: Authorization: Bearer +``` + +**安全要点**: +- Token 内存缓存,不落盘 +- Token 过期前 1 小时自动刷新(懒刷新策略) +- 认证失败不清除缓存 → 重试 3 次后 `Invalidate()` + +### 6.3 MC4.0 认证 + +``` +1. POST /api/central/auth/conf/get → 获取临时 Token +2. Token 有效期: 8 小时(保守估计) +3. 后续请求: header["token"] = +``` + +### 6.4 网关内部接口(B 组) + +B 组接口供管理端或 Vol.Pro 内部调用,认证方式: +- 内网部署:IP 白名单(Simple) +- 外网部署:共享 Secret Key(HMAC 签名),Phase 4 实现 + +--- + +## 7. 路由设计 + +### 7.1 网关主动接口(调 Vol.Pro,不暴露给外部) + +网关内部通过 `VolProClient` 调用,不在 Minimal API 中注册路由。 + +``` +POST {VolProBaseUrl}/api/gateway/register A1 +POST {VolProBaseUrl}/api/gateway/heartbeat A2 +POST {VolProBaseUrl}/api/gateway/sync/devices A3 +POST {VolProBaseUrl}/api/gateway/sync/alarms A4 +``` + +### 7.2 网关暴露接口(B 组,供管理端调用) + +```csharp +// Program.cs 路由注册伪代码 +app.MapGet("/api/gateway/health", async (AdapterRegistry reg) => { + var results = await Task.WhenAll(reg.All.Select(async a => new { + a.AdapterCode, a.DisplayName, + Healthy = await a.HealthCheckAsync(), + a.Capabilities + })); + return Results.Ok(results); +}); + +app.MapGet("/api/gateway/devices", async (string adapter, int page, int size, string? keyword, AdapterRegistry reg) => { + var a = reg.FindByCode(adapter); + if (a == null) return Results.NotFound(); + return Results.Ok(await a.GetDevicesAsync(page, size, keyword)); +}); + +app.MapGet("/api/gateway/tree", async (string adapter, AdapterRegistry reg) => { + var a = reg.FindByCode(adapter); + if (a == null) return Results.NotFound(); + return Results.Ok(await a.GetObjectTreeAsync()); +}); + +app.MapGet("/api/gateway/streams/{adapter}/{deviceId}/live", async (string adapter, string deviceId, AdapterRegistry reg) => { + var a = reg.FindByCode(adapter); + if (a == null) return Results.NotFound(); + return Results.Ok(await a.GetLiveUrlAsync(deviceId)); +}); + +app.MapPost("/api/gateway/streams/{adapter}/{deviceId}/ptz", async (string adapter, string deviceId, PtzRequest req, AdapterRegistry reg) => { + var a = reg.FindByCode(adapter); + if (a == null) return Results.NotFound(); + if (req.Action == "stop") await a.PtzStopAsync(deviceId); + else await a.PtzControlAsync(deviceId, req.Direction, req.Speed); + return Results.Ok(); +}); + +app.MapGet("/api/gateway/alarms/{adapter}", async (string adapter, int page, int size, DateTime from, DateTime to, AdapterRegistry reg) => { + var a = reg.FindByCode(adapter); + if (a == null) return Results.NotFound(); + return Results.Ok(await a.GetAlarmsAsync(page, size, from, to)); +}); + +app.MapPost("/api/gateway/alarms/{adapter}/{alarmId}/confirm", async (string adapter, string alarmId, AdapterRegistry reg) => { + var a = reg.FindByCode(adapter); + if (a == null) return Results.NotFound(); + await a.ConfirmAlarmAsync(alarmId); + return Results.Ok(); +}); + +// ... 更多 B 组接口 +``` + +### 7.3 路由设计原则 + +- **适配器参数前置**:所有 B 组接口第一个路径参数都是 `{adapter}`,通过注册中心查找 +- **能力检查懒加载**:请求到达时才检查适配器是否实现对应接口 +- **404 语义**:适配器不存在或未实现对应能力 → 404(而非 500) +- **统一错误格式**:`{ "error": "ADAPTER_NOT_FOUND", "message": "Adapter 'xxx' not found" }` + +--- + +## 8. 同步流程设计 + +### 8.1 网关启动同步 + +``` +1. 网关启动 → 加载配置 (NodeCode, Token, VolProBaseUrl) +2. 初始化适配器 (并行: Owl + MC4) +3. POST /api/gateway/register → 获取 NodeId + 已有设备列表 +4. 按 AdapterCode 分流已有设备 → 适配器对比差异 +5. 各适配器发现子设备 → 构建 StandardDevice[] 列表 +6. POST /api/gateway/sync/devices → Vol.Pro Upsert 设备 +7. 开启 15s 心跳定时器 +``` + +### 8.2 手动全量同步(B3) + +``` +管理端 → POST /api/gateway/devices/sync?adapter=MC4:31ku +网关: + 1. 找到 Mc4Adapter + 2. (MC4) GetObjectTree() → 解析区域+设备 → StandardDevice[] + 3. (Owl) GetDevices() + GetChannels() → StandardDevice[] + 4. POST /api/gateway/sync/devices → Vol.Pro + 5. 返回 { added, updated, removed } +``` + +### 8.3 MC4.0 同步(FullReplace 模式) + +``` +MC4.0 对象树遍历: + type=1 (区域) → 名称匹配 warehouse_regions → 新建或绑区 + type=2 (设备) → Upsert base_device + - 首次写入: DeviceName/Category/Group/ExtraData 全量 + - 后续同步: 仅更新 IsOnline/ExtraData/ParentDeviceId + type=2 子节点 → parentSourceId 解析 → ParentDeviceId +``` + +### 8.4 Owl 同步(Merge 模式) + +``` +Owl 设备列表遍历: + GET /devices → NVR 设备 (IsParent=是, DeviceGroup=视频设备) + GET /channels → 通道 (ParentDeviceId=NVR, IsParent=否) + 通道额外写 video_channel 扩展记录 (OwlStreamApp/OwlStreamName) +``` + +--- + +## 9. 错误处理 + +### 9.1 错误码规范 + +| HTTP 状态码 | error_code | 场景 | +|:---:|------|------| +| 200 | - | 正常 | +| 400 | `INVALID_PARAMETER` | 参数缺失或格式错误 | +| 401 | `UNAUTHORIZED` | Token 验证失败 | +| 404 | `ADAPTER_NOT_FOUND` | 适配器不存在 | +| 404 | `CAPABILITY_NOT_SUPPORTED` | 适配器未实现该能力 | +| 502 | `UPSTREAM_ERROR` | 第三方 API 返回错误 | +| 503 | `ADAPTER_OFFLINE` | 适配器健康检查失败 | +| 504 | `UPSTREAM_TIMEOUT` | 第三方 API 超时 | +| 500 | `INTERNAL_ERROR` | 网关内部错误 | + +### 9.2 适配器日志 + +```csharp +public void Log(string adapterCode, string operation, string detail, Exception? ex = null) +{ + var level = ex != null ? "ERROR" : "INFO"; + var msg = $"[{DateTime.UtcNow:O}] [{level}] [{adapterCode}] {operation}: {detail}"; + if (ex != null) msg += $"\n{ex}"; + Console.WriteLine(msg); +} +``` + +--- + +## 10. 配置管理 + +### 10.1 appsettings.json 结构 + +```json +{ + "Logging": { + "LogLevel": { "Default": "Information" } + }, + "Owl": { + "BaseUrl": "http://localhost:15123", + "Username": "admin", + "Password": "your_password" + }, + "MC4": { + "BaseUrl": "http://localhost:3000" + }, + "Gateway": { + "VolProBaseUrl": "http://localhost:9100", + "NodeCode": "gw-31ku", + "NodeToken": "xxxxxxxxxx", + "HeartbeatIntervalSec": 15, + "AdapterInitTimeoutSec": 30 + } +} +``` + +### 10.2 环境变量覆盖(Docker 部署) + +```bash +GATEWAY__OWL__BASEURL=http://192.168.1.100:15123 +GATEWAY__OWL__PASSWORD=prod_password +GATEWAY__GATEWAY__NODETOKEN=prod_token +``` + +### 10.3 配置验证 + +启动时验证必填项: +``` +Gateway.VolProBaseUrl ✓ +Gateway.NodeCode ✓ (长度 1-50) +Gateway.NodeToken ✓ (长度 8-100) +Owl.BaseUrl ✓ (格式 http(s)://...) +Owl.Username ✓ +Owl.Password ✓ +MC4.BaseUrl ✓ +``` + +--- + +## 11. 部署方案 + +### 11.1 单机部署 + +```bash +cd gateway +dotnet publish src/IntegrationGateway.Host -c Release -o publish +cd publish +./IntegrationGateway.Host --urls http://0.0.0.0:5100 +``` + +### 11.2 Docker 部署 + +```dockerfile +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +COPY publish/ . +ENV ASPNETCORE_URLS=http://+:5100 +ENTRYPOINT ["dotnet", "IntegrationGateway.Host.dll"] +``` + +### 11.3 双实例部署 + +``` +实例A: gw-31ku :5100 → MC4.0(31号库) + Owl(仓库视频) +实例B: gw-11ku :5101 → MC4.0(11号库) + 海康ISC(门禁) +``` + +### 11.4 运维命令 + +```bash +# 健康检查 +curl http://localhost:5100/api/gateway/health + +# 手动同步 +curl -X POST "http://localhost:5100/api/gateway/devices/sync?adapter=MC4:31ku" + +# 查看日志 +docker logs -f integration-gateway +``` + +--- + +## 12. 性能指标 + +| 指标 | 目标值 | 说明 | +|------|:---:|------| +| 网关启动时间 | < 5s | 含适配器并行初始化 | +| 设备同步吞吐 | 100 设备/s | 含 HTTP 往返 | +| 实时取流响应 | < 500ms | 从请求到返回流地址 | +| 内存占用 | < 100MB | 空载状态 | +| 并发连接数 | 50 | 同时处理的管理端请求 | + +--- + +## 13. 测试策略 + +### 13.1 单元测试(Phase 4) + +``` +IntegrationGateway.Core.Tests/ +├── AdapterRegistryTests # 注册/查找/初始化 +├── RateLimiterTests # 令牌桶行为 +└── ModelSerializationTests # JSON 序列化往返 +``` + +### 13.2 集成测试(需子系统 Mock) + +``` +适配器层 Mock → 验证 Controller 路由分发 +Vol.Pro API Mock → 验证网关注册/心跳/同步流程 +``` + +### 13.3 边界测试 + +| 场景 | 预期行为 | +|------|----------| +| Owl 离线 | HealthCheck → false, 设备 IsOnline 不变 | +| MC4.0 超时 | 返回 504, 不影响 Owl 适配器 | +| 并发取流 | RateLimiter 排队, 不丢请求 | +| 配置错误 | 启动时校验失败, 拒绝启动 | + +--- + +## 14. 版本历史 + +| 版本 | 日期 | 变更 | +|------|------|------| +| 1.0 | 2025-05-17 | 初版详细设计 | + +--- + +> **下一步**: Phase 0 Day 1 按本设计实施网关项目骨架。