diff --git a/doc/整合方案/SecMPS_最终整合方案_v3.0.md b/doc/整合方案/SecMPS_最终整合方案_v3.0.md new file mode 100644 index 0000000..6a06ef7 --- /dev/null +++ b/doc/整合方案/SecMPS_最终整合方案_v3.0.md @@ -0,0 +1,531 @@ +# SecMPS 整合方案(最终版):IntegrationGateway + 统一设备管理 + +> **版本**: v3.0 +> **日期**: 2026-05-16 +> **状态**: 待实施 +> **基准**: 基于 v2.0 + Owl/ZLMediaKit 双文档验证 + 14 项修正 + +--- + +## 一、总体架构 + +``` +前端层 + web.vite 管理端(设备管理+网关管理) warehouse 大屏(Map/Live/IoT/Alarm) + | HTTP REST | HTTP REST + SignalR + v v +Vol.Pro 后端 (api_sqlsugar) + DeviceManagerController / GatewayNodeController / SignalR Hubs + Quartz: SyncDevicesJob / RealtimePollJob / AlarmPollJob + 数据库: 6 张表 (base_device / video_channel / video_record / iot_devicedata / iot_alarm / gateway_nodes) + | 注册/下发设备/心跳/同步数据 + v +IntegrationGateway 实例A (:5100) IntegrationGateway 实例B (:5101) + NodeCode: gw-31ku NodeCode: gw-11ku + Adapters: MC4 / Owl Adapters: MC4 / HikvisionISC + | HTTP | HTTP + v v +MC4.0 (:3000) Owl (:15123) MC4.0 (:3000) 海康ISC (:80) + +注: Owl 管理端口为 15123,ZLMediaKit 由 Owl 内部管理(:8000),网关不直接接触 +``` + +### 核心设计原则 + +- **网关无状态**:配置仅 NodeCode/Token/VolProUrl,挂了重装即恢复 +- **AdapterCode 双段标识**:`"MC4:31ku"` 区分同类型多实例,格式 `{AdapterType}:{InstanceName}`,InstanceName 仅字母数字下划线 +- **DeviceGroup 路由**:基类表用字典字段决定适配器和行为,不依赖扩展表 +- **ExtraData JSON**:所有适配器特有字段存入 ExtraData,新增适配器不增表 +- **心跳机制**:网关 15s 心跳,Vol.Pro 超 30s 级联设备离线 +- **字段分治**:网关字段(ExtraData/IsOnline/IsParent/ParentDeviceId)同步覆盖,管理员字段(DeviceName/Category/Location/MapModelId)首次写入后不再覆盖 + +--- + +## 二、网关架构(方案 C+) + +### 2.1 网关注册与心跳流程 + +``` +管理端: gateway_nodes 表新增 → 生成 NodeCode + Token +网关配置: { NodeCode, Token, VolProUrl } +网关启动 → POST /register { nodeCode,token,adapterTypes,baseUrl } +Vol.Pro 校验 Token: + NodeCode 匹配 → Upsert: 更新 AdapterTypes/BaseUrl/IsOnline=在线 → 返回已有 NodeId + NodeCode 不匹配且 Token 验证通过 → 插入新记录 → 返回新 NodeId + 验证失败 → 401 +响应: { gatewayNodeId, devices: [base_device WHERE GatewayNodeId=当前] } +网关按 AdapterCode 分流 → Adapter 连接第三方 → 发现子设备 +网关 → POST /sync/devices → Vol.Pro 写入/更新 base_device(首次写全量,后续仅更新网关字段) +网关每 15s → POST /heartbeat { nodeCode, token } +Vol.Pro Job: IsOnline=在线 且 LastHeartbeat < now-30s → IsOnline=离线 → 级联: base_device.IsOnline=离线 WHERE GatewayNodeId=该节点 +``` + +### 2.2 网关配置 + +```json +{ + "VolProBaseUrl": "http://localhost:9100", + "NodeCode": "gw-31ku", + "NodeToken": "xxxxxxxxxx" +} +``` + +AdapterCode 格式规范:`{AdapterType}:{InstanceName}`,例如 `"MC4:31ku"`、`"Owl:main"`。 +AdapterType = 网关注册的 Adapter 类名(字母数字),InstanceName = 网关实例名(字母数字下划线),分隔符 `:`。 + +### 2.3 适配器能力矩阵 + +| 接口 | Owl | MC4.0 | 门禁(未来) | +|------|:---:|:-----:|:----------:| +| IHasOwnDeviceTree | ❌ | ✅ | ❌ | +| IHasFlatDevices | ✅ | ❌ | ✅ | +| IHasPoints | ❌ | ✅ | ❌ | +| IHasStreams | ✅ | ❌ | ❌ | +| IHasAlarms | ⚠️(AI事件可选) | ✅ | ✅ | +| IAcceptsMetadataPush | ✅ | ❌ | ⚠️ | + +> Owl PTZ 限制:仅支持 continuous(方向移动) + stop(停止),不支持预设位/绝对定位/相对定位。ONVIF PTZ 未实现。 + +### 2.4 双向同步引擎 + +| 方向 | 说明 | MC4.0 | Owl | +|------|------|-------|-----| +| PullToVolPro | 第三方→Vol.Pro | FullReplace | Merge | +| PushToSource | Vol.Pro→第三方 | 告警确认/控制 | 元数据/PTZ | +| Bidirectional | 先写第三方再更新本地 | 告警确认 | — | + +### 2.5 对接 API 规范 + +网关与 Vol.Pro 之间有两组接口,调用方向不同。 + +#### A. 网关 → Vol.Pro(网关主动调用) + +| # | 接口 | 说明 | +|---|------|------| +| A1 | `POST /api/gateway/register` | 网关启动注册(Upsert逻辑),获取所管设备列表 | +| A2 | `POST /api/gateway/heartbeat` | 心跳(每 15s) | +| A3 | `POST /api/gateway/sync/devices` | 上送设备数据(新增/状态变更),仅更新网关字段 | +| A4 | `POST /api/gateway/sync/alarms` | 上送告警数据 | + +**A1 注册** — 认证: NodeToken, 逻辑: Upsert + +``` +Request: { nodeCode, token, adapterTypes, baseUrl } +Response: { nodeId, devices: [ 当前网关的顶层设备 ] } +Error: 401 认证失败(Token 错误或 NodeCode 不存在且 Token 无效) +``` + +**A2 心跳** — 认证: NodeToken + +``` +Request: { nodeCode, token } +Response: { status: "ok" } +``` + +**A3 设备同步** — 认证: NodeToken + +``` +Request: { nodeCode, token, devices: [{ adapterCode, sourceId, name, category, group, + isParent, parentSourceId, isOnline, ipAddress, port, extraData }] } +Response: { added, updated, removed } +``` + +> 字段分治规则:首次入库(DeviceId==0)写全量;已有记录仅更新 IsOnline、IsParent、ParentDeviceId、ExtraData、LastSyncTime、IpAddress、Port。DeviceName/DeviceCategory/DeviceGroup/Location/MapModelId 仅在首次入库时写入。 +> parentSourceId 解析:同步前按 (AdapterCode, SourceId) 批量查询已有 DeviceId 映射表,将 parentSourceId 转为 ParentDeviceId。 + +**A4 告警同步** — 认证: NodeToken + +``` +Request: { nodeCode, token, alarms: [{ sourceAlarmId, deviceSourceId, adapterCode, + level, desc, value, startTime }] } +Response: { added } +``` + +--- + +#### B. Vol.Pro / 管理端 → 网关(查询与控制) + +| # | 接口 | 说明 | +|---|------|------| +| B1 | `GET /api/gateway/health` | 网关及所有适配器状态 | +| B2 | `GET /api/gateway/devices?adapter=&page=&size=` | 设备列表 | +| B3 | `POST /api/gateway/devices/sync?adapter=` | 手动触发全量同步 | +| B4 | `GET /api/gateway/realtime/{adapter}/{deviceId}` | 实时点位值 | +| B5 | `POST /api/gateway/realtime/{adapter}/control` | 反向控制 | +| B6a | `GET /api/gateway/streams/{adapter}/{id}/live` | 实时取流 | +| B6b | `GET /api/gateway/streams/{adapter}/{id}/playback?start=&end=` | 回放取流(HLS VOD) | +| B7 | `POST /api/gateway/streams/{adapter}/{id}/ptz` | 云台控制(仅方向移动+停止) | +| B8 | `GET /api/gateway/alarms/{adapter}?from=&to=&page=&size=` | 告警查询 | +| B9 | `POST /api/gateway/alarms/{adapter}/{alarmId}/confirm` | 告警确认(写回第三方+更新本地State) | + +> B 组接口由管理端或 Vol.Pro 后端直接调用网关,认证方式为内网直连或网关侧 IP 白名单。 +> B6a 实时取流 → Owl `POST /channels/:id/play`;B6b 回放取流 → Owl `GET /recordings/channels/:cid/index.m3u8?start_ms=&end_ms=` +> B7 仅支持 `continuous`(方向移动: up/down/left/right/zoom_in/zoom_out) + `stop`,不支持预设位 +> B9 确认第三方成功后,同步更新本地 iot_alarm.State='已确认' + +--- + +## 三、数据模型(6 张表) + +### 3.1 区域表 warehouse_regions(现有) + +层级: warehouse_regions(区域) → warehouse_devicepoint(点位) → base_device(设备) + +| 字段 | 说明 | +|------|------| +| Id | int PK | +| RegionName | nvarchar(255) | +| ParentId | int? (自引用树) | + +### 3.2 网关节点表 gateway_nodes + +| 字段 | 类型 | 说明 | +|------|------|------| +| NodeId | INT AUTO_INCREMENT | 网关节点ID | +| NodeCode | NVARCHAR(50) | 网关唯一编码 | +| NodeName | NVARCHAR(100) | 网关名称 | +| NodeToken | NVARCHAR(100) | 认证令牌 | +| AdapterTypes | NVARCHAR(200) | 适配器类型(网关上报) | +| BaseUrl | NVARCHAR(200) | 网关地址(网关上报) | +| LastHeartbeat | DATETIME | 上次心跳时间 | +| IsOnline | NVARCHAR(20) | 在线状态(字典: 在线/离线) | +| Enable | NVARCHAR(20) | 启用状态(字典: 启用/禁用) | + +### 3.3 统一设备主表 base_device + +| 字段 | 类型 | 说明 | +|------|------|------| +| DeviceId | INT AUTO_INCREMENT | Vol.Pro内部ID | +| DeviceName | NVARCHAR(100) | 设备名称(管理员字段) | +| AdapterCode | NVARCHAR(50) | "MC4:31ku"(类型:实例) | +| SourceId | NVARCHAR(100) | 源系统设备ID | +| DeviceCategory | NVARCHAR(50) | 设备种类(字典) | +| DeviceGroup | NVARCHAR(20) | 设备分组(字典) | +| PointId | INT? | 所属点位ID | +| GatewayNodeId | INT? | 所属网关节点ID | +| IsParent | NVARCHAR(20) | 是否父设备(字典: 是/否) | +| ParentDeviceId | INT? | 父设备自引用 | +| IsOnline | NVARCHAR(20) | 在线状态(网关字段) | +| MapModelId | NVARCHAR(100) | VgoMap模型ID(管理员字段) | +| MapModelScale | FLOAT | | +| MapModelRotation | NVARCHAR(100) | | +| ExtraData | TEXT | ★ 适配器扩展JSON(网关字段) | +| LastSyncTime | DATETIME | 上次同步时间 | +| Enable | NVARCHAR(20) | 启用状态(字典: 启用/禁用) | + +唯一约束: (AdapterCode, SourceId) + +> 网关字段(同步覆盖):ExtraData/IsOnline/IsParent/ParentDeviceId/IpAddress/Port/LastSyncTime +> 管理员字段(首次写入后不覆盖):DeviceName/DeviceCategory/DeviceGroup/PointId/Location/Lat/Lng/MapModelId/MapModelScale/MapModelRotation + +### 3.4 DeviceGroup 分组规则 + +| DeviceGroup | 网关适配器 | 前端按钮组 | 同步模式 | +|:---:|------|------|:---:| +| 视频设备 | OwlAdapter → IHasStreams | 实时预览/云台/回放/快照 | Merge | +| IoT设备 | Mc4Adapter → IHasPoints | 实时数据/控制/告警 | FullReplace | +| 门禁设备 | AccessAdapter | 远程开门/记录/告警 | Merge | +| 道闸设备 | BarrierAdapter | 抬杆/落杆/记录 | Merge | +| 报警设备 | AlarmAdapter | 查看告警/布防撤防 | Merge | + +### 3.5 ExtraData 格式示例 + +```json +// 摄像机 (Owl) +{ "owlDeviceId": "gb_xxx", "protocol": "GB28181", "manufacturer": "海康" } + +// 温湿度变送器 (MC4子设备) +{ "mc4DeviceId": 1001, "pointIndex": 0, "unit": "℃", "isControlPoint": false } + +// 空调控制器 (MC4子设备) +{ "mc4DeviceId": 1002, "pointIndex": 2, "unit": null, "isControlPoint": true } +``` + +### 3.6 层级示例 + +``` +gateway_nodes: gw-31ku + +warehouse_regions → warehouse_devicepoint → base_device + +例: 厂区 → 新库区 → 31号库房(点位) → 设备 + +base_device (PointId=点位ID, GatewayNodeId=gw-31ku.NodeId): + NVR-01 (DeviceCategory=硬盘录像机, DeviceGroup=视频设备, IsParent=是) + ├── 通道01 (ParentDeviceId=NVR, video_channel.DeviceId=通道01.DeviceId, OwlStreamApp="rtp") + ├── 通道02 (ParentDeviceId=NVR, video_channel.DeviceId=通道02.DeviceId, OwlStreamApp="rtp") + 东北角高位摄像机 (DeviceCategory=摄像机, DeviceGroup=视频设备) + 动环采集器 (DeviceCategory=动环采集器, DeviceGroup=IoT设备, IsParent=是) + ├── 温湿度变送器 (ParentDeviceId=采集器, ExtraData={pointIndex:0,unit:"℃"}) + ├── 空调控制器 (ParentDeviceId=采集器, ExtraData={pointIndex:2,isControlPoint:true}) + └── 紧急报警按钮 (DeviceGroup=报警设备, ParentDeviceId=采集器) + +video_channel 表(通道Owl流信息): + { ChannelId=1, DeviceId=通道01.DeviceId(→base_device), OwlStreamApp="rtp", HasPtz=1 } + { ChannelId=2, DeviceId=通道02.DeviceId(→base_device), OwlStreamApp="rtp", HasPtz=0 } + +> 通道 = base_device 子记录(名称/在线/层级) + video_channel 扩展记录(流地址/云台能力/录像模式) +> video_channel 的 DeviceId 指向通道自身的 base_device.DeviceId(不是 NVR 的 DeviceId) +> video_channel 的 OwlStreamApp/OwlStreamName/SnapshotUrl 为缓存,首次取流后写入,后续优先读缓存 +``` + +--- + +## 四、Vol.Pro 同步接口(新增适配器零改动) + +```csharp +// POST /api/gateway/sync/devices +public async Task SyncDevices(string nodeCode, List devices) +{ + var node = await _db.gateway_nodes.FirstAsync(n => n.NodeCode == nodeCode); + + // ★ 批量查询已有设备映射表 (用于 parentSourceId 解析) + var existingIds = await _db.base_device + .Where(x => x.GatewayNodeId == node.NodeId) + .ToDictionaryAsync(x => (x.AdapterCode, x.SourceId), x => x.DeviceId); + + foreach (var d in devices) + { + var key = (d.AdapterCode, d.SourceId); + existingIds.TryGetValue(key, out var existingId); + var entity = existingId > 0 + ? await _db.base_device.FindAsync(existingId) + : new base_device { AdapterCode = d.AdapterCode, SourceId = d.SourceId }; + + bool isNew = entity.DeviceId == 0; + + // ★ 解析 parentSourceId → ParentDeviceId + int? parentDeviceId = null; + if (!string.IsNullOrEmpty(d.ParentSourceId)) + { + existingIds.TryGetValue((d.AdapterCode, d.ParentSourceId), out var pid); + if (pid > 0) parentDeviceId = pid; + } + + // 仅首次入库写管理员字段 + if (isNew) + { + entity.DeviceName = d.Name; + entity.DeviceCategory = d.Category; + entity.DeviceGroup = d.Group; + } + + // 每次同步写网关字段 + entity.IsOnline = d.IsOnline ? "在线" : "离线"; + entity.IsParent = d.IsParent ? "是" : "否"; + entity.ParentDeviceId = parentDeviceId; + entity.ExtraData = d.ExtraDataJson; + entity.GatewayNodeId = node.NodeId; + entity.IpAddress = d.IpAddress; + entity.Port = d.Port; + entity.LastSyncTime = DateTime.UtcNow; + + if (isNew) _db.base_device.Add(entity); + else _db.base_device.Update(entity); + } + await _db.SaveChangesAsync(); +} +``` + +--- + +## 五、管理端设备操作集成 + +> **设计决策**: 不再需要独立的设备管理页面。Vol.Pro 框架自带三级主从表显示能力(warehouse_regions → warehouse_devicepoint → base_device),可直接在框架生成的 base_device 列表页面中嵌入操作按钮,按 DeviceGroup 动态渲染。 + +### 5.1 操作按钮矩阵 + +| DeviceGroup | 操作按钮 | 说明 | +|:---:|------|------| +| 视频设备 | 实时预览 / 云台控制(仅方向键) / 查看回放 / 获取快照 / 同步通道 | 全部通过网关 B 组接口代理到 Owl | +| IoT设备 | 查看实时数据 / 设备控制 / 刷新点位 / 查看告警 | 通过网关 B4/B5 代理到 MC4.0 | +| 门禁设备 | 远程开门 / 查看记录 / 查看告警 | Phase 3 接入海康ISC后启用 | +| 道闸设备 | 抬杆 / 落杆 / 查看记录 | Phase 3 接入 | +| 报警设备 | 查看告警 / 布防撤防 | Phase 3 接入 | + +### 5.2 前端按钮嵌入方案 + +框架生成的 base_device 列表页面的"操作"列默认只有"编辑/删除"。在 **base_deviceController Partial** 中扩展前端分组逻辑,替换默认操作列为自定义渲染。 + +```javascript +// web.vite/src/extension/warehouse/base_device.jsx 或 views/warehouse/base_device/components/ActionColumn.vue +const actionMap = { + '视频设备': VideoDeviceActions, // 实时预览/云台/回放/快照/同步通道 + 'IoT设备': IoTDeviceActions, // 实时数据/控制/刷新/告警 + '门禁设备': AccessDeviceActions, // 远程开门/记录/告警 + '道闸设备': BarrierDeviceActions,// 抬杆/落杆/记录 + '报警设备': AlarmDeviceActions, // 告警/布防撤防 +} + +// 操作列模板中根据 row.deviceGroup 动态渲染对应组件 + +``` + +**要点**: +- 框架"操作"列通过自定义插槽替换,不修改框架生成的 Vue 文件本体 +- 组件放在 `views/warehouse/base_device/components/` 下,防代码生成覆盖 +- 五个分组组件各自独立(~80-150 行),新增分组仅加一个文件 + +### 5.3 前端操作组件清单 + +| 文件 | 大小 | 说明 | +|------|:---:|------| +| `VideoDeviceActions.vue` | ~120行 | 预览/云台/回放/快照按钮组,预览弹窗内嵌 Jessibuca 播放器(或