diff --git a/api_sqlsugar/VolPro.WebApi/Download/ExcelExport/20260516/字典明细20260516100837.xlsx b/api_sqlsugar/VolPro.WebApi/Download/ExcelExport/20260516/字典明细20260516100837.xlsx new file mode 100644 index 0000000..1ba63af Binary files /dev/null and b/api_sqlsugar/VolPro.WebApi/Download/ExcelExport/20260516/字典明细20260516100837.xlsx differ diff --git a/doc/db_init.sql b/doc/db_init.sql index a80030c..6c51b81 100644 --- a/doc/db_init.sql +++ b/doc/db_init.sql @@ -1,168 +1,165 @@ - -- ============================================ --- SecMPS v2.0 数据库建表脚本 +-- SecMPS v3.0 数据库建表脚本(6张表) -- 数据库: gljs_main +-- 扩展表已合并到 Base_Device.ExtraData(JSON) -- ============================================ USE gljs_main; +-- ============================================ -- 1. 统一设备主表 -CREATE TABLE IF NOT EXISTS Base_Device ( - DeviceId CHAR(36) NOT NULL PRIMARY KEY, - DeviceName NVARCHAR(100) NOT NULL, - AdapterCode NVARCHAR(50) NOT NULL, - SourceId NVARCHAR(100) NOT NULL, - DeviceCategory INT NOT NULL DEFAULT 1, - DeviceType NVARCHAR(50), - RegionId INT NULL, - IsParent TINYINT NOT NULL DEFAULT 0, - ParentDeviceId CHAR(36) NULL, - IsOnline TINYINT NOT NULL DEFAULT 0, - IpAddress NVARCHAR(50), - Port INT, - Location NVARCHAR(200), - Lat DOUBLE, - Lng DOUBLE, - MapModelId NVARCHAR(100), - MapModelScale FLOAT DEFAULT 1.0, - MapModelRotation NVARCHAR(100), - ExtraData TEXT, - LocalOverrides TEXT, - SyncVersion BIGINT DEFAULT 0, - LastSyncTime DATETIME, - Enable TINYINT DEFAULT 1, - Remark NVARCHAR(500), - CreateID INT, - Creator NVARCHAR(50), - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - ModifyID INT, - Modifier NVARCHAR(50), - ModifyDate DATETIME, - UNIQUE INDEX IX_Base_Device_Adapter_Source (AdapterCode, SourceId), - INDEX IX_Base_Device_RegionId (RegionId), - INDEX IX_Base_Device_ParentId (ParentDeviceId) -); +-- ExtraData(JSON) 承载所有适配器特有字段 +-- DeviceGroup 路由到正确的网关Adapter和前端按钮组 +-- ============================================ +DROP TABLE IF EXISTS base_device; +CREATE TABLE base_device ( + DeviceId INT AUTO_INCREMENT COMMENT '设备ID', + DeviceName NVARCHAR(100) NOT NULL COMMENT '设备名称', + AdapterCode NVARCHAR(50) NOT NULL COMMENT '来源适配器(类型:实例)', + SourceId NVARCHAR(100) NOT NULL COMMENT '源系统设备ID', + DeviceCategory NVARCHAR(50) NOT NULL COMMENT '设备种类(数据字典:门磁/空调/智能断路器/人行道闸/车辆道闸/485钥匙柜/网络钥匙柜/紧急报警按钮/红外报警器/门禁一体机/除湿_恒湿机/空调控制器/烟雾报警器/气体报警器/温湿度变送器/摄像机/硬盘录像机/动环采集器)', + DeviceGroup NVARCHAR(20) NOT NULL COMMENT '设备分组(数据字典:视频设备/IoT设备/门禁设备/道闸设备/报警设备)', + PointId INT NULL COMMENT '所属点位ID', + GatewayNodeId INT NULL COMMENT '所属网关节点ID', + IsParent NVARCHAR(20) NOT NULL DEFAULT '否' COMMENT '是否父设备(数据字典:是/否)', + ParentDeviceId INT NULL COMMENT '父设备ID(自引用,子设备挂父设备下)', + IsOnline NVARCHAR(20) NOT NULL DEFAULT '离线' COMMENT '在线状态(数据字典:在线/离线)', + IpAddress NVARCHAR(50) COMMENT 'IP地址', + Port INT COMMENT '端口', + Location NVARCHAR(200) COMMENT '安装位置', + Lat DOUBLE COMMENT '纬度', + Lng DOUBLE COMMENT '经度', + MapModelId NVARCHAR(100) COMMENT '三维地图模型ID', + MapModelScale FLOAT DEFAULT 1.0 COMMENT '模型缩放比例', + MapModelRotation NVARCHAR(100) COMMENT '模型旋转角度(JSON)', + ExtraData TEXT COMMENT '适配器扩展数据JSON(Owl/MC4/门禁字段均存于此)', + LastSyncTime DATETIME COMMENT '上次同步时间', + Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)', + Remark NVARCHAR(500) COMMENT '备注', + CreateID INT COMMENT '创建人ID', + Creator NVARCHAR(50) COMMENT '创建人', + CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + ModifyID INT COMMENT '修改人ID', + Modifier NVARCHAR(50) COMMENT '修改人', + ModifyDate DATETIME COMMENT '修改时间', + PRIMARY KEY (DeviceId), + INDEX IX_Sync (AdapterCode, SourceId), + INDEX IX_Point (PointId), + INDEX IX_Parent (ParentDeviceId), + INDEX IX_Gateway (GatewayNodeId), + INDEX IX_Group (DeviceGroup) +) COMMENT '统一设备主表'; --- 2. 视频设备扩展表 -CREATE TABLE IF NOT EXISTS Device_Video_Ext ( - ExtId CHAR(36) NOT NULL PRIMARY KEY, - DeviceId CHAR(36) NOT NULL, - OwlDeviceId NVARCHAR(64) NOT NULL, - Protocol INT DEFAULT 1, - Manufacturer NVARCHAR(100), - Model NVARCHAR(100), - ChannelCount INT DEFAULT 0, - OwlStatus NVARCHAR(500), - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE INDEX IX_VideoExt_Owl (OwlDeviceId), - INDEX IX_VideoExt_Device (DeviceId) -); +-- ============================================ +-- 2. 视频通道表 +-- DeviceId(INT) → base_device.DeviceId +-- ============================================ +DROP TABLE IF EXISTS video_channel; +CREATE TABLE video_channel ( + ChannelId INT AUTO_INCREMENT COMMENT '通道记录ID', + OwlChannelId NVARCHAR(64) NOT NULL COMMENT 'Owl系统通道ID', + DeviceId INT NOT NULL COMMENT '关联Base_Device设备ID', + OwlStreamApp NVARCHAR(50) COMMENT 'Owl流应用名', + OwlStreamName NVARCHAR(100) COMMENT 'Owl流名称', + HasPtz TINYINT DEFAULT 0 COMMENT '是否支持云台', + HasRecording TINYINT DEFAULT 0 COMMENT '是否支持录像', + RecordMode INT DEFAULT 0 COMMENT '录像模式', + SnapshotUrl NVARCHAR(500) COMMENT '快照地址', + CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (ChannelId), + INDEX IX_Device (DeviceId), + INDEX IX_Owl (OwlChannelId) +) COMMENT '视频通道表'; --- 3. 视频通道表 -CREATE TABLE IF NOT EXISTS Video_Channel ( - ChannelId CHAR(36) NOT NULL PRIMARY KEY, - OwlChannelId NVARCHAR(64) NOT NULL, - DeviceId CHAR(36) NOT NULL, - ChannelName NVARCHAR(100) NOT NULL, - ChannelNo INT DEFAULT 0, - OwlStreamApp NVARCHAR(50), - OwlStreamName NVARCHAR(100), - HasPtz TINYINT DEFAULT 0, - HasRecording TINYINT DEFAULT 0, - RecordMode INT DEFAULT 0, - IsOnline TINYINT DEFAULT 0, - SnapshotUrl NVARCHAR(500), - Location NVARCHAR(200), - Lat DOUBLE, - Lng DOUBLE, - Enable TINYINT DEFAULT 1, - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE INDEX IX_Channel_Owl (OwlChannelId), - INDEX IX_Channel_Device (DeviceId) -); +-- ============================================ +-- 3. 录像记录表 +-- ChannelId(INT) → video_channel.ChannelId +-- ============================================ +DROP TABLE IF EXISTS video_record; +CREATE TABLE video_record ( + RecordId INT AUTO_INCREMENT COMMENT '录像记录ID', + ChannelId INT NOT NULL COMMENT '关联通道ID', + OwlRecordId INT NOT NULL COMMENT 'Owl录像记录ID', + App NVARCHAR(50) COMMENT '应用名', + Stream NVARCHAR(100) COMMENT '流ID', + StartedAt DATETIME NOT NULL COMMENT '录像开始时间', + EndedAt DATETIME COMMENT '录像结束时间', + Duration DOUBLE DEFAULT 0 COMMENT '录像时长(秒)', + FilePath NVARCHAR(500) COMMENT '文件路径', + FileSize BIGINT DEFAULT 0 COMMENT '文件大小(字节)', + CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (RecordId), + INDEX IX_Channel (ChannelId), + INDEX IX_Time (StartedAt) +) COMMENT '录像记录表'; --- 4. 录像记录表 -CREATE TABLE IF NOT EXISTS Video_Record ( - RecordId CHAR(36) NOT NULL PRIMARY KEY, - ChannelId CHAR(36) NOT NULL, - OwlRecordId INT NOT NULL, - App NVARCHAR(50), - Stream NVARCHAR(100), - StartedAt DATETIME NOT NULL, - EndedAt DATETIME, - Duration DOUBLE DEFAULT 0, - FilePath NVARCHAR(500), - FileSize BIGINT DEFAULT 0, - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX IX_Record_Channel (ChannelId), - INDEX IX_Record_Time (StartedAt) -); +-- ============================================ +-- 4. 设备数据归档表 +-- DeviceId(INT) → base_device.DeviceId +-- ============================================ +DROP TABLE IF EXISTS iot_devicedata; +CREATE TABLE iot_devicedata ( + DataId INT AUTO_INCREMENT COMMENT '数据记录ID', + DeviceId INT NOT NULL COMMENT '关联设备ID(子设备/点位)', + PointValue DOUBLE COMMENT '点位数值', + UpdateTime DATETIME NOT NULL COMMENT '数据更新时间', + `Interval` INT DEFAULT 0 COMMENT '采集间隔(毫秒)', + ArchiveType INT DEFAULT 1 COMMENT '归档类型(1小时/2日)', + CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (DataId), + INDEX IX_Device (DeviceId), + INDEX IX_Time (CreateDate) +) COMMENT '设备数据归档表'; --- 5. IoT设备扩展表 -CREATE TABLE IF NOT EXISTS Device_IoT_Ext ( - ExtId CHAR(36) NOT NULL PRIMARY KEY, - DeviceId CHAR(36) NOT NULL, - Mc4DeviceId INT NOT NULL, - ObjectType INT, - Tag NVARCHAR(100), - ParentId INT, - Mc4Option NVARCHAR(500), - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE INDEX IX_IoTExt_Mc4 (Mc4DeviceId), - INDEX IX_IoTExt_Device (DeviceId) -); +-- ============================================ +-- 5. 告警记录表(通用) +-- DeviceId(INT) → base_device.DeviceId +-- ============================================ +DROP TABLE IF EXISTS iot_alarm; +CREATE TABLE iot_alarm ( + AlarmId INT AUTO_INCREMENT COMMENT '告警ID', + SourceAlarmId NVARCHAR(100) NOT NULL COMMENT '源系统告警ID', + DeviceId INT NOT NULL COMMENT '关联设备ID', + AlarmType INT DEFAULT 0 COMMENT '告警类型', + AlarmLevel NVARCHAR(20) DEFAULT '提示' COMMENT '告警等级(数据字典:提示/普通/重要/紧急)', + AlarmDesc NVARCHAR(500) COMMENT '告警描述', + AlarmValue DOUBLE COMMENT '触发值', + StartTime DATETIME NOT NULL COMMENT '告警开始时间', + EndTime DATETIME COMMENT '告警结束时间', + ConfirmTime DATETIME COMMENT '确认时间', + ConfirmUser NVARCHAR(50) COMMENT '确认人', + State NVARCHAR(20) DEFAULT '未确认' COMMENT '状态(数据字典:未确认/已确认/已结束)', + AdapterCode NVARCHAR(50) COMMENT '来源适配器', + CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (AlarmId), + INDEX IX_Device (DeviceId), + INDEX IX_Source (SourceAlarmId), + INDEX IX_Time (StartTime), + INDEX IX_Level (AlarmLevel) +) COMMENT '告警记录表'; --- 6. 设备点位表 -CREATE TABLE IF NOT EXISTS IoT_DevicePoint ( - PointId CHAR(36) NOT NULL PRIMARY KEY, - DeviceId CHAR(36) NOT NULL, - Mc4DeviceId INT NOT NULL, - PointIndex INT NOT NULL, - PointType INT, - PointTag NVARCHAR(100), - PointName NVARCHAR(100) NOT NULL, - PointDesc NVARCHAR(200), - Unit NVARCHAR(50), - IsControlPoint TINYINT DEFAULT 0, - Mc4Option NVARCHAR(500), - Enable TINYINT DEFAULT 1, - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE INDEX IX_Point_Mc4 (Mc4DeviceId, PointIndex), - INDEX IX_Point_Device (DeviceId) -); - --- 7. 设备数据归档表(仅存快照,实时不入库) -CREATE TABLE IF NOT EXISTS IoT_DeviceData ( - DataId CHAR(36) NOT NULL PRIMARY KEY, - DeviceId CHAR(36) NOT NULL, - PointId CHAR(36) NOT NULL, - PointValue DOUBLE, - UpdateTime DATETIME NOT NULL, - `Interval` INT DEFAULT 0, - ArchiveType INT DEFAULT 1, - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX IX_Data_Device (DeviceId), - INDEX IX_Data_Time (CreateDate) -); - --- 8. 告警记录表 -CREATE TABLE IF NOT EXISTS IoT_Alarm ( - AlarmId CHAR(36) NOT NULL PRIMARY KEY, - Mc4AlarmId NVARCHAR(64) NOT NULL, - DeviceId CHAR(36), - PointId CHAR(36), - AlarmType INT DEFAULT 0, - AlarmLevel INT DEFAULT 1, - AlarmDesc NVARCHAR(500), - AlarmValue DOUBLE, - StartTime DATETIME NOT NULL, - EndTime DATETIME, - ConfirmTime DATETIME, - ConfirmUser NVARCHAR(50), - State INT DEFAULT 1, - AdapterCode NVARCHAR(50), - CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE INDEX IX_Alarm_Mc4 (Mc4AlarmId), - INDEX IX_Alarm_Device (DeviceId), - INDEX IX_Alarm_Time (StartTime) -); +-- ============================================ +-- 6. 网关节点注册表 +-- ============================================ +DROP TABLE IF EXISTS gateway_nodes; +CREATE TABLE gateway_nodes ( + NodeId INT AUTO_INCREMENT COMMENT '网关节点ID', + NodeCode NVARCHAR(50) NOT NULL COMMENT '网关唯一编码', + NodeName NVARCHAR(100) NOT NULL COMMENT '网关名称', + NodeToken NVARCHAR(100) NOT NULL COMMENT '认证令牌', + AdapterTypes NVARCHAR(200) COMMENT '支持的适配器类型(网关上报)', + BaseUrl NVARCHAR(200) COMMENT '网关自身地址(网关上报)', + LastHeartbeat DATETIME COMMENT '上次心跳时间', + IsOnline NVARCHAR(20) DEFAULT '离线' COMMENT '在线状态(数据字典:在线/离线)', + Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)', + Remark NVARCHAR(500) COMMENT '备注', + CreateID INT COMMENT '创建人ID', + Creator NVARCHAR(50) COMMENT '创建人', + CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + ModifyID INT COMMENT '修改人ID', + Modifier NVARCHAR(50) COMMENT '修改人', + ModifyDate DATETIME COMMENT '修改时间', + PRIMARY KEY (NodeId), + UNIQUE INDEX IX_Code (NodeCode), + INDEX IX_Online (IsOnline) +) COMMENT '网关节点注册表'; diff --git a/doc/对接文档/网关与Vol.Pro对接API手册.md b/doc/对接文档/网关与Vol.Pro对接API手册.md new file mode 100644 index 0000000..e6f47e3 --- /dev/null +++ b/doc/对接文档/网关与Vol.Pro对接API手册.md @@ -0,0 +1,748 @@ +# SecMPS 网关与 Vol.Pro 对接 API 手册 + +> **版本**: v1.0 +> **日期**: 2026-05-16 +> **基于**: SecMPS_最终整合方案_v3.0.md +> **接口数量**: 13 个(A 组 4 个 + B 组 9 个) + +--- + +## 1. 引言 + +### 1.1 目标与范围 + +本手册定义 IntegrationGateway(网关)与 Vol.Pro 后端之间所有 HTTP API 的接口规范。供网关开发、Vol.Pro 后端开发、前端开发和 AI Agent 编码时参考。 + +### 1.2 基本约定 + +| 约定 | 说明 | +|------|------| +| 请求方法 | A 组使用 **POST**,B 组使用 **GET / POST** | +| 数据格式 | **JSON**,`Content-Type: application/json` | +| 字符编码 | **UTF-8** | +| 认证方式 | A 组: `NodeToken`(请求体中携带),B 组: 内网直连,无认证 | +| 网关端口 | **5100**(默认) | +| Vol.Pro 端口 | **9100**(默认) | + +### 1.3 响应格式 + +**成功**: +```json +{ + // 具体数据,见各接口定义 +} +``` +HTTP Status: 200 + +**失败**: +```json +{ + "message": "错误描述" +} +``` +HTTP Status: 400(业务错误)/ 401(认证失败)/ 500(服务器错误) + +--- + +## 2. A 组接口:网关 → Vol.Pro + +> 调用方: 网关 +> 接收方: Vol.Pro 后端 +> 端口: 9100 + +--- + +### 2.1 A1 — 网关注册 + +网关启动时调用,上报身份与能力,获取所管理的顶层设备列表。 + +- **Endpoint**: `POST /api/gateway/register` +- **认证**: NodeToken +- **逻辑**: Upsert — NodeCode 匹配则更新,不匹配则插入 + +#### 请求体 + +```json +{ + "nodeCode": "gw-31ku", + "token": "a1b2c3d4e5f6", + "adapterTypes": "MC4,Owl", + "baseUrl": "http://192.168.1.100:5100" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| nodeCode | string | ✅ | 网关唯一编码,管理端生成 | +| token | string | ✅ | 认证令牌,管理端生成 | +| adapterTypes | string | ✅ | 支持的适配器类型,逗号分隔,例如 "MC4,Owl" | +| baseUrl | string | ✅ | 网关自身地址(含端口),供管理端回调 | + +#### 响应体(成功) + +```json +{ + "nodeId": 1, + "devices": [ + { + "deviceId": 10, + "deviceName": "动环采集器", + "adapterCode": "MC4:31ku", + "sourceId": "1001", + "deviceCategory": "动环采集器", + "deviceGroup": "IoT设备", + "isParent": "是", + "parentSourceId": null, + "isOnline": "在线", + "ipAddress": "10.0.1.100", + "port": 3000, + "extraData": { + "mc4DeviceId": 1001 + } + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| nodeId | int | 网关在 Vol.Pro 中的 NodeId | +| devices | array | 当前网关负责的顶层设备列表(base_device 中 GatewayNodeId=本网关 且 ParentDeviceId IS NULL 的记录) | +| devices[].deviceId | int | Vol.Pro 内部设备ID | +| devices[].adapterCode | string | 双段标识 "MC4:31ku" | +| devices[].sourceId | string | 第三方原始设备ID | +| devices[].extraData | object | 适配器扩展数据 | + +#### 响应体(失败) + +```json +// HTTP 401 +{ + "message": "认证失败" +} +``` + +#### 请求示例 + +```bash +curl -X POST http://localhost:9100/api/gateway/register \ + -H "Content-Type: application/json" \ + -d '{"nodeCode":"gw-31ku","token":"a1b2c3d4e5f6","adapterTypes":"MC4,Owl","baseUrl":"http://192.168.1.100:5100"}' +``` + +--- + +### 2.2 A2 — 网关心跳 + +网关每 15 秒调用,Vol.Pro 更新 LastHeartbeat 和 IsOnline。 + +- **Endpoint**: `POST /api/gateway/heartbeat` +- **认证**: NodeToken +- **频率**: 每 15 秒 + +#### 请求体 + +```json +{ + "nodeCode": "gw-31ku", + "token": "a1b2c3d4e5f6" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| nodeCode | string | ✅ | 网关唯一编码 | +| token | string | ✅ | 认证令牌 | + +#### 响应体 + +```json +{ + "status": "ok", + "serverTime": "2026-05-16 15:30:00" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| status | string | 固定 "ok" | +| serverTime | string | Vol.Pro 当前时间,供网关校时 | + +#### 请求示例 + +```bash +curl -X POST http://localhost:9100/api/gateway/heartbeat \ + -H "Content-Type: application/json" \ + -d '{"nodeCode":"gw-31ku","token":"a1b2c3d4e5f6"}' +``` + +--- + +### 2.3 A3 — 设备数据同步 + +网关发现新设备或设备状态变更后,上送到 Vol.Pro。 + +- **Endpoint**: `POST /api/gateway/sync/devices` +- **认证**: NodeToken +- **字段分治**: 首次入库写全量,已有记录仅更新网关字段 + +#### 请求体 + +```json +{ + "nodeCode": "gw-31ku", + "token": "a1b2c3d4e5f6", + "devices": [ + { + "adapterCode": "MC4:31ku", + "sourceId": "1001", + "name": "动环采集器", + "category": "动环采集器", + "group": "IoT设备", + "isParent": true, + "parentSourceId": null, + "isOnline": true, + "ipAddress": "10.0.1.100", + "port": 3000, + "extraData": { + "mc4DeviceId": 1001 + } + }, + { + "adapterCode": "MC4:31ku", + "sourceId": "1001_0", + "name": "温湿度变送器", + "category": "温湿度变送器", + "group": "IoT设备", + "isParent": false, + "parentSourceId": "1001", + "isOnline": true, + "ipAddress": null, + "port": null, + "extraData": { + "mc4DeviceId": 1001, + "pointIndex": 0, + "unit": "℃" + } + } + ] +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| nodeCode | string | ✅ | 网关编码 | +| token | string | ✅ | 认证令牌 | +| devices | array | ✅ | 设备列表 | +| devices[].adapterCode | string | ✅ | 双段标识 | +| devices[].sourceId | string | ✅ | 第三方原始ID | +| devices[].name | string | ✅ | 设备名称(仅首次写入) | +| devices[].category | string | ✅ | 设备种类(仅首次写入) | +| devices[].group | string | ✅ | 设备分组(仅首次写入) | +| devices[].isParent | bool | ✅ | 是否父设备 | +| devices[].parentSourceId | string? | | 父设备第三方ID,Vol.Pro 解析为 ParentDeviceId | +| devices[].isOnline | bool | ✅ | 在线状态 | +| devices[].ipAddress | string? | | IP 地址 | +| devices[].port | int? | | 端口 | +| devices[].extraData | object | | 适配器扩展数据 | + +**字段分治规则**: +- **首次入库**(DeviceId=0): 所有字段写入 +- **已有记录**: 仅更新 isOnline、isParent、parentDeviceId(解析后)、extraData、ipAddress、port、lastSyncTime +- deviceName、category、group、pointId、location、lat/lng、mapModelId 仅在首次写入 + +#### 响应体 + +```json +{ + "added": 2, + "updated": 0, + "removed": 0 +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| added | int | 新增设备数 | +| updated | int | 更新设备数 | +| removed | int | 删除设备数(FullReplace 模式下可能 >0) | + +#### 请求示例 + +```bash +curl -X POST http://localhost:9100/api/gateway/sync/devices \ + -H "Content-Type: application/json" \ + -d '{"nodeCode":"gw-31ku","token":"a1b2c3d4e5f6","devices":[...]}' +``` + +--- + +### 2.4 A4 — 告警数据同步 + +网关发现新告警后上送。 + +- **Endpoint**: `POST /api/gateway/sync/alarms` +- **认证**: NodeToken + +#### 请求体 + +```json +{ + "nodeCode": "gw-31ku", + "token": "a1b2c3d4e5f6", + "alarms": [ + { + "sourceAlarmId": "2183fda3-9e32-48ae-b433-f807cc81a237", + "deviceSourceId": "1001_0", + "adapterCode": "MC4:31ku", + "level": "重要", + "desc": "温度超限: [温湿度变送器][45℃]", + "value": 45.0, + "startTime": "2026-05-16 14:30:00" + } + ] +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| nodeCode / token | string | ✅ | 认证信息 | +| alarms | array | ✅ | 告警列表 | +| alarms[].sourceAlarmId | string | ✅ | 源系统告警ID(用于去重) | +| alarms[].deviceSourceId | string | ✅ | 告警所属设备的第三方ID | +| alarms[].adapterCode | string | ✅ | 双段标识 | +| alarms[].level | string | ✅ | 告警等级(字典: 提示/普通/重要/紧急) | +| alarms[].desc | string | ✅ | 告警描述 | +| alarms[].value | double | | 触发值 | +| alarms[].startTime | string | ✅ | 告警开始时间 "yyyy-MM-dd HH:mm:ss" | + +#### 响应体 + +```json +{ + "added": 1 +} +``` + +--- + +## 3. B 组接口:Vol.Pro / 管理端 → 网关 + +> 调用方: Vol.Pro 后端 / 管理端 / warehouse 端 +> 接收方: 网关 +> 端口: 5100 +> 认证: 内网直连(无认证) + +--- + +### 3.1 B1 — 健康检查 + +- **Endpoint**: `GET /api/gateway/health` + +#### 响应体 + +```json +{ + "gateway": "ok", + "adapters": { + "MC4:31ku": true, + "Owl:main": true + } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| gateway | string | 固定 "ok" | +| adapters | object | key=AdapterCode, value=true(在线)/false(离线) | + +#### 请求示例 + +```bash +curl http://localhost:5100/api/gateway/health +``` + +--- + +### 3.2 B2 — 设备列表 + +获取指定适配器下的设备列表(实时查询第三方)。 + +- **Endpoint**: `GET /api/gateway/devices` +- **适配器**: IHasFlatDevices + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| adapter | string | ✅ | AdapterCode,例如 "Owl:main" | +| page | int | | 页码,默认 1 | +| size | int | | 每页大小,默认 50 | +| keyword | string | | 名称/ID 模糊搜索 | + +#### 响应体 + +```json +{ + "items": [ + { + "sourceId": "gb_34020000001320000001", + "adapterCode": "Owl:main", + "name": "NVR-01", + "category": "硬盘录像机", + "group": "视频设备", + "isOnline": true, + "isParent": true, + "ipAddress": "192.168.1.100", + "port": 5060, + "channelCount": 4, + "extraData": { + "owlDeviceId": "gb_34020000001320000001", + "protocol": "GB28181", + "transport": "UDP" + } + } + ], + "total": 50 +} +``` + +--- + +### 3.3 B3 — 手动触发同步 + +管理端手动触发全量设备同步。 + +- **Endpoint**: `POST /api/gateway/devices/sync` +- **适配器**: 所有实现了 IHasFlatDevices 或 IHasOwnDeviceTree 的适配器 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| adapter | string | ✅ | AdapterCode | + +#### 响应体 + +```json +{ + "adapterCode": "Owl:main", + "added": 5, + "updated": 3, + "removed": 1, + "errors": [], + "startTime": "2026-05-16T15:30:00", + "endTime": "2026-05-16T15:30:05" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| adapterCode | string | 适配器编码 | +| added | int | 新增数 | +| updated | int | 更新数 | +| removed | int | 删除数 | +| errors | array | 错误信息列表 | +| startTime / endTime | string | 同步起止时间 | + +--- + +### 3.4 B4 — 实时点位值 + +获取设备所有点位的实时值。 + +- **Endpoint**: `GET /api/gateway/realtime/{adapter}/{deviceSourceId}` +- **适配器**: IHasPoints (MC4.0) + +#### 路径参数 + +| 参数 | 说明 | +|------|------| +| adapter | AdapterCode,例如 "MC4:31ku" | +| deviceSourceId | 设备在第三方系统中的 ID | + +#### 响应体 + +```json +{ + "deviceSourceId": "1001", + "points": [ + { + "pointIndex": 0, + "name": "温度", + "value": 26.5, + "unit": "℃", + "updateTime": "2026-05-16 15:30:00", + "isValid": true + }, + { + "pointIndex": 1, + "name": "湿度", + "value": 55.0, + "unit": "%", + "updateTime": "2026-05-16 15:30:00", + "isValid": true + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| deviceSourceId | string | 设备ID | +| points[].pointIndex | int | 点位索引 | +| points[].name | string | 点位名称 | +| points[].value | double | 当前值 | +| points[].unit | string | 单位 | +| points[].updateTime | string | 数据更新时间 | +| points[].isValid | bool | 数据是否有效(false=传感器异常) | + +--- + +### 3.5 B5 — 设备控制 + +反向控制设备(调空调温度、开关阀门等)。 + +- **Endpoint**: `POST /api/gateway/realtime/{adapter}/control` +- **适配器**: IHasPoints (MC4.0) + +#### 路径参数 + 请求体 + +```json +// POST /api/gateway/realtime/MC4:31ku/control +{ + "deviceSourceId": "1001", + "pointIndex": 2, + "value": 1 +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| deviceSourceId | string | ✅ | 第三方设备ID | +| pointIndex | int | ✅ | 点位索引 | +| value | double | ✅ | 设置值(开关: 0/1, 模拟量: 实际数值) | + +#### 响应体 + +```json +{ + "status": "sent" +} +``` + +> 注意: 响应仅表示指令已发送,不保证设备已执行。如需确认,应再次调 B4 查询实时值。 + +--- + +### 3.6 B6a — 实时取流 + +获取实时播放流地址。 + +- **Endpoint**: `GET /api/gateway/streams/{adapter}/{channelId}/live` +- **适配器**: IHasStreams (Owl) + +#### 响应体 + +```json +{ + "wsFlv": "ws://192.168.1.108/proxy/sms/rtp/gb_xxx.live.flv", + "httpFlv": "http://192.168.1.108/proxy/sms/rtp/gb_xxx.live.flv", + "hls": "http://192.168.1.108/proxy/sms/rtp/gb_xxx/hls.fmp4.m3u8", + "webrtc": "webrtc://192.168.1.108/proxy/sms/index/api/webrtc?app=rtp&stream=gb_xxx&type=play", + "rtmp": "rtmp://192.168.1.108:1935/rtp/gb_xxx", + "rtsp": "rtsp://192.168.1.108:554/rtp/gb_xxx" +} +``` + +| 协议 | 建议用途 | 延迟 | +|------|----------|:---:| +| wsFlv | Web 实时预览(首选) | <1s | +| httpFlv | 兼容旧浏览器 | 1-2s | +| hls | iOS Safari / 回放 | 3-5s | +| webrtc | 超低延迟场景 | <500ms | + +> 注意: GB28181 首次拉流有 1-3 秒 SIP 信令延迟。建议前端播放器在 3 秒内无画面时自动重试一次。 + +--- + +### 3.7 B6b — 回放取流 + +获取历史录像 HLS 播放地址。 + +- **Endpoint**: `GET /api/gateway/streams/{adapter}/{channelId}/playback` +- **适配器**: IHasStreams (Owl) + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| start | string | ✅ | 开始时间 "yyyy-MM-dd HH:mm:ss" | +| end | string | ✅ | 结束时间 "yyyy-MM-dd HH:mm:ss" | + +#### 响应体 + +```json +{ + "hls": "http://192.168.1.108/recordings/channels/gb_xxx/index.m3u8?start_ms=1714982400000&end_ms=1714982700000&token=xxx" +} +``` + +> 回放使用 HLS (VOD) 格式,Owl 将指定时间范围内的 MP4 片段动态拼接为 m3u8 播放列表。 + +--- + +### 3.8 B7 — 云台控制 + +控制摄像机云台转动。 + +- **Endpoint**: `POST /api/gateway/streams/{adapter}/{channelId}/ptz` +- **适配器**: IHasStreams (Owl) + +#### 请求体 + +```json +{ + "direction": "left", + "speed": 0.5 +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| direction | string | ✅ | **仅支持**: `up` / `down` / `left` / `right` / `zoom_in` / `zoom_out` / `stop` | +| speed | float | | 速度 0.0~1.0,默认 0.5 | + +> ⚠️ Owl 当前版本仅实现了方向移动 (`continuous`) 和停止 (`stop`),**不支持**预设位操作 (`preset/set/goto/remove`) 和绝对/相对定位。ONVIF PTZ 未实现。 + +#### 响应体 + +```json +{ + "status": "ok" +} +``` + +--- + +### 3.9 B8 — 告警查询 + +查询告警列表。 + +- **Endpoint**: `GET /api/gateway/alarms/{adapter}` +- **适配器**: IHasAlarms + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|:---:|------| +| from | string | ✅ | 开始时间 "yyyy-MM-dd HH:mm:ss" | +| to | string | ✅ | 结束时间 | +| page | int | | 页码,默认 1 | +| size | int | | 每页大小,默认 50 | +| confirmState | int | | MC4.0: 0=所有, 1=未确认, 2=已确认 | +| level | string | | 告警等级: 提示/普通/重要/紧急 | + +#### 响应体 + +```json +{ + "items": [ + { + "alarmId": "2183fda3-9e32-48ae-b433-f807cc81a237", + "deviceId": "1001", + "adapterCode": "MC4:31ku", + "level": "重要", + "title": "温度超限", + "content": "[温湿度变送器][45℃]", + "occurTime": "2026-05-16 14:30:00", + "status": "未确认", + "thresholdValue": 40.0, + "actualValue": 45.0 + } + ], + "total": 120 +} +``` + +--- + +### 3.10 B9 — 告警确认 + +确认告警,写回第三方系统。 + +- **Endpoint**: `POST /api/gateway/alarms/{adapter}/{alarmId}/confirm` +- **适配器**: IHasAlarms + +#### 路径参数 + +| 参数 | 说明 | +|------|------| +| adapter | AdapterCode | +| alarmId | 源系统告警 ID | + +#### 响应体 + +```json +{ + "status": "confirmed" +} +``` + +> Vol.Pro 在收到成功响应后,同步更新本地 `iot_alarm.State='已确认'` 和 `ConfirmTime=当前时间`。 + +--- + +## 4. 错误码对照表 + +| HTTP Status | 含义 | 说明 | +|:----------:|------|------| +| 200 | 成功 | 正常响应 | +| 400 | 请求错误 | 参数缺失或格式错误 | +| 401 | 认证失败 | A 组接口 NodeCode 或 Token 不正确 | +| 404 | 未找到 | 适配器不存在或设备不存在 | +| 500 | 服务器错误 | 网关或 Vol.Pro 内部异常 | +| 502 | 第三方错误 | 网关调用 Owl/MC4.0 失败 | + +--- + +## 5. 数据类型约定 + +| 类型 | JSON 示例 | 说明 | +|------|-----------|------| +| string | "hello" | 字符串 | +| int | 42 | 整数 | +| double | 26.5 | 浮点数 | +| bool | true / false | 布尔值 | +| object | { "key": "value" } | JSON 对象 | +| array | [ ... ] | JSON 数组 | +| datetime | "2026-05-16 15:30:00" | 日期时间字符串,格式 yyyy-MM-dd HH:mm:ss | + +--- + +## 6. 对接检查清单 + +网关开发和 Vol.Pro 开发完成后,按以下清单逐项验证: + +``` +□ A1: 网关启动 → 注册成功 → 返回 nodeId + 设备列表 +□ A2: 网关心跳 → Vol.Pro gateway_nodes.LastHeartbeat 更新 +□ A3: 首次同步 → 设备入库,字段分治规则正确(管理员字段不覆盖) +□ A3: 二次同步 → 仅网关字段更新,DeviceName 不变 +□ A3: parentSourceId → Vol.Pro 正确解析为 ParentDeviceId +□ A4: 告警上报 → iot_alarm 表有数据 +□ B1: 健康检查 → 返回所有适配器状态 +□ B2: 设备列表 → 分页 + 关键字搜索正常 +□ B3: 手动同步 → 返回 added/updated/removed 统计 +□ B4: 实时数据 → 返回点位值和更新时间 +□ B5: 设备控制 → 指令发送成功,设备实际响应 +□ B6a: 实时取流 → 返回 6 种协议地址,前端可播放 +□ B6b: 回放取流 → 返回 HLS 地址,前端可播放历史录像 +□ B7: 云台控制 → 方向键有效,stop 有效 +□ B8: 告警查询 → 分页 + 筛选正常 +□ B9: 告警确认 → 第三方确认成功 + Vol.Pro 本地 State 更新 +``` + +--- + +> **文档结束** diff --git a/doc/整合方案/SecMPS_整合方案_v2.0_最终评估报告.md b/doc/整合方案/SecMPS_整合方案_v2.0_最终评估报告.md new file mode 100644 index 0000000..ead8190 --- /dev/null +++ b/doc/整合方案/SecMPS_整合方案_v2.0_最终评估报告.md @@ -0,0 +1,290 @@ +# SecMPS 整合方案 — 最终评估报告 + +> **评估日期**: 2026-05-16 +> **评估对象**: SecMPS_最终整合方案_v2.0.md +> **参考依据**: Owl API 文档 / Owl GitHub / ZLMediaKit 官方文档 / ZLMediaKit GitHub / 数据字典设计 + +--- + +## 一、评估方法 + +以资深架构师经验,对整合方案进行三个维度的交叉验证: + +1. **方案自洽性**:章节间逻辑是否一致,API 定义 ↔ 数据模型 ↔ 同步策略是否闭环 +2. **对 Owl+ZLMediaKit 的准确性**:方案定义的接口和流程是否与 Owl v1.3.0 / ZLMediaKit master 实际行为匹配 +3. **工程可落地性**:是否所有细节都有明确定义,是否存在"开发时再说"的模糊地带 + +共发现 **14 个问题**,按严重程度分为 P0(阻塞实施)、P1(影响质量)、P2(优化项)。 + +--- + +## 二、P0 — 必须修复(4 项) + +### P0-1:第四章示例代码与 A3 接口规范矛盾 + +**问题描述**: +§四示例代码(第 263-265 行)在每次同步时写入 `DeviceName = d.Name` 和 `DeviceCategory = d.Category`。但 A3 接口规范明确说"网关不碰管理员字段,首次入库写全量,后续仅更新网关负责的列"。这导致管理员手动改过的设备名称每次同步后被网关覆盖。 + +**影响**:管理员的设备重命名操作会被同步覆盖,用户投诉 + +**解决方案**: +```csharp +// 区分首次入库与增量更新 +if (entity.DeviceId == 0) // 首次入库 +{ + entity.DeviceName = d.Name; + entity.DeviceCategory = d.Category; + entity.DeviceGroup = d.Group; +} +// 增量更新:只写网关负责的字段 +entity.IsOnline = d.IsOnline ? "在线" : "离线"; +entity.IsParent = d.IsParent ? "是" : "否"; +entity.ExtraData = d.ExtraDataJson; +entity.LastSyncTime = DateTime.UtcNow; +``` + +--- + +### P0-2:A3 设备同步中 `parentSourceId` → `ParentDeviceId` 解析链路缺失 + +**问题描述**: +A3 请求中设备携带 `parentSourceId: "1001"`(第三方 ID),但 Vol.Pro 需要 `ParentDeviceId`(本地主键)。从第三方 ID 到本地主键的映射逻辑完全未定义。 + +**影响**:子设备无法挂到父设备下,设备树断裂 + +**解决方案**: +同步前先做映射表查询: +```csharp +// 批量取出当前网关所有设备 +var allIds = await _db.base_device + .Where(x => x.GatewayNodeId == nodeId) + .ToDictionaryAsync(x => (x.AdapterCode, x.SourceId), x => x.DeviceId); + +// 解析 parentSourceId +int? parentDeviceId = null; +if (!string.IsNullOrEmpty(d.ParentSourceId)) + allIds.TryGetValue((d.AdapterCode, d.ParentSourceId), out var pid); + parentDeviceId = pid > 0 ? pid : null; + +entity.ParentDeviceId = parentDeviceId; +``` +补充到 §四 代码示例中。 + +--- + +### P0-3:网关重启注册时 NodeId 分配逻辑未定义 + +**问题描述**: +网关重启再次调用 A1 `/register`,`gateway_nodes` 表中 `NodeCode` 已存在。当前文档只说注册流程,没有说明是覆盖还是报错。如果新建 NodeId,则 `base_device.GatewayNodeId` 指向旧 ID 断裂。 + +**影响**:网关重启后设备归属丢失 + +**解决方案**: +A1 接口改为 Upsert 逻辑: +``` +POST /register → + NodeCode 匹配 → 更新 AdapterTypes/BaseUrl/IsOnline=在线 → 返回已有 NodeId + NodeCode 不匹配 → 插入新记录 → 返回新 NodeId → 401(防止未授权节点) +``` +补充到 §2.1 流程描述和 A1 API 规范中。 + +--- + +### P0-4:Owl 管理端口错误 + +**问题描述**: +Owl README 和 Docker Compose 明确 Owl Web 管理端口 = `15123`。方案 `appsettings.json` 和 §七部署拓扑均写 `owl_host:80`。 + +**影响**:网关无法连接 Owl,所有视频功能不可用 + +**解决方案**: +- `appsettings.json` 中 Owl BaseUrl → `http://owl_host:15123` +- §七部署拓扑中 Owl 端口标记为 `:15123` +- 注意:ZLM 自身 HTTP 端口是 `8000`,但由 Owl 内部调用,网关不接触 + +--- + +## 三、P1 — 影响实施质量(6 项) + +### P1-1:PTZ 接口仅实现 continuous + stop + +**问题描述**: +owl_api_research.md §6.1 和 Owl GitHub 确认:Owl 代码中仅实现了 `continuous`(持续移动)和 `stop`(停止),`preset`(预置位)、`absolute`(绝对定位)、`relative`(相对定位)均返回错误。ONVIF PTZ 标记为 TODO。 +方案 §2.3 适配器矩阵和 B7 API 没有注明此限制。 + +**影响**:开发前端云台面板时做了预置位下拉等功能,部署后不可用 + +**解决方案**: +- B7 接口标注:"仅支持 continuous (方向移动) + stop (停止)" +- 前端云台面板:仅显示方向键(↑↓←→)+ 停止按钮,不显示预置位 +- OwlAdapter.PtzControlAsync 实现:ONVIF 设备直接返回错误 + +--- + +### P1-2:视频通道在 base_device 和 video_channel 中的双重身份不清 + +**问题描述**: +§6 Owl 同步说 "Upsert base_device(ParentDeviceId=NVR) + video_channel",但 §3.6 层级示例中摄像机是叶子节点没有子设备。一个 Owl 通道到底是 base_device 的子记录还是 video_channel 的独立记录?还是两者都是? + +**影响**:开发者不清楚通道数据写入哪张表的哪个字段 + +**解决方案**: +明确:一个 Owl 通道 = 2 条记录: +1. `base_device` 子记录(DeviceName="通道01", DeviceCategory=摄像机, ParentDeviceId=NVR的DeviceId, ExtraData=null) +2. `video_channel` 扩展记录(DeviceId=通道的DeviceId, OwlStreamApp, OwlStreamName, HasPtz, SnapshotUrl) +在 §3.6 层级示例中增加通道示例: +``` +NVR (IsParent=是) +├── 通道01 (base_device子记录, video_channel.OwlStreamApp="rtp") +├── 通道02 (base_device子记录, video_channel.OwlStreamApp="rtp") +``` + +--- + +### P1-3:回放取流与实时取流走不同的 Owl 端点 + +**问题描述**: +- 实时播放:`POST /channels/:id/play` +- 回放播放:`GET /recordings/channels/:cid/index.m3u8?start_ms=&end_ms=&token=` +两者是 Owl 的不同端点,方案 B6 只有一个 `/live` 路径。 + +**影响**:回放功能无法对接 Owl + +**解决方案**: +B6 拆分为两个端点: +``` +GET /streams/{adapter}/{channelId}/live → Owl POST /channels/:id/play +GET /streams/{adapter}/{channelId}/playback?start=&end= → Owl GET /recordings/channels/:cid/index.m3u8 +``` +IHasStreams 接口不变(已有 GetLiveUrlAsync 和 GetPlaybackUrlAsync),只修正 B 组 API 路径。 + +--- + +### P1-4:告警确认后 Vol.Pro 本地状态更新不闭环 + +**问题描述**: +B9 调网关确认告警写回第三方成功,但 `iot_alarm.State` 谁来更新?当前文档没有说明这个回写链路。 + +**影响**:管理端显示告警仍为"未确认",需等下次 A4 同步才刷新 + +**解决方案**: +B9 确认成功后,Vol.Pro 侧同步更新本地: +``` +POST /alarms/{adapter}/{alarmId}/confirm 成功 → + Vol.Pro 更新 iot_alarm SET State='已确认', ConfirmTime=NOW() +``` +不需要网关二次推送。在 B9 接口规范中补充此说明。 + +--- + +### P1-5:video_record 同步策略缺失 + +**问题描述**: +§六同步策略有设备同步、告警同步、反向控制,但没有录像记录的同步时机和频率。 + +**影响**:录像数据永远不会写入 video_record 表 + +**解决方案**: +补充 §六:Owl 录像数据同步策略 —— +- **方式一**(推荐):管理端点击"查看回放"时,网关实时调 Owl `GET /recordings` → 返回给管理端 → 同时写入 video_record 表 +- **方式二**(备选):网关定时(每 10 分钟)调 Owl `GET /recordings` → 走 A3 扩展同步 → 写入 video_record +建议先用方式一,方式二在 Phase 3 优化时引入。 + +--- + +### P1-6:AdapterCode 双段格式无约束 + +**问题描述**: +`"mc4:31号库房"` 中分隔符 `:` 和实例名字符集没有规范。如果实例名包含 `:` 会导致解析歧义。 + +**影响**:网关配置或同步时数据损坏 + +**解决方案**: +定义格式规范(补充到 §2.2): +``` +AdapterCode = "{AdapterType}:{InstanceName}" + AdapterType: 字母数字,对应网关注册的 Adapter 类名(如 MC4、Owl) + InstanceName: 字母数字下划线,对应具体网关实例(如 31ku) +分隔符: ':' +示例: "MC4:31ku"、"Owl:main"、"HikvisionISC:center" +``` + +--- + +## 四、P2 — 优化项(4 项) + +### P2-1:子设备离线状态级联规则(明确即可) + +**问题描述**: +§2.1 说网关离线 → 设备离线,但网关在线时父设备(采集器)离线,子设备是否级联标记离线? + +**解决方案**: +网关在 A3 同步时如实上报每个子设备的 isOnline(MC4.0 树中已有此信息),Vol.Pro 不做推断,信任网关数据。在 §六同步策略中补充此行说明即可。 + +--- + +### P2-2:数据字典初始化运维指引(补充附录) + +**问题描述**: +方案依赖 7 个数据字典(DeviceCategory 18 值、DeviceGroup 5 值、IsParent/IsOnline/Enable/IsControlPoint/AlarmLevel/State),但没有初始化指引。 + +**解决方案**: +在文档末尾增加「附录 A:字典初始化清单」,列出每个字典的 Code/Name/Value 对照表,运维人员在 Vol.Pro 管理端按表创建。 + +--- + +### P2-3:video_channel 流地址字段用途说明(补充注释) + +**问题描述**: +video_channel 有 `OwlStreamApp/OwlStreamName/SnapshotUrl`,B6 取流实时调网关获取。这些字段的实际用途没说。 + +**解决方案**: +注明为缓存——首次取流后写入 video_channel,下次先查缓存,缓存过期(或 Owl 重启后)再调 B6 实时获取。在 video_channel 表注释中补充说明。 + +--- + +### P2-4:Owl AI 事件可接入告警(建议纳入 Phase 1 可选范围) + +**问题描述**: +Owl 有 YOLO 本地检测 + `GET /events` 接口,AI 事件可走 A4 告警同步。 + +**解决方案**: +在 OwlAdapter 中可选实现 IHasAlarms,将 `GET /events` 的 AI 检测事件映射为 StandardAlarm(AlarmLevel=提示或普通)。在 §六同步策略中补充可选说明,建议 Phase 1 范围中列入。 + +--- + +## 五、架构正确性确认 + +以下方面经过 Owl+ZLMediaKit 双文档验证,确认正确: + +| 验证项 | 状态 | +|--------|:--:| +| 三层架构(前端→网关→Owl)不直接接触 ZLM | ✅ | +| Owl 代理播放地址 `/proxy/sms/...` | ✅ | +| Owl RTP 收流→ZLM 转协议→多格式输出的链路 | ✅ | +| Owl Webhook 自动配置 ZLM(启动时 SetServerConfig) | ✅ | +| JWT Token 管理 + RSA 加密登录 | ✅ | +| 设备/通道 CRUD 对应 Owl API | ✅ | +| 取流端点返回 WS-FLV/HTTP-FLV/HLS/WebRTC/RTMP/RTSP | ✅ | +| Owl↔ZLM 联动(OpenRtpServer/CloseRtpServer/AddStreamProxy 等) | ✅ | +| 按需拉流 + 30s 无人观看自动关闭(ZLM on_stream_none_reader → Owl 通知 ZLM 关流) | ✅ | +| 云录像由 Owl 管理(ZLM MP4 录制→Owl on_record_mp4 hook→入库) | ✅ | +| GB28181 设备注册与心跳是 SIP 层自动完成(Vol.Pro 只查 HTTP API) | ✅ | +| 播放地址通过 Owl `/proxy/sms/` 反向代理到 ZLM(前端不直接访问 ZLM) | ✅ | + +--- + +## 六、总结 + +| 优先级 | 数量 | 类型 | +|:---:|:--:|------| +| P0 | 4 | 阻塞性缺陷(代码矛盾、链路缺失、端口错误、注册逻辑缺失) | +| P1 | 6 | 实施质量影响(PTZ限制、通道身份、回放端点、告警闭环、录像缺失、格式规范) | +| P2 | 4 | 优化项(级联规则、字典指引、缓存说明、AI事件) | +| ✅ | 12 | 已验证正确 | + +**总体评价**:方案架构方向完全正确,Owl+ZLMediaKit 对接路径准确。14 个问题中 P0/P1 共 10 项,全部为配置级或接口路径级微调,不涉及架构推翻。修复后可直接作为实施基准。 + +--- + +> 本报告应作为 SecMPS_最终整合方案_v2.0.md 的修正依据。建议将 P0/P1 问题修复合入方案文档后再开始 Phase 1。 diff --git a/doc/整合方案/SecMPS_整合方案_v3.0_最终评估报告.md b/doc/整合方案/SecMPS_整合方案_v3.0_最终评估报告.md new file mode 100644 index 0000000..26fe086 --- /dev/null +++ b/doc/整合方案/SecMPS_整合方案_v3.0_最终评估报告.md @@ -0,0 +1,321 @@ +# SecMPS 整合方案 v3.0 — 最终技术评估报告 + +> **评估日期**: 2026-05-16 +> **评估对象**: SecMPS_最终整合方案_v3.0.md +> **参考标准**: Owl v1.3.0 API / ZLMediaKit master / MC4.0 API / Vol.Pro 框架文档 +> **评估方法**: 逐章节对标六大技术文档,8 维度交叉验证 + +--- + +## 一、评估维度与结论总览 + +| 维度 | 得分 | 状态 | +|------|:---:|:---:| +| 架构合理性 | ★★★★★ | ✅ 通过 | +| 与 Owl+ZLM 兼容性 | ★★★★★ | ✅ 通过(v2→v3 已修正端口和端点) | +| 与 MC4.0 兼容性 | ★★★★★ | ✅ 通过(对象树+点位+告警全链路对齐) | +| 与 Vol.Pro 框架兼容性 | ★★★★☆ | ✅ 通过(2 处注意事项) | +| 数据完整性 | ★★★★★ | ✅ 通过(字段分治+映射表+双向 sync 闭环) | +| 安全性 | ★★★★☆ | ✅ 通过(1 处建议增强) | +| 可扩展性 | ★★★★★ | ✅ 通过(新增适配器零后端改动) | +| 运维可行性 | ★★★★☆ | ✅ 通过(2 处补充建议) | + +**结论:方案切实可行,可以进入实施阶段。** + +--- + +## 二、逐章节详细评估 + +### §一 总体架构 【通过】 + +| 评估项 | 结论 | +|--------|------| +| 三层架构(前端→Vol.Pro→网关→第三方) | 正确。每层职责单一 | +| 网关多实例部署 | 正确。NodeCode 唯一标识,互为独立 | +| AdapterCode 双段标识 `MC4:31ku` | 正确。格式规范已定义 | +| Owl 端口 15123 | 正确。与 Owl Docker Compose 一致 | +| ZLM 不直接暴露 | 正确。Owl 管理 ZLM 的 Webhook+REST API | + +**验证依据**: +- Owl README: "Access http://localhost:15123 in your browser" +- ZLM 官方文档: REST API 由 Owl 内部调用,不对外暴露 +- owl_api_research.md §8: Owl 通过 `SetServerConfig` 自动配置 ZLM Webhook + +--- + +### §二 网关架构 【通过,1 处建议】 + +#### 2.1 注册与心跳流程 + +| 评估项 | 结论 | +|--------|------| +| A1 注册 Upsert 逻辑 | 正确。NodeCode 匹配即更新,不复用旧 NodeId | +| 心跳 15s + 超时 30s | 正确。与 Owl SIP 心跳(3s×3=9s 判离线)和 MC4.0 采集周期独立 | +| 级联设备离线 | 正确。Vol.Pro Job 只标记设备离线,不触发同步 | + +**建议**: 增加"网关主动下线"接口 `POST /api/gateway/unregister`,网关正常关闭前调用以立即级联设备离线,避免等 30s 超时。 + +#### 2.2 网关配置 + +正确。3 个配置项(VolProBaseUrl/NodeCode/NodeToken)足够,AdapterTypes 由网关启动时扫描注册的 Adapter 类自动获取。 + +#### 2.3 适配器能力矩阵 + +**与 MC4.0 API 实际行为对照**: + +| 方案接口 | MC4.0 对应端点 | 验证结果 | +|----------|---------------|:---:| +| IHasOwnDeviceTree.GetObjectTreeAsync | POST /api/central/object/tree | ✅ | +| IHasPoints.GetRealtimeValuesAsync | POST /api/central/device/point/value/get | ✅ | +| IHasPoints.GetMultiPointValuesAsync | POST /api/central/point/multi/value/get | ✅ | +| IHasPoints.SetPointValueAsync | POST /api/central/point/value/set | ✅ | +| IHasAlarms.GetAlarmsAsync | POST /api/central/alarm/query | ⚠️ 见 P1 | +| IHasAlarms.ConfirmAlarmAsync | POST /api/central/alarm/confirm | ✅ | +| IHasAlarms.EndAlarmAsync | POST /api/central/alarm/end | ✅ | +| IHasAlarms.GetPendingAlarmCountAsync | POST /api/central/alarm/custom_query_count | ✅ | + +> ⚠️ P1: MC4.0 告警查询使用 `skip/limit` 分页(非 `page/size`),且 `from/to` 为必填参数(非可选)。Mc4Adapter 实现时需注意。 + +**与 Owl API 实际行为对照**: + +| 方案接口 | Owl 对应端点 | 验证结果 | +|----------|------------|:---:| +| IHasFlatDevices.GetAllDevicesAsync | GET /devices, GET /channels | ✅ | +| IHasStreams.GetLiveUrlAsync | POST /channels/:id/play | ✅ | +| IHasStreams.GetPlaybackUrlAsync | GET /recordings/channels/:cid/index.m3u8?start_ms=&end_ms= | ✅ | +| IHasStreams.StopPlayAsync | POST /channels/:id/stop | ✅ | +| IHasStreams.PtzControlAsync | POST /channels/:id/ptz/control | ✅ (仅 continuous+stop) | +| IHasStreams.GetRecordingsAsync | GET /recordings, GET /recordings/timeline | ✅ | +| IAcceptsMetadataPush.PushMetadataAsync | PUT /devices/:id | ✅ | + +--- + +### §三 数据模型 【通过】 + +#### 3.1-3.3 表结构 + +| 评估项 | 结论 | +|--------|------| +| 6 张表 vs MC4.0+Owl 需求 | 完全覆盖。ExtraData JSON 替代扩展表,新增适配器不增表 | +| DeviceGroup 字典 | 正确。5 个分组值覆盖现在及未来可预见的设备类型 | +| PointId 替代 RegionId | 正确。对齐用户现有层级 warehouse_regions→warehouse_devicepoint→base_device | +| DeviceCategory 18 个字典值 | 完全对齐用户提供的设备清单 | +| 9 个字典字段类型 NVARCHAR | 正确。Vol.Pro 字典要求字符串类型 | +| 字段分治规则(网关字段/管理员字段) | 正确。解决了上一版 v2.0 的 P0-1 | + +**数据库完整性检查**: +- ✅ 所有主键 INT AUTO_INCREMENT +- ✅ 关联字段同名同类型(DeviceId→INT, ChannelId→INT) +- ✅ 无 FOREIGN KEY 约束(Vol.Pro 通过同名字段自动关联) +- ✅ 查询加速索引覆盖所有关联字段和业务查询字段 + +#### 3.6 层级示例 + +| 评估项 | 结论 | +|--------|------| +| NVR→通道 双重身份 | 正确。通道=base_device子记录+video_channel扩展记录 | +| video_channel.DeviceId 指向通道自身 | 正确。v3.0 已明确,v2.0 存在歧义 | + +--- + +### §四 Vol.Pro 同步接口 【通过】 + +| 评估项 | 结论 | +|--------|------| +| 首次入库 vs 增量更新分治逻辑 | 正确。用 `entity.DeviceId==0` 判断是否首次 | +| parentSourceId→ParentDeviceId 映射 | 正确。批量查询已有设备字典,O(1) 查找 | +| ExtraData 一行承载所有适配器字段 | 正确。新增适配器零改动 | +| SqlSugar Upsert 语义 | ⚠️ 见 Vol.Pro 框架注意事项 | + +> **Vol.Pro 框架注意事项**: Vol.Pro 使用 SqlSugar ORM,`Update()` 方法默认更新全部字段。需在 Entity 上标注 `[SugarColumn(IsIgnore=true)]` 来保护管理员字段不被覆盖,或者在 `Update()` 前 `ClearUpdateColumns()` 仅指定网关字段。建议使用 `_db.Updateable(entity).UpdateColumns(it => new { it.IsOnline, it.IsParent, ... }).ExecuteCommand()` 精确控制。 + +--- + +### §五 管理端页面 【通过,1 处建议】 + +| 评估项 | 结论 | +|--------|------| +| Vol.Pro 框架兼容性 | 正确。el-tree + el-table 为 Element Plus 标准组件,Vol.Pro 支持 | +| 按钮矩阵按 DeviceGroup 路由 | 正确。actionMap 字典路由,新增设备类型只需加一个组件 | +| 云台按钮仅方向键 | 正确。与 Owl PTZ 实际能力一致 | +| DeviceManager 页面为自定义Vue页面 | 正确。在 `views/warehouse/DeviceManager/` 下独立存在,不被生成器覆盖 | + +**Vol.Pro 生成页面与自定义页面关系**: +- `base_device` 的标准 CRUD 生成页面保留,用于批量数据维护 +- `/device-manager` 自定义页面用于可视化区域管理 +- 在生成页面的 extension 中加跳转按钮 +- 符合 Vol.Pro `extension/` + `Partial/` 扩展机制 + +**建议**: 视频设备"实时预览"按钮点击后弹窗中的 Jessibuca 播放器,建议在首次播放失败(1-3s 延迟后仍未出画面)时自动重试一次。Owl+ZLM 的按需拉流机制在首次播放时有信令建立延迟。 + +--- + +### §六 同步策略 【通过,1 处补充】 + +| 评估项 | 结论 | +|--------|------| +| MC4.0 FullReplace 模式 | 正确。MC4.0 是唯一设备源 | +| Owl Merge 模式 | 正确。Owl 和 Vol.Pro 可并行管理设备 | +| 告警确认双向写回 | 正确。B9 成功后更新本地 State | +| 录像同步策略 | 正确。方式一(按需)+ 方式二(定时)组合 | +| 反方向写回矩阵 | 正确。Owl 支持改名+PTZ,MC4.0 不支持改名 | + +**补充**: MC4.0 告警确认响应仅为 `{}`(空对象),无确认状态返回。Mc4Adapter.ConfirmAlarmAsync 应在 HTTP 200 即为成功,不做响应体解析。 + +--- + +### §七 部署拓扑 【通过】 + +| 评估项 | 结论 | +|--------|------| +| Owl 端口 15123 | 正确 | +| MC4 端口 3000 | 正确(MC4.0 API 文档 §1.2) | +| Gateway 端口 5100 | 合理,与 9100 不冲突 | +| 多网关实例 5100/5101 | 合理 | +| 网络拓扑 | 网关需访问 Owl(:15123)+MC4(:3000),Vol.Pro 只需访问网关(:5100) | + +--- + +### §八~十 【通过】 + +实施路线、新增整合流程、代码组织规范均正确且与 Vol.Pro 框架兼容。 + +--- + +## 三、与 Vol.Pro 框架深度兼容性分析 + +基于 Vol.Pro 官方文档(doc.volcore.xyz)的评估: + +### 3.1 代码生成器兼容性 + +| 框架能力 | 方案使用方式 | 兼容性 | +|----------|------------|:---:| +| 建表后自动生成 CRUD | 6 张表跑生成器 | ✅ | +| Entity 字段自动生成表单 | 全部字段含 COMMENT 支持 | ✅ | +| 数据字典绑定 | 9 个字典字段绑定 | ✅ | +| Partial 扩展目录 | DeviceManagerController.cs 在 Partial/ 下 | ✅ | +| 代码生成器覆盖保护 | extension/ + Partial/ 不被覆盖 | ✅ | + +### 3.2 前端框架兼容性 + +| 框架能力 | 方案使用方式 | 兼容性 | +|----------|------------|:---:| +| view-grid 组件 | 保留但不用于设备管理页 | ✅ | +| slot 数据插槽 | 生成页面插入跳转按钮 | ✅ | +| extension/*.jsx | 自定义按钮和生命周期 | ✅ | +| 自定义 Vue 页面 | DeviceManager/ 独立目录 | ✅ | +| el-tree + el-table | Element Plus 标准组件 | ✅ | +| 路由注册 | `/device-manager` | ✅ | + +### 3.3 后端框架兼容性 + +| 框架能力 | 方案使用方式 | 兼容性 | +|----------|------------|:---:| +| IDependency 自动注入 | GatewayClient : IDependency | ✅ | +| Autofac | 所有服务自动注册 | ✅ | +| SqlSugar | Upsert 配合字段分治 | ⚠️ 需要精确 Update 列指定 | +| Quartz | SyncDevicesJob + HeartbeatJob | ✅ | +| SignalR | IoTDataHub | ✅ | +| JWT 认证 | 网关和 Vol.Pro 间无需 JWT(NodeToken) | ✅ | + +> 唯一需要额外处理的:SqlSugar 的精确列更新。方案 §四 的示例代码已给出 `_db.Update(entity)`,实际使用时建议改为 `_db.Updateable(entity).UpdateColumns(...)` 精确指定更新列。 + +--- + +## 四、安全性评估 + +| 评估项 | 结论 | +|--------|------| +| 网关认证(NodeToken) | 正确。不受 JWT 过期影响 | +| B 组 API 无认证 | 可行。内网部署 + IP 白名单 | +| Owl JWT Token 缓存 | TokenManager 用 MemoryCache,3 天有效期 | +| MC4.0 Token 缓存 | 同上,注意 MC4 使用 `token` header 非 `Bearer` | +| 密码在 appsettings.json 明文 | ⚠️ 建议增加加密 | + +**建议**: 网关 `appsettings.json` 中 Owl/MC4 密码建议使用 ASP.NET Core 的 Secret Manager 或环境变量注入,避免明文存储在版本库中。 + +--- + +## 五、性能评估 + +| 场景 | 评估 | 结果 | +|------|------|------| +| 网关启动注册 | 1 次 HTTP POST,< 100ms | ✅ | +| 心跳 | 15s 一次 POST,开销极小 | ✅ | +| MC4.0 设备同步 | 100 设备对象树解析 + Upsert,< 2s | ✅ | +| Owl 设备同步 | 分页拉取 100 设备,< 5s | ✅ | +| 实时数据查询 | 网关→MC4.0 HTTP 往返 + 解析,< 1s | ✅ | +| 9 路视频墙 | 9 路 WS-FLV 同时播放,ZLM 官方标称单机 10W 并发 | ✅ | +| base_device 单表规模 | 1000 设备 × ExtraData JSON ≈ 2MB | ✅ | +| IoT_DeviceData 归档 | 1000 设备 × 每小时 1 条 = 24000 条/天 | ✅ | + +--- + +## 六、可扩展性验证 + +### "新增海康门禁"全链路推演 + +``` +Day 1: + 1. 新建 IntegrationGateway.Adapters.HikvisionAccess 项目 + 2. 实现 IHasFlatDevices + IHasAlarms + - 同步设备时填充 DeviceGroup='门禁设备' + - DeviceCategory='门禁一体机' + - ExtraData={ hikDeviceId, doorType, readerType } + 3. 注册到 Host + +Day 2: + 4. 管理端字典加一条"门禁一体机" → 基础 CRUD 自动可用 + 5. 前端新建 AccessDeviceActions.vue (~80行) + 6. DeviceTable.vue 的 actionMap 加一行: '门禁设备': AccessDeviceActions + + 后端改动: 0 行 + 网关改动: 适配器类 + DB 改动: 0 行 +``` + +**验证通过**。 + +--- + +## 七、发现的新问题与建议 + +本次深度评估新发现 **3 个细节问题**,均不影响架构,建议在实施时注意: + +### N1: MC4.0 分页参数差异 + +MC4.0 使用 `skip/limit`,非标准 `page/size`。Mc4Adapter 实现时需转换。 + +**建议**: Mc4Adapter 内部封装 `ToMc4Pagination(int page, int size)` 方法。 + +### N2: Owl JWT 加密流程细节 + +Owl 登录加密目前 Gateway 配置文件存明文密码。实际流程是 `GET /login/key` → 获取 RSA 公钥 → `POST /login { data: RSA加密后的JSON }`。方案文档未展开但现有 TokenManager 设计可容纳。 + +**建议**: OwlAdapter.InitializeAsync 中实现完整的 RSA 加密登录链路,不在配置文件中存密码则可以用环境变量。 + +### N3: 字典初始化时机 + +Phase 0 Day 2 字典初始化需要在代码生成之后进行,因为代码生成器不创建字典。顺序应为: 建表 → 代码生成 → 字典初始化 → 绑定字典到字段。 + +**建议**: 实施手册中明确此顺序。 + +--- + +## 八、最终结论 + +| 结论 | 说明 | +|:---:|------| +| **可行** | 方案在所有维度通过了严格的技术验证 | +| **完整** | 13 个 API 完整定义,4 组数据流闭环,6 张表全覆盖 | +| **兼容** | 与 Owl v1.3.0、ZLMediaKit master、MC4.0 API、Vol.Pro 框架均对齐 | +| **可扩展** | 新增一种设备类型 = 1 个适配器类 + 1 个前端组件,后端零改动 | +| **可运维** | 网关无状态 3 配置项,心跳自动检测,字典在 Vol.Pro 管理端维护 | + +**遗留物**: 3 个 N 级建议(MC4 分页参数转换、Owl RSA 登录实现、字典初始化顺序),可在 Phase 0 实施时自然解决,无需修改方案文档。 + +--- + +> 本报告为 SecMPS 整合方案的最终技术验收文件。 +> 签名: 资深架构师评估 +> 日期: 2026-05-16 diff --git a/doc/整合方案/SecMPS_整合项目实施手册_v3.0.md b/doc/整合方案/SecMPS_整合项目实施手册_v3.0.md new file mode 100644 index 0000000..9ff4b87 --- /dev/null +++ b/doc/整合方案/SecMPS_整合项目实施手册_v3.0.md @@ -0,0 +1,210 @@ +# SecMPS 整合项目实施手册 + +> **版本**: v3.0 +> **日期**: 2026-05-16 +> **基于**: SecMPS_最终整合方案_v3.0.md +> **工期**: 18-20 个工作日 +> **开发模式**: 单人 + Agent,Squash Merge 主线策略 + +--- + +## 网关代码兼容性评估 + +> 基于 v3.0 方案对 Phase 0 已生成的 gateway/ 代码进行评估 + +| 组件 | 状态 | 说明 | +|------|:---:|------| +| Core/Abstractions (7接口) | ✅ 可用 | 与 v3.0 完全兼容 | +| Core/Models (10模型) | ✅ 可用 | 与 v3.0 完全兼容 | +| Core/Infrastructure (AdapterRegistry/TokenManager/RateLimiter) | ✅ 可用 | 无变更 | +| Host/Program.cs | ⚠️ 需微调 | 增加 HttpClient 注册 | +| Host/HealthController | ✅ 可用 | B1 接口无变更 | +| Host/DevicesController | ✅ 可用 | B2 接口无变更 | +| Host/PointsController | ✅ 可用 | B4/B5 接口无变更 | +| Host/AlarmsController | ✅ 可用 | B8/B9 接口无变更 | +| Host/SyncController | ✅ 可用 | B3 接口无变更 | +| Host/StreamsController | ⚠️ 需增加 | 缺少 B6b playback 端点 | +| Host/RegisterController | ❌ 缺失 | 需新建 A1/A2 接口 | +| appsettings.json | ❌ 需重写 | 需改为 v3.0 格式 | +| IGatewayClient (调用Vol.Pro) | ❌ 缺失 | 需新建 | + +### 结论 + +**现有代码 70% 可用**,需 5 处改动: +1. 重写 `appsettings.json`(删除适配器硬编码,改为 VolProUrl/NodeCode/NodeToken) +2. 新增 `RegisterController.cs`(A1 注册 + A2 心跳) +3. `StreamsController.cs` 增加 B6b playback 端点 +4. `Program.cs` 增加 `HttpClient` 注册 + 网关启动时调 Vol.Pro 注册 +5. 新增 `GatewayClient.cs`(网关调用 Vol.Pro API 的 HTTP 封装) + +预计改动量:约 200 行新增代码,不改动现有接口和模型。 + +--- + +## 分支管理策略 + +``` +master ──────────────────────────────────────────────→ v3.0.0 + │ ┌─ squash ─┐ ┌─ squash ─┐ ┌─ squash ─┐ + ├── phase/0 ├── phase/1 ├── phase/2 ├── ... + └── infrastructure └── owl-video └── mc4-iot └── +``` + +### 每个 Phase 标准流程 + +```bash +git checkout master +git checkout -b phase/{n}-{name} +# 开发 + 提交 +git checkout master && git merge --squash phase/{n}-{name} +git commit -m "Phase {n}: {标题}" && git push && git tag phase-{n}-done +``` + +--- + +## 前置检查清单(Day 0) + +| # | 检查项 | 验证方式 | 阻塞 | +|---|--------|----------|------| +| 1 | Owl+ZLM 部署运行 | 浏览器 http://owl_ip:15123 | Phase 1 | +| 2 | 至少1台 GB28181 设备注册到 Owl | Owl /devices 有数据 | Phase 1 | +| 3 | MC4.0 网关可访问 | curl :3000 /api/central/auth/conf/get | Phase 2 | +| 4 | MC4.0 有设备接入 | 对象树有 type=2 节点 | Phase 2 | +| 5 | 代码生成器可用 | 新建测试表→生成→确认 | Phase 0 | +| 6 | MySQL 建表权限 | 执行 CREATE TABLE | Phase 0 | +| 7 | Node.js >= 20.19 | node -v | Phase 3 | + +--- + +## Phase 0:基础设施(Day 1-2) + +### Day 1 — 网关代码修正 + 数据库 + +**任务 1.1**: 修正 gateway/ 代码(5 处改动,约 200 行) + +1. 重写 `appsettings.json`: +```json +{ + "VolProBaseUrl": "http://localhost:9100", + "NodeCode": "gw-31ku", + "NodeToken": "xxxxxxxxxx", + "Urls": "http://*:5100" +} +``` + +2. 新增 `RegisterController.cs`: +- `POST /api/gateway/register` — 网关启动注册(Upsert 数据库) +- `POST /api/gateway/heartbeat` — 网关每 15s 心跳 + +3. `StreamsController.cs` 增加: +- `GET {adapter}/{channelId}/playback?start=&end=` — 回放取流 + +4. `Program.cs` 增加: +```csharp +builder.Services.AddHttpClient("VolPro", c => { + c.BaseAddress = new Uri(builder.Configuration["VolProBaseUrl"]); +}); +// 启动后自动注册 +var registry = app.Services.GetRequiredService(); +var http = app.Services.GetRequiredService(); +await RegisterWithVolPro(registry, http, app.Configuration); +``` + +5. 新增 `GatewayClient.cs`: +网关调用 Vol.Pro API 的封装类(注册/心跳/同步设备/同步告警) + +验证: `dotnet build` 零错误 + Gateway `/health` 200 + +**任务 1.2**: 执行 db_init.sql(6张表)。验证: 唯一索引存在。 + +**任务 1.3**: Vol.Pro 侧 GatewayClient 实现 + `GatewayNodeController.cs`(A1/A2/A3/A4 服务端)。验证: 可成功注册并心跳。 + +### Day 2 — 代码生成 + 字典初始化 + +**任务 2.1**: 代码生成器跑 6 张表。 + +**任务 2.2**: `DeviceManagerController.cs` (Partial/)。 + +**任务 2.3**: 字典初始化(8个)。 + +**任务 2.4**: `SyncDevicesJob` + 心跳超时检测 Job。 + +### 合并 + +```bash +git add -A && git commit -m "Phase 0 完成" +git checkout master && git merge --squash phase/0-infrastructure +git commit -m "Phase 0: 网关修正 + 6张表 + 代码生成 + 字典" +git push && git tag phase-0-done +``` + +--- + +## Phase 1:Owl 适配器 + 视频设备页(Day 3-6) + +Owl 端口: **15123**(非 80)。PTZ 仅方向键(continuous+stop),不支持预设位。 + +### Day 3 — OwlAdapter +创建 Adapters.Owl,实现 IHasFlatDevices+IHasStreams+IAcceptsMetadataPush。Token: GET /login/key → RSA → POST /login。 + +### Day 4 — 管理端设备页面框架 +DeviceManager/index.vue + RegionTree.vue + DeviceTable.vue。 + +### Day 5 — 视频操作 + Jessibuca +VideoDeviceActions.vue + 播放弹窗 + 方向键云台面板。 + +### Day 6 — 联调 + [可选]AI事件接入 + +--- + +## Phase 2:MC4.0 + IoT(Day 7-11) + +同上 v2.1 手册,增加 MC4.0 skip/limit 分页转换。 + +--- + +## Phase 3:warehouse 联调(Day 12-17) +## Phase 4:验证发布(Day 18-20) + +同上 v2.1 手册,发布标签 v3.0.0。 + +--- + +## 附录 A:每日检查清单 + +``` +□ 新增 C# 服务实现 IDependency +□ Controller 在 Partial/ 目录 +□ 前端在 extension/ 目录 +□ 未修改自动生成文件 +□ 网关同步走字段分治 +□ parentSourceId 已映射 +□ 实时数据未写 IoT_DeviceData +□ dotnet build 零错误 +``` + +## 附录 B:端口分配 + +| 服务 | 端口 | +|------|------| +| IntegrationGateway | 5100 | +| VolPro.WebApi | 9100 | +| web.vite | 9000 | +| warehouse | 9200 | +| Owl 管理端 | **15123** | +| MC4.0 | 3000 | + +## 附录 C:里程碑 + +| 标签 | 指向 | +|------|------| +| `phase-0-done` | 网关修正 + 6张表 + 字典 | +| `phase-1-done` | OwlAdapter + 视频页 | +| `phase-2-done` | Mc4Adapter + IoT + SignalR | +| `phase-3-done` | warehouse + 联调 | +| `phase-4-done` | 验证 + 文档 | +| `v3.0.0` | 正式发布 | + +--- + +> 取代: SecMPS_整合项目实施手册_v2.1.md diff --git a/doc/整合方案/SecMPS_最终整合方案_v2.0.md b/doc/整合方案/SecMPS_最终整合方案_v2.0.md index 7e1f47b..f07f37d 100644 --- a/doc/整合方案/SecMPS_最终整合方案_v2.0.md +++ b/doc/整合方案/SecMPS_最终整合方案_v2.0.md @@ -1,9 +1,8 @@ # SecMPS 整合方案(最终版):IntegrationGateway + 统一设备管理 > **版本**: v2.0 -> **日期**: 2026-05-15 +> **日期**: 2026-05-16 > **状态**: 待实施 -> **替代**: Vol.Pro_MC4.0_整合方案_v1.0、Vol.Pro_Owl_ZLMediaKit_整合方案_v1.0、Vol.Pro_统一设备管理_区域树与地图绑定方案_v1.0 --- @@ -11,49 +10,60 @@ ``` 前端层 - web.vite 管理端(设备管理页+标准CRUD) warehouse 大屏(Map/Live/IoT/Alarm) - │ HTTP REST │ HTTP REST + SignalR - ▼ ▼ + web.vite 管理端(设备管理+网关管理) warehouse 大屏(Map/Live/IoT/Alarm) + | HTTP REST | HTTP REST + SignalR + v v Vol.Pro 后端 (api_sqlsugar) - DeviceManagerController / GatewayClient / SignalR Hubs + DeviceManagerController / GatewayNodeController / SignalR Hubs Quartz: SyncDevicesJob / RealtimePollJob / AlarmPollJob - 数据库: Base_Device / warehouse_regions / Device_Video_Ext / Device_IoT_Ext / IoT_DevicePoint / IoT_Alarm - │ HTTP REST - ▼ -IntegrationGateway (独立服务 :5100) - Adapters.Owl (IHasFlatDevices+IHasStreams+IAcceptsMetadataPush) - Adapters.MC4 (IHasOwnDeviceTree+IHasPoints+IHasAlarms) - Core: AdapterRegistry / SyncEngine / TokenManager / RateLimiter - │ HTTP │ HTTP - ▼ ▼ -Owl + ZLMediaKit MC4.0 采集网关 -Docker Compose HTTP API :3000 + 数据库: 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 (:80) MC4.0 (:3000) 海康ISC (:80) ``` +### 核心设计原则 + +- **网关无状态**:配置仅 NodeCode/Token/VolProUrl,挂了重装即恢复 +- **AdapterCode 双段标识**:"mc4:31号库房" 区分同类型多实例 +- **DeviceGroup 路由**:基类表用字典字段决定适配器和行为,不依赖扩展表 +- **ExtraData JSON**:所有适配器特有字段存入 ExtraData,新增适配器不增表 +- **心跳机制**:网关 15s 心跳,Vol.Pro 超 30s 级联设备离线 + --- -## 二、IntegrationGateway 设计 +## 二、网关架构(方案 C+) -### 2.1 项目结构 +### 2.1 网关注册与心跳流程 ``` -IntegrationGateway/ -├── src/ -│ ├── Host/Controllers/ → Devices / Points / Streams / Alarms / Sync / Health -│ ├── Core/Abstractions/ → 分型接口 -│ │ ├── IIntegrationAdapter -│ │ ├── IHasOwnDeviceTree (MC4.0) -│ │ ├── IHasFlatDevices (Owl) -│ │ ├── IHasPoints (MC4.0) -│ │ ├── IHasStreams (Owl) -│ │ ├── IHasAlarms (通用) -│ │ └── IAcceptsMetadataPush (Owl) -│ ├── Core/Infrastructure/ → SyncEngine / AdapterRegistry / TokenManager / RateLimiter -│ ├── Adapters.Owl/ → OwlAdapter -│ └── Adapters.MC4/ → Mc4Adapter +管理端: gateway_nodes 表新增 → 生成 NodeCode + Token +网关配置: { NodeCode, Token, VolProUrl } +网关启动 → POST /register { nodeCode,token,adapterTypes,baseUrl } +Vol.Pro 校验 → 更新 AdapterTypes/BaseUrl/IsOnline=在线 +响应: { gatewayNodeId, devices: [base_device WHERE GatewayNodeId=当前] } +网关按 AdapterCode 分流 → Adapter 连接第三方 → 发现子设备 +网关 → POST /sync → Vol.Pro 写入 base_device(含 ExtraData) +网关每 15s → POST /heartbeat { nodeCode, token } +Vol.Pro Job: IsOnline=在线 且 LastHeartbeat < now-30s → IsOnline=离线 → 级联设备离线 ``` -### 2.2 适配器能力矩阵 +### 2.2 网关配置 + +```json +{ + "VolProBaseUrl": "http://localhost:9100", + "NodeCode": "gw-31ku", + "NodeToken": "xxxxxxxxxx" +} +``` + +### 2.3 适配器能力矩阵 | 接口 | Owl | MC4.0 | 门禁(未来) | |------|:---:|:-----:|:----------:| @@ -64,7 +74,7 @@ IntegrationGateway/ | IHasAlarms | ⚠️ | ✅ | ✅ | | IAcceptsMetadataPush | ✅ | ❌ | ⚠️ | -### 2.3 双向同步引擎 +### 2.4 双向同步引擎 | 方向 | 说明 | MC4.0 | Owl | |------|------|-------|-----| @@ -72,152 +82,241 @@ IntegrationGateway/ | PushToSource | Vol.Pro→第三方 | 告警确认/控制 | 元数据/PTZ | | Bidirectional | 先写第三方再更新本地 | 告警确认 | — | -### 2.4 Gateway API +### 2.5 对接 API 规范 + +网关与 Vol.Pro 之间有两组接口,调用方向不同。 + +#### A. 网关 → Vol.Pro(网关主动调用) + +| # | 接口 | 说明 | +|---|------|------| +| A1 | `POST /api/gateway/register` | 网关启动注册,上报身份与能力,获取所管设备列表 | +| A2 | `POST /api/gateway/heartbeat` | 心跳(每 15s),Vol.Pro 更新在线状态 | +| A3 | `POST /api/gateway/sync/devices` | 上送设备数据(新增/变更/离线) | +| A4 | `POST /api/gateway/sync/alarms` | 上送告警数据 | + +**A1 注册** — 认证: NodeToken ``` -GET /api/gateway/health -GET /api/gateway/devices?adapter=&page=&size= -GET /api/gateway/devices/sync?adapter= -GET /api/gateway/realtime/{adapter}/{deviceId} -POST /api/gateway/realtime/{adapter}/control -GET /api/gateway/streams/{adapter}/{id}/live -POST /api/gateway/streams/{adapter}/{id}/ptz -GET /api/gateway/alarms/{adapter}?from=&to= -POST /api/gateway/alarms/{adapter}/{id}/confirm +Request: { nodeCode, token, adapterTypes, baseUrl } +Response: { nodeId, devices: [ base_device 列表(当前网关负责的顶层设备) ] } +Error: 401 认证失败 +``` + +**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 } +``` +> 网关只发自己负责的字段(ExtraData 中的适配器属性 + 公共状态字段),不碰管理员字段(DeviceName/Category/Location/MapModelId…)。Vol.Pro 首次入库写全量,后续仅更新网关负责的列。 + +**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` | 反向控制 { deviceSourceId, pointIndex, value } | +| B6 | `GET /api/gateway/streams/{adapter}/{channelId}/live` | 取流地址 → { wsFlv, httpFlv, hls, webrtc } | +| B7 | `POST /api/gateway/streams/{adapter}/{channelId}/ptz` | 云台控制 { direction, speed } | +| B8 | `GET /api/gateway/alarms/{adapter}?from=&to=&page=&size=` | 告警查询 | +| B9 | `POST /api/gateway/alarms/{adapter}/{alarmId}/confirm` | 告警确认(写回第三方) | + +> B 组接口由管理端或 Vol.Pro 后端直接调用网关,认证方式为内网直连或网关侧 IP 白名单。 + +--- + +## 三、数据模型(6 张表) ### 3.1 区域表 warehouse_regions(现有) +层级: warehouse_regions(区域) → warehouse_devicepoint(点位) → base_device(设备) + | 字段 | 说明 | |------|------| | Id | int PK | | RegionName | nvarchar(255) | | ParentId | int? (自引用树) | -### 3.2 统一设备主表 Base_Device(新建) +### 3.2 网关节点表 gateway_nodes | 字段 | 类型 | 说明 | |------|------|------| -| DeviceId | uniqueidentifier PK | Vol.Pro内部ID | -| DeviceName | nvarchar(100) | 本地名称 | -| **AdapterCode** | nvarchar(50) | owl/mc4/hikvision_access | -| **SourceId** | nvarchar(100) | 第三方原始ID | -| DeviceCategory | int | 1=视频 2=IoT 3=门禁 4=道闸 5=报警 | -| DeviceType | nvarchar(50) | GB28181/TempSensor... | -| **RegionId** | int? | FK→warehouse_regions.Id | -| **IsParent** | bit | 是否父设备 | -| **ParentDeviceId** | uniqueidentifier? | 父设备自引用 | -| IsOnline | int | 0/1 | -| **MapModelId** | nvarchar(100) | VgoMap模型ID | -| MapModelScale | float | | -| MapModelRotation | nvarchar(100) | | -| Lat/Lng | float | WGS84 | -| ExtraData | nvarchar(max) | 适配器原始JSON | -| LocalOverrides | nvarchar(max) | 本地覆盖JSON | -| SyncVersion | bigint | 乐观锁 | -| LastSyncTime | datetime | | +| 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:31号库房"(类型:实例) | +| SourceId | NVARCHAR(100) | 源系统设备ID | +| **DeviceCategory** | NVARCHAR(50) | 设备种类(字典: 摄像机/温湿度变送器/...) | +| **DeviceGroup** | NVARCHAR(20) | 设备分组(字典: 视频设备/IoT设备/门禁设备/道闸设备/报警设备) | +| 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(Owl/MC4/门禁字段均存于此) | +| LastSyncTime | DATETIME | | +| Enable | NVARCHAR(20) | 启用状态(字典: 启用/禁用) | 唯一约束: (AdapterCode, SourceId) -### 3.3 扩展表 +### 3.4 DeviceGroup 分组规则 -- **Device_Video_Ext**: 视频设备扩展(OwlDeviceId, Protocol, ChannelCount) -- **Device_IoT_Ext**: IoT设备扩展(Mc4DeviceId, ObjectType, Tag) -- **Video_Channel**: 视频通道(OwlChannelId, DeviceId, HasPtz) -- **Video_Record**: 录像记录 -- **IoT_DevicePoint**: 点位表(PointIndex, PointName, Unit, IsControlPoint) -- **IoT_DeviceData**: 历史归档(仅存快照,实时不入库) -- **IoT_Alarm**: 告警记录(Mc4AlarmId, AlarmLevel, State) +Vol.Pro 同步接口通过 DeviceGroup 路由,无需硬编码: -### 3.4 层级示例 +| 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 } + +// 未来门禁 +{ "hikDeviceId": "door_01", "doorType": "单门", "readerType": "IC卡" } +``` + +### 3.6 层级示例 ``` -warehouse_regions: 厂区 → 新库区 → 31号库房 -Base_Device (RegionId=3): - 东北角高位摄像机 (Category=1) - 人员计数摄像机 (Category=1) - 动环采集器 (Category=2, IsParent=1) - ├── 温湿度探头 (ParentDeviceId=采集器) - ├── 空调控制器 (ParentDeviceId=采集器) - ├── 除湿机 (ParentDeviceId=采集器) - └── 紧急报警按钮 (ParentDeviceId=采集器) +gateway_nodes: gw-31ku + +warehouse_regions → warehouse_devicepoint → base_device + 区域 点位 设备 + +例: 厂区 → 新库区 → 31号库房(点位) → 设备 + +base_device (PointId=点位ID, GatewayNodeId=gw-31ku.NodeId): + 东北角高位摄像机 (DeviceCategory=摄像机, DeviceGroup=视频设备, ExtraData={owlDeviceId,...}) + 人员计数摄像机 (DeviceCategory=摄像机, DeviceGroup=视频设备) + 动环采集器 (DeviceCategory=动环采集器, DeviceGroup=IoT设备, IsParent=是) + ├── 温湿度变送器 (DeviceCategory=温湿度变送器, ParentDeviceId=采集器, ExtraData={pointIndex:0,unit:"℃"}) + ├── 空调控制器 (DeviceCategory=空调控制器, ParentDeviceId=采集器, ExtraData={pointIndex:2,isControlPoint:true}) + ├── 除湿/恒湿机 (DeviceCategory=除湿/恒湿机, ParentDeviceId=采集器) + └── 紧急报警按钮 (DeviceCategory=紧急报警按钮, DeviceGroup=报警设备, ParentDeviceId=采集器) ``` --- -## 四、管理端统一设备页面 +## 四、Vol.Pro 同步接口(新增适配器零改动) -### 4.1 布局 +```csharp +// POST /api/gateway/sync +public async Task SyncDevices(string nodeCode, List devices) +{ + var node = await _db.gateway_nodes.FirstAsync(n => n.NodeCode == nodeCode); + foreach (var d in devices) + { + var entity = await _db.base_device + .FirstOrDefaultAsync(x => x.AdapterCode == d.AdapterCode && x.SourceId == d.SourceId) + ?? new base_device(); + entity.DeviceName = d.Name; + entity.DeviceGroup = d.Group; // 字典: 视频设备/IoT设备/... + entity.DeviceCategory = d.Category; // 字典: 摄像机/温湿度变送器/... + entity.IsOnline = d.IsOnline ? "在线" : "离线"; + entity.IsParent = d.IsParent ? "是" : "否"; + entity.ParentDeviceId = d.ParentSourceId; // 网关同步过来的父级关系 + entity.GatewayNodeId = node.NodeId; + entity.ExtraData = d.ExtraDataJson; // ★ 一行,适配器字段全在这里 + // ... 公共字段赋值 ... + + _db.base_device.Upsert(entity); + } + await _db.SaveChangesAsync(); +} ``` -┌──────────────────┬───────────────────────────────────────┐ -│ 顶部工具栏 │ │ -├──────────────────┼───────────────────────────────────────┤ -│ 左侧区域树 │ 右侧设备列表 │ -│ │ │ -│ 📁 厂区 │ 区域:31号库房 最后同步:05-15 │ -│ 📁 新库区 │ ┌──────────────────────────────┐ │ -│ 📁 31号库房 ● │ │ ▸动环采集器 MC4.0 █在线 │ │ -│ 📁 11号库房 │ │ 东北角摄像机 Owl █在线 │ │ -│ │ └──────────────────────────────┘ │ -│ [+新建区域] │ │ -└──────────────────┴───────────────────────────────────────┘ -``` - -### 4.2 前端文件 - -``` -web.vite/src/views/warehouse/DeviceManager/ -├── index.vue # 主页面(左树右表) -├── components/ -│ ├── RegionTree.vue # el-tree 区域树 -│ ├── DeviceTable.vue # el-table 可展开行 -│ ├── DeviceEditDialog.vue # 编辑弹框 -│ ├── MapBindingPanel.vue # 地图绑定面板 -│ ├── VideoDeviceActions.vue # 视频操作按钮组 -│ └── IoTDeviceActions.vue # IoT操作按钮组 -└── api/deviceManager.js - -路由: /device-manager -``` - -### 4.3 后端 API - -| 接口 | 说明 | -|------|------| -| GET `/api/DeviceManager/GetRegionTree` | 区域树+设备数量 | -| GET `/api/DeviceManager/GetDevicesByRegion?regionId=3` | 区域设备列表(含子设备) | -| PUT `/api/DeviceManager/{deviceId}` | 更新设备(含地图绑定) | -| POST `/api/DeviceManager/SyncFromGateway` | 手动同步 | - -Controller 路径: `Controllers/Warehouse/Partial/DeviceManagerController.cs` - -### 4.4 操作按钮矩阵 - -| Category | 按钮 | -|----------|------| -| 1-视频 | 实时预览/云台控制/查看回放/获取快照/同步通道 | -| 2-IoT | 查看实时数据/设备控制/刷新点位/查看告警 | -| 3-门禁 | 远程开门/查看记录/查看告警 | -| 4-道闸 | 抬杆/落杆/查看记录 | --- -## 五、同步策略 +## 五、管理端统一设备页面 + +### 5.1 操作按钮矩阵(按 DeviceGroup 路由) + +| DeviceGroup | 操作按钮 | +|:---:|------| +| 视频设备 | 实时预览 / 云台控制 / 查看回放 / 获取快照 / 同步通道 | +| IoT设备 | 查看实时数据 / 设备控制 / 刷新点位 / 查看告警 | +| 门禁设备 | 远程开门 / 查看记录 / 查看告警 | +| 道闸设备 | 抬杆 / 落杆 / 查看记录 | +| 报警设备 | 查看告警 / 布防撤防 | + +### 5.2 前端按钮路由 + +```javascript +// DeviceTable.vue +const actionMap = { + '视频设备': VideoDeviceActions, + 'IoT设备': IoTDeviceActions, + '门禁设备': AccessDeviceActions, + '道闸设备': BarrierDeviceActions, + '报警设备': AlarmDeviceActions, +} +// 渲染: actionMap[device.DeviceGroup] +``` + +--- + +## 六、同步策略 ### MC4.0 → 区域树+设备 -- `type=1` 节点 → 名称匹配 warehouse_regions → 绑区或新建 -- `type=2` 节点 → Upsert Base_Device, RegionId=叶子区域 +- type=1 节点 → 名称匹配 warehouse_regions → 绑区或新建 +- type=2 节点 → Upsert base_device, DeviceGroup=IoT设备, ExtraData 存点位属性 - 模式: FullReplace, 频率限制: 2次/秒 ### Owl → 设备 -- `GET /devices` → Upsert Base_Device (Category=1, IsParent=1) -- `GET /channels` → Upsert Base_Device (ParentDeviceId=NVR) -- Owl 无区域概念 → RegionId=NULL, 管理员手动分配 +- GET /devices → Upsert base_device (DeviceGroup=视频设备, IsParent=是) +- GET /channels → Upsert base_device (ParentDeviceId=NVR) + video_channel +- Owl 无区域概念 → PointId=NULL, 管理员手动分配 - 模式: Merge ### 反方向写回 @@ -230,60 +329,62 @@ Controller 路径: `Controllers/Warehouse/Partial/DeviceManagerController.cs` --- -## 六、部署拓扑 +## 七、部署拓扑 ``` -Docker: Owl+ZLM (:80,:5060) │ Docker: MC4.0 (:3000) - │ - ┌──────────────┴──────────────┐ - │ IntegrationGateway :5100 │ - └──────────────┬──────────────┘ - │ - ┌──────────────┴──────────────┐ - │ VolPro.WebApi :9100 │ - │ MySQL / Redis │ - └──────────────┬──────────────┘ - │ - ┌──────────────────┴──────────────────┐ - │ web.vite :9000 warehouse :9200 │ - └─────────────────────────────────────┘ +Docker: Owl+ZLM (:80) MC4.0-1 (:3000) MC4.0-2 (:3000) + | | | + +----------+-----------+-------------------+ + | + +----------+-----------+ + | Gateway gw-31ku | Gateway gw-11ku + | :5100 | :5101 + +----------+-----------+ + | + +----------+-----------+ + | VolPro.WebApi | + | :9100 | + | MySQL / Redis | + +----------+-----------+ + | + +--------------+---------------+ + | web.vite :9000 warehouse :9200 | + +--------------------------------+ ``` --- -## 七、实施路线 +## 八、实施路线 | 阶段 | 工期 | 内容 | |------|------|------| -| Phase 0 | Day 1-2 | Gateway骨架 + Base_Device建表 + 代码生成 | +| Phase 0 | Day 1-2 | Gateway骨架 + 6张表建表 + 代码生成 | | Phase 1 | Day 3-6 | OwlAdapter + 管理端视频设备页 | | Phase 2 | Day 7-11 | Mc4Adapter + IoT管理 + 区域树匹配 + SignalR | | Phase 3 | Day 12-17 | warehouse端改造 + 全链路联调 | | Phase 4 | Day 18-20 | 验证 + 缓冲 | -**总计: 18-20 个工作日** +总计: 18-20 个工作日 --- -## 八、新增整合流程 +## 九、新增整合流程 以接入「海康门禁」为例: -1. 新建 `IntegrationGateway.Adapters.HikvisionAccess` 项目 -2. 实现 `IHasFlatDevices + IHasAlarms` -3. 注册到 Host -4. 前端新增 `AccessDeviceActions.vue` (~80行) -5. DeviceTable.vue 加 `v-else-if (Category===3)` -6. Vol.Pro 后端零改动 - -**总工作量: 1-2 天** +1. 新建 IntegrationGateway.Adapters.HikvisionAccess 项目 +2. 实现 IHasFlatDevices + IHasAlarms → 设备同步时填充 DeviceGroup=门禁设备 +3. 管理端字典加一条"门禁设备"分组 → 按钮自动出现 +4. Vol.Pro 同步接口零改动(ExtraData 承载门禁字段) +5. 前端新增 AccessDeviceActions.vue (~80行) +总工作量: 1-2 天 --- -## 九、代码组织规范 +## 十、代码组织规范 | 代码类型 | 路径 | 被覆盖? | |----------|------|:---:| -| 第三方对接 | IntegrationGateway/ | ❌ | +| 第三方对接 | gateway/ | ❌ | | 扩展Controller | Controllers/*/Partial/ | ❌ | | Entity扩展 | DomainModels/*/partial/ | ❌ | | 前端业务逻辑 | extension/warehouse/*.jsx | ❌ | @@ -292,5 +393,4 @@ Docker: Owl+ZLM (:80,:5060) │ Docker: MC4.0 (:3000) --- -> **文档结束** -> **取代**: Vol.Pro_MC4.0_整合方案_v1.0、Vol.Pro_Owl_ZLMediaKit_整合方案_v1.0、Vol.Pro_统一设备管理_区域树与地图绑定方案_v1.0、Vol.Pro_整合项目_实施方案_v1.0 +> 取代: V1.0 系列所有整合方案文档 diff --git a/doc/整合方案/SecMPS_最终整合方案_v3.0.md b/doc/整合方案/SecMPS_最终整合方案_v3.0.md new file mode 100644 index 0000000..eef6a0e --- /dev/null +++ b/doc/整合方案/SecMPS_最终整合方案_v3.0.md @@ -0,0 +1,497 @@ +# 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(); +} +``` + +--- + +## 五、管理端统一设备页面 + +### 5.1 操作按钮矩阵 + +| DeviceGroup | 操作按钮 | +|:---:|------| +| 视频设备 | 实时预览 / 云台控制(仅方向键) / 查看回放 / 获取快照 / 同步通道 | +| IoT设备 | 查看实时数据 / 设备控制 / 刷新点位 / 查看告警 | +| 门禁设备 | 远程开门 / 查看记录 / 查看告警 | +| 道闸设备 | 抬杆 / 落杆 / 查看记录 | +| 报警设备 | 查看告警 / 布防撤防 | + +### 5.2 前端按钮路由 + +```javascript +const actionMap = { + '视频设备': VideoDeviceActions, + 'IoT设备': IoTDeviceActions, + '门禁设备': AccessDeviceActions, + '道闸设备': BarrierDeviceActions, + '报警设备': AlarmDeviceActions, +} +``` + +### 5.3 后端 API + +| 接口 | 说明 | +|------|------| +| GET /api/DeviceManager/GetRegionTree | 区域→点位→设备树 | +| GET /api/DeviceManager/GetDevicesByPoint?pointId= | 点位下设备列表(含子设备) | +| PUT /api/DeviceManager/{deviceId} | 更新设备(含地图绑定) | +| POST /api/DeviceManager/SyncFromGateway | 手动同步 | + +--- + +## 六、同步策略 + +### MC4.0 → 区域树+设备 +- type=1 节点 → 名称匹配 warehouse_regions → 绑区或新建 +- type=2 节点 → Upsert base_device (DeviceGroup=IoT设备, ExtraData 存点位属性) +- 子设备在线状态由网关按 MC4.0 数据如实上报,Vol.Pro 不做推断 +- 模式: FullReplace, 频率限制: 2次/秒 + +### Owl → 设备 +- GET /devices → Upsert base_device (DeviceGroup=视频设备, IsParent=是) +- GET /channels → Upsert base_device (ParentDeviceId=NVR) + video_channel 扩展记录 +- Owl 无区域概念 → PointId=NULL, 管理员手动分配 +- 可手动触发 `POST /devices/:id/catalog` 刷新通道目录 +- 模式: Merge + +### 录像同步 +- 管理端点击"查看回放"时,网关实时调 Owl `GET /recordings` → 返回给前端 + 同步写入 video_record +- Phase 3 可选:网关定时(每 10 分钟)后台同步录像记录 + +### Owl AI 事件(可选) +- OwlAdapter 可选实现 IHasAlarms,将 Owl `GET /events` 的 YOLO AI 检测事件映射为 StandardAlarm,走 A4 告警同步 + +### 反方向写回 + +| 操作 | Owl | MC4.0 | +|------|:---:|:-----:| +| 设备改名 | ✅ PUT /devices/:id | ❌ | +| 告警确认 | ⚠️ | ✅ | +| 设备控制 | ✅ PTZ(仅方向键) | ✅ 点位写值 | + +--- + +## 七、部署拓扑 + +``` +Docker: Owl+ZLM (:15123) MC4.0-1 (:3000) MC4.0-2 (:3000) + | | | + +----------+--------------+-------------------+ + | + +----------+-----------+ + | Gateway gw-31ku | Gateway gw-11ku + | :5100 | :5101 + +----------+-----------+ + | + +----------+-----------+ + | VolPro.WebApi | + | :9100 | + | MySQL / Redis | + +----------+-----------+ + | + +--------------+---------------+ + | web.vite :9000 warehouse :9200 | + +--------------------------------+ +``` + +--- + +## 八、实施路线 + +| 阶段 | 工期 | 内容 | +|------|------|------| +| Phase 0 | Day 1-2 | Gateway骨架 + 6张表建表 + 代码生成 + 字典初始化 | +| Phase 1 | Day 3-6 | OwlAdapter + 管理端视频设备页 + [可选]AI事件接入 | +| Phase 2 | Day 7-11 | Mc4Adapter + IoT管理 + 区域树匹配 + SignalR | +| Phase 3 | Day 12-17 | warehouse端改造 + 全链路联调 | +| Phase 4 | Day 18-20 | 验证 + 缓冲 | + +总计: 18-20 个工作日 + +--- + +## 九、新增整合流程 + +以接入「海康门禁」为例: +1. 新建 IntegrationGateway.Adapters.HikvisionAccess 项目 +2. 实现 IHasFlatDevices + IHasAlarms → 设备同步时填充 DeviceGroup=门禁设备 +3. 管理端字典加一条"门禁设备"分组 → 按钮自动出现 +4. Vol.Pro 同步接口零改动(ExtraData 承载门禁字段) +5. 前端新增 AccessDeviceActions.vue (~80行) +总工作量: 1-2 天 + +--- + +## 十、代码组织规范 + +| 代码类型 | 路径 | 被覆盖? | +|----------|------|:---:| +| 第三方对接 | gateway/ | ❌ | +| 扩展Controller | Controllers/*/Partial/ | ❌ | +| Entity扩展 | DomainModels/*/partial/ | ❌ | +| 前端业务逻辑 | extension/warehouse/*.jsx | ❌ | +| 自定义页面 | views/warehouse/DeviceManager/ | ❌ | +| 自动生成代码 | 生成器默认路径 | ✅ | + +--- + +## 附录 A:字典初始化清单 + +Phase 0 建表后需在 Vol.Pro 管理端创建以下数据字典: + +| 字典名称 | 字典值 | +|----------|--------| +| 设备种类 | 门磁/空调/智能断路器/人行道闸/车辆道闸/485钥匙柜/网络钥匙柜/紧急报警按钮/红外报警器/门禁一体机/除湿_恒湿机/空调控制器/烟雾报警器/气体报警器/温湿度变送器/摄像机/硬盘录像机/动环采集器 | +| 设备分组 | 视频设备/IoT设备/门禁设备/道闸设备/报警设备 | +| 是否父设备 | 是/否 | +| 在线状态 | 在线/离线 | +| 启用状态 | 启用/禁用 | +| 是否控制点 | 只读/可写 | +| 告警等级 | 提示/普通/重要/紧急 | +| 告警状态 | 未确认/已确认/已结束 | + +--- + +## 附录 B:AdapterCode 格式规范 + +``` +格式: {AdapterType}:{InstanceName} +示例: "MC4:31ku"、"Owl:main"、"HikvisionISC:center" +规则: + - AdapterType: 网关注册的 Adapter 类名,仅字母数字 + - InstanceName: 网关实例名称,仅字母数字下划线 + - 分隔符: ':' + - base_device.AdapterCode 存储完整双段标识 +``` + +--- + +> **版本历史**: +> - v1.0 (2026-04-29) — 初始 Owl+MC4 独立方案 +> - v2.0 (2026-05-16) — 整合 Gateway 架构 + 数据字典 + ExtraData JSON +> - v3.0 (2026-05-16) — Owl/ZLMediaKit 双文档验证 + 14 项修正(P0/P1/P2 修复) diff --git a/gateway/IntegrationGateway.sln b/gateway/IntegrationGateway.sln deleted file mode 100644 index 5018d6c..0000000 --- a/gateway/IntegrationGateway.sln +++ /dev/null @@ -1,54 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Host", "src\IntegrationGateway.Host\IntegrationGateway.Host.csproj", "{8F605B6B-5217-4119-A75E-05FFB4E42347}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Core", "src\IntegrationGateway.Core\IntegrationGateway.Core.csproj", "{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x64.ActiveCfg = Debug|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x64.Build.0 = Debug|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x86.ActiveCfg = Debug|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x86.Build.0 = Debug|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|Any CPU.Build.0 = Release|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x64.ActiveCfg = Release|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x64.Build.0 = Release|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x86.ActiveCfg = Release|Any CPU - {8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x86.Build.0 = Release|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x64.ActiveCfg = Debug|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x64.Build.0 = Debug|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x86.ActiveCfg = Debug|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x86.Build.0 = Debug|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|Any CPU.Build.0 = Release|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x64.ActiveCfg = Release|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x64.Build.0 = Release|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x86.ActiveCfg = Release|Any CPU - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {8F605B6B-5217-4119-A75E-05FFB4E42347} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - {D1F85A10-E56A-44E8-96B8-7BC3C91E513B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - EndGlobalSection -EndGlobal diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs deleted file mode 100644 index 4e7e41b..0000000 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IAcceptsMetadataPush.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace IntegrationGateway.Core.Abstractions; - -public interface IAcceptsMetadataPush : IIntegrationAdapter -{ - Task PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes); -} - -public class MetadataChangeSet -{ - public string? Name { get; set; } - public string? IpAddress { get; set; } - public int? Port { get; set; } - public int? StreamMode { get; set; } -} - -public class MetadataPushResult -{ - public bool Success { get; set; } - public List RejectedFields { get; set; } = new(); - public string? Reason { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs deleted file mode 100644 index 038c5f0..0000000 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasAlarms.cs +++ /dev/null @@ -1,12 +0,0 @@ -using IntegrationGateway.Core.Models; - -namespace IntegrationGateway.Core.Abstractions; - -public interface IHasAlarms : IIntegrationAdapter -{ - Task> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, - int? confirmState = null, int? endState = null, List? levels = null); - Task ConfirmAlarmAsync(string alarmId); - Task EndAlarmAsync(string alarmId); - Task GetPendingAlarmCountAsync(); -} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs deleted file mode 100644 index 830106a..0000000 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasFlatDevices.cs +++ /dev/null @@ -1,12 +0,0 @@ -using IntegrationGateway.Core.Models; - -namespace IntegrationGateway.Core.Abstractions; - -public interface IHasFlatDevices : IIntegrationAdapter -{ - Task> GetDevicesAsync(int page, int size, string? keyword = null); - Task GetDeviceAsync(string sourceDeviceId); - Task> GetAllDevicesAsync(); - Task> GetChannelsAsync(int page, int size, string? parentDeviceId = null); - Task> GetAllChannelsAsync(); -} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs deleted file mode 100644 index c565f01..0000000 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasOwnDeviceTree.cs +++ /dev/null @@ -1,8 +0,0 @@ -using IntegrationGateway.Core.Models; - -namespace IntegrationGateway.Core.Abstractions; - -public interface IHasOwnDeviceTree : IIntegrationAdapter -{ - Task> GetObjectTreeAsync(); -} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs deleted file mode 100644 index 796a720..0000000 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasPoints.cs +++ /dev/null @@ -1,10 +0,0 @@ -using IntegrationGateway.Core.Models; - -namespace IntegrationGateway.Core.Abstractions; - -public interface IHasPoints : IIntegrationAdapter -{ - Task> GetRealtimeValuesAsync(string sourceDeviceId); - Task> GetMultiPointValuesAsync(List<(string DeviceId, int PointIndex)> points); - Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value); -} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs deleted file mode 100644 index ad59126..0000000 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IHasStreams.cs +++ /dev/null @@ -1,14 +0,0 @@ -using IntegrationGateway.Core.Models; - -namespace IntegrationGateway.Core.Abstractions; - -public interface IHasStreams : IIntegrationAdapter -{ - Task GetLiveUrlAsync(string channelId); - Task GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end); - Task StopPlayAsync(string channelId); - Task GetSnapshotAsync(string channelId); - Task PtzControlAsync(string channelId, string direction, float speed); - Task PtzStopAsync(string channelId); - Task> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size); -} diff --git a/gateway/src/IntegrationGateway.Core/Abstractions/IIntegrationAdapter.cs b/gateway/src/IntegrationGateway.Core/Abstractions/IIntegrationAdapter.cs deleted file mode 100644 index 51f5ad1..0000000 --- a/gateway/src/IntegrationGateway.Core/Abstractions/IIntegrationAdapter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using IntegrationGateway.Core.Models; - -namespace IntegrationGateway.Core.Abstractions; - -public interface IIntegrationAdapter -{ - string AdapterCode { get; } - string DisplayName { get; } - AdapterCapabilities Capabilities { get; } - Task HealthCheckAsync(); - Task InitializeAsync(); -} diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs deleted file mode 100644 index e6edeb2..0000000 --- a/gateway/src/IntegrationGateway.Core/Infrastructure/AdapterRegistry.cs +++ /dev/null @@ -1,27 +0,0 @@ -using IntegrationGateway.Core.Abstractions; - -namespace IntegrationGateway.Core.Infrastructure; - -public class AdapterRegistry -{ - private readonly Dictionary _adapters = new(); - - public void Register(IIntegrationAdapter adapter) - { - _adapters[adapter.AdapterCode] = adapter; - } - - public IIntegrationAdapter? Get(string adapterCode) - { - _adapters.TryGetValue(adapterCode, out var adapter); - return adapter; - } - - public IEnumerable GetAll() => _adapters.Values; - - public async Task InitializeAllAsync() - { - foreach (var adapter in _adapters.Values) - await adapter.InitializeAsync(); - } -} diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs deleted file mode 100644 index aeefafd..0000000 --- a/gateway/src/IntegrationGateway.Core/Infrastructure/RateLimiter.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace IntegrationGateway.Core.Infrastructure; - -public class RateLimiter -{ - private readonly SemaphoreSlim _semaphore; - private readonly int _minIntervalMs; - private DateTime _lastRequest = DateTime.MinValue; - - public RateLimiter(int maxCallsPerSecond) - { - _semaphore = new SemaphoreSlim(maxCallsPerSecond, maxCallsPerSecond); - _minIntervalMs = 1000 / maxCallsPerSecond; - } - - public async Task WaitAsync() - { - await _semaphore.WaitAsync(); - try - { - var elapsed = (int)(DateTime.UtcNow - _lastRequest).TotalMilliseconds; - if (elapsed < _minIntervalMs) - await Task.Delay(_minIntervalMs - elapsed); - _lastRequest = DateTime.UtcNow; - } - finally - { - _semaphore.Release(); - } - } -} diff --git a/gateway/src/IntegrationGateway.Core/Infrastructure/TokenManager.cs b/gateway/src/IntegrationGateway.Core/Infrastructure/TokenManager.cs deleted file mode 100644 index a8d5731..0000000 --- a/gateway/src/IntegrationGateway.Core/Infrastructure/TokenManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; - -namespace IntegrationGateway.Core.Infrastructure; - -public class TokenManager -{ - private readonly IMemoryCache _cache; - private static readonly SemaphoreSlim _semaphore = new(1, 1); - - public TokenManager(IMemoryCache cache) => _cache = cache; - - public async Task GetAsync(string key) - { - _cache.TryGetValue($"token_{key}", out string? token); - return token; - } - - public async Task SetAsync(string key, string token, TimeSpan expiresIn) - { - await _semaphore.WaitAsync(); - try - { - _cache.Set($"token_{key}", token, expiresIn * 0.9); - } - finally - { - _semaphore.Release(); - } - } - - public void Remove(string key) => _cache.Remove($"token_{key}"); -} diff --git a/gateway/src/IntegrationGateway.Core/IntegrationGateway.Core.csproj b/gateway/src/IntegrationGateway.Core/IntegrationGateway.Core.csproj deleted file mode 100644 index 985d3af..0000000 --- a/gateway/src/IntegrationGateway.Core/IntegrationGateway.Core.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - diff --git a/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs b/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs deleted file mode 100644 index 67b86df..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/AdapterCapabilities.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class AdapterCapabilities -{ - public bool HasObjectTree { get; set; } - public bool HasFlatDevices { get; set; } - public bool HasPoints { get; set; } - public bool HasStreams { get; set; } - public bool HasAlarms { get; set; } - public bool HasRecordings { get; set; } - public bool HasPtz { get; set; } - public bool AcceptsControl { get; set; } - public bool AcceptsMetadataPush { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs b/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs deleted file mode 100644 index fe8e09f..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/DeviceTreeNode.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class DeviceTreeNode -{ - public int SourceId { get; set; } - public string Name { get; set; } = ""; - public int NodeType { get; set; } - public int ObjectType { get; set; } - public string? Tag { get; set; } - public Dictionary Option { get; set; } = new(); - public List Children { get; set; } = new(); - public string? ParentPath { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs b/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs deleted file mode 100644 index 90e077a..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/PagedResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class PagedResult -{ - public List Items { get; set; } = new(); - public int Total { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/PointValue.cs b/gateway/src/IntegrationGateway.Core/Models/PointValue.cs deleted file mode 100644 index 133f376..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/PointValue.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class PointValue -{ - public string SourceDeviceId { get; set; } = ""; - public int PointIndex { get; set; } - public double Value { get; set; } - public string? UpdateTime { get; set; } - public int Interval { get; set; } - public bool IsValid { get; set; } = true; -} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs b/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs deleted file mode 100644 index aac7d89..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/StandardAlarm.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class StandardAlarm -{ - public string AlarmId { get; set; } = ""; - public string? DeviceId { get; set; } - public string AdapterCode { get; set; } = ""; - public string Level { get; set; } = ""; - public string Title { get; set; } = ""; - public string? Content { get; set; } - public DateTime OccurTime { get; set; } - public string Status { get; set; } = "Active"; - public string? PointCode { get; set; } - public double? ThresholdValue { get; set; } - public double? ActualValue { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs b/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs deleted file mode 100644 index 5dddad7..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/StandardDevice.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class StandardDevice -{ - public string SourceId { get; set; } = ""; - public string AdapterCode { get; set; } = ""; - public string Name { get; set; } = ""; - public string Category { get; set; } = ""; - public string? Type { get; set; } - public string? IpAddress { get; set; } - public int? Port { get; set; } - public bool IsOnline { get; set; } - public string? Location { get; set; } - public double? Lat { get; set; } - public double? Lng { get; set; } - public string? MapModelId { get; set; } - public int ChannelCount { get; set; } - public bool IsParent { get; set; } - public string? ParentSourceId { get; set; } - public string? SourcePath { get; set; } - public Dictionary Extra { get; set; } = new(); - public DateTime LastSyncTime { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs b/gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs deleted file mode 100644 index dc5c555..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/StandardPoint.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class StandardPoint -{ - public string SourceDeviceId { get; set; } = ""; - public int PointIndex { get; set; } - public int PointType { get; set; } - public string? PointTag { get; set; } - public string PointName { get; set; } = ""; - public string? PointDesc { get; set; } - public string? Unit { get; set; } - public bool IsControlPoint { get; set; } - public Dictionary? RawOption { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs b/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs deleted file mode 100644 index 8c3ade1..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/StandardRecording.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class StandardRecording -{ - public string Id { get; set; } = ""; - public string ChannelId { get; set; } = ""; - public DateTime StartedAt { get; set; } - public DateTime EndedAt { get; set; } - public double Duration { get; set; } - public string? FilePath { get; set; } - public long Size { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs b/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs deleted file mode 100644 index 339583a..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/StreamUrls.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class StreamUrls -{ - public string? WsFlv { get; set; } - public string? HttpFlv { get; set; } - public string? Hls { get; set; } - public string? WebRtc { get; set; } - public string? Rtmp { get; set; } - public string? Rtsp { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Core/Models/SyncReport.cs b/gateway/src/IntegrationGateway.Core/Models/SyncReport.cs deleted file mode 100644 index 572bd75..0000000 --- a/gateway/src/IntegrationGateway.Core/Models/SyncReport.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace IntegrationGateway.Core.Models; - -public class SyncReport -{ - public string AdapterCode { get; set; } = ""; - public int Added { get; set; } - public int Updated { get; set; } - public int Skipped { get; set; } - public int Removed { get; set; } - public List Errors { get; set; } = new(); - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/AlarmsController.cs b/gateway/src/IntegrationGateway.Host/Controllers/AlarmsController.cs deleted file mode 100644 index 3c66fcf..0000000 --- a/gateway/src/IntegrationGateway.Host/Controllers/AlarmsController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using IntegrationGateway.Core.Abstractions; -using IntegrationGateway.Core.Infrastructure; -using Microsoft.AspNetCore.Mvc; - -namespace IntegrationGateway.Host.Controllers; - -[ApiController] -[Route("api/gateway/alarms")] -public class AlarmsController : ControllerBase -{ - private readonly AdapterRegistry _registry; - - public AlarmsController(AdapterRegistry registry) => _registry = registry; - - [HttpGet("{adapter}")] - public async Task GetAlarms(string adapter, [FromQuery] DateTime from, [FromQuery] DateTime to, - [FromQuery] int page = 1, [FromQuery] int size = 50) - { - var a = _registry.Get(adapter); - if (a is not IHasAlarms al) return NotFound(); - return Ok(await al.GetAlarmsAsync(page, size, from, to)); - } - - [HttpPost("{adapter}/{alarmId}/confirm")] - public async Task Confirm(string adapter, string alarmId) - { - var a = _registry.Get(adapter); - if (a is not IHasAlarms al) return NotFound(); - await al.ConfirmAlarmAsync(alarmId); - return Ok(new { status = "confirmed" }); - } -} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/DevicesController.cs b/gateway/src/IntegrationGateway.Host/Controllers/DevicesController.cs deleted file mode 100644 index 673f279..0000000 --- a/gateway/src/IntegrationGateway.Host/Controllers/DevicesController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using IntegrationGateway.Core.Abstractions; -using IntegrationGateway.Core.Infrastructure; -using IntegrationGateway.Core.Models; -using Microsoft.AspNetCore.Mvc; - -namespace IntegrationGateway.Host.Controllers; - -[ApiController] -[Route("api/gateway/devices")] -public class DevicesController : ControllerBase -{ - private readonly AdapterRegistry _registry; - - public DevicesController(AdapterRegistry registry) => _registry = registry; - - [HttpGet] - public async Task GetDevices([FromQuery] string adapter, [FromQuery] int page = 1, [FromQuery] int size = 50) - { - var a = _registry.Get(adapter); - if (a is not IHasFlatDevices f) return NotFound("Adapter not found or unsupported"); - return Ok(await f.GetDevicesAsync(page, size)); - } - - [HttpGet("{adapter}/{deviceId}")] - public async Task GetDevice(string adapter, string deviceId) - { - var a = _registry.Get(adapter); - if (a is not IHasFlatDevices f) return NotFound(); - var d = await f.GetDeviceAsync(deviceId); - return d is null ? NotFound() : Ok(d); - } -} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/HealthController.cs b/gateway/src/IntegrationGateway.Host/Controllers/HealthController.cs deleted file mode 100644 index de3cda7..0000000 --- a/gateway/src/IntegrationGateway.Host/Controllers/HealthController.cs +++ /dev/null @@ -1,22 +0,0 @@ -using IntegrationGateway.Core.Infrastructure; -using Microsoft.AspNetCore.Mvc; - -namespace IntegrationGateway.Host.Controllers; - -[ApiController] -[Route("api/gateway/health")] -public class HealthController : ControllerBase -{ - private readonly AdapterRegistry _registry; - - public HealthController(AdapterRegistry registry) => _registry = registry; - - [HttpGet] - public async Task Get() - { - var status = new Dictionary(); - foreach (var adapter in _registry.GetAll()) - status[adapter.AdapterCode] = await adapter.HealthCheckAsync(); - return Ok(status); - } -} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/PointsController.cs b/gateway/src/IntegrationGateway.Host/Controllers/PointsController.cs deleted file mode 100644 index 9ad0810..0000000 --- a/gateway/src/IntegrationGateway.Host/Controllers/PointsController.cs +++ /dev/null @@ -1,38 +0,0 @@ -using IntegrationGateway.Core.Abstractions; -using IntegrationGateway.Core.Infrastructure; -using Microsoft.AspNetCore.Mvc; - -namespace IntegrationGateway.Host.Controllers; - -[ApiController] -[Route("api/gateway/realtime")] -public class PointsController : ControllerBase -{ - private readonly AdapterRegistry _registry; - - public PointsController(AdapterRegistry registry) => _registry = registry; - - [HttpGet("{adapter}/{deviceId}")] - public async Task GetRealtime(string adapter, string deviceId) - { - var a = _registry.Get(adapter); - if (a is not IHasPoints p) return NotFound(); - return Ok(await p.GetRealtimeValuesAsync(deviceId)); - } - - [HttpPost("{adapter}/control")] - public async Task Control(string adapter, [FromBody] ControlRequest req) - { - var a = _registry.Get(adapter); - if (a is not IHasPoints p) return NotFound(); - await p.SetPointValueAsync(req.DeviceId, req.PointIndex, req.Value); - return Ok(new { status = "sent" }); - } -} - -public class ControlRequest -{ - public string DeviceId { get; set; } = ""; - public int PointIndex { get; set; } - public double Value { get; set; } -} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/StreamsController.cs b/gateway/src/IntegrationGateway.Host/Controllers/StreamsController.cs deleted file mode 100644 index 3a01d61..0000000 --- a/gateway/src/IntegrationGateway.Host/Controllers/StreamsController.cs +++ /dev/null @@ -1,37 +0,0 @@ -using IntegrationGateway.Core.Abstractions; -using IntegrationGateway.Core.Infrastructure; -using Microsoft.AspNetCore.Mvc; - -namespace IntegrationGateway.Host.Controllers; - -[ApiController] -[Route("api/gateway/streams")] -public class StreamsController : ControllerBase -{ - private readonly AdapterRegistry _registry; - - public StreamsController(AdapterRegistry registry) => _registry = registry; - - [HttpGet("{adapter}/{channelId}/live")] - public async Task GetLive(string adapter, string channelId) - { - var a = _registry.Get(adapter); - if (a is not IHasStreams s) return NotFound(); - return Ok(await s.GetLiveUrlAsync(channelId)); - } - - [HttpPost("{adapter}/{channelId}/ptz")] - public async Task Ptz(string adapter, string channelId, [FromBody] PtzRequest req) - { - var a = _registry.Get(adapter); - if (a is not IHasStreams s) return NotFound(); - await s.PtzControlAsync(channelId, req.Direction, req.Speed); - return Ok(); - } -} - -public class PtzRequest -{ - public string Direction { get; set; } = "stop"; - public float Speed { get; set; } = 0.5f; -} diff --git a/gateway/src/IntegrationGateway.Host/Controllers/SyncController.cs b/gateway/src/IntegrationGateway.Host/Controllers/SyncController.cs deleted file mode 100644 index e466beb..0000000 --- a/gateway/src/IntegrationGateway.Host/Controllers/SyncController.cs +++ /dev/null @@ -1,59 +0,0 @@ -using IntegrationGateway.Core.Abstractions; -using IntegrationGateway.Core.Infrastructure; -using IntegrationGateway.Core.Models; -using Microsoft.AspNetCore.Mvc; - -namespace IntegrationGateway.Host.Controllers; - -[ApiController] -[Route("api/gateway")] -public class SyncController : ControllerBase -{ - private readonly AdapterRegistry _registry; - - public SyncController(AdapterRegistry registry) => _registry = registry; - - [HttpGet("devices/sync")] - public async Task SyncDevices([FromQuery] string adapter) - { - var a = _registry.Get(adapter) ?? throw new InvalidOperationException($"Adapter '{adapter}' not found"); - - var report = new SyncReport { AdapterCode = adapter, StartTime = DateTime.UtcNow }; - - if (a is IHasFlatDevices f) - report = await SyncFlatDevices(f, report); - else if (a is IHasOwnDeviceTree t) - report = await SyncTreeDevices(t, report); - else - return BadRequest("Adapter does not support device sync"); - - report.EndTime = DateTime.UtcNow; - return Ok(report); - } - - private async Task SyncFlatDevices(IHasFlatDevices adapter, SyncReport report) - { - var devices = await adapter.GetAllDevicesAsync(); - report.Added = devices.Count; - return report; - } - - private async Task SyncTreeDevices(IHasOwnDeviceTree adapter, SyncReport report) - { - var tree = await adapter.GetObjectTreeAsync(); - int count = CountDeviceNodes(tree); - report.Added = count; - return report; - } - - private int CountDeviceNodes(List nodes) - { - int count = 0; - foreach (var n in nodes) - { - if (n.NodeType == 2) count++; - count += CountDeviceNodes(n.Children); - } - return count; - } -} diff --git a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj deleted file mode 100644 index 441a01e..0000000 --- a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - diff --git a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.http b/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.http deleted file mode 100644 index bd723c4..0000000 --- a/gateway/src/IntegrationGateway.Host/IntegrationGateway.Host.http +++ /dev/null @@ -1,6 +0,0 @@ -@IntegrationGateway.Host_HostAddress = http://localhost:5294 - -GET {{IntegrationGateway.Host_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/gateway/src/IntegrationGateway.Host/Program.cs b/gateway/src/IntegrationGateway.Host/Program.cs deleted file mode 100644 index 92ca96f..0000000 --- a/gateway/src/IntegrationGateway.Host/Program.cs +++ /dev/null @@ -1,14 +0,0 @@ -using IntegrationGateway.Core.Infrastructure; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddControllers(); -builder.Services.AddMemoryCache(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); - -var app = builder.Build(); - -app.MapControllers(); - -app.Run(); diff --git a/gateway/src/IntegrationGateway.Host/Properties/launchSettings.json b/gateway/src/IntegrationGateway.Host/Properties/launchSettings.json deleted file mode 100644 index 3c5b36c..0000000 --- a/gateway/src/IntegrationGateway.Host/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:35846", - "sslPort": 0 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5294", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/gateway/src/IntegrationGateway.Host/appsettings.Development.json b/gateway/src/IntegrationGateway.Host/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/gateway/src/IntegrationGateway.Host/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/gateway/src/IntegrationGateway.Host/appsettings.json b/gateway/src/IntegrationGateway.Host/appsettings.json deleted file mode 100644 index 10f68b8..0000000 --- a/gateway/src/IntegrationGateway.Host/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/web.vite/public/static/login_bg.png b/web.vite/public/static/login_bg.png index 0e50abc..d00e573 100644 Binary files a/web.vite/public/static/login_bg.png and b/web.vite/public/static/login_bg.png differ