Compare commits

..

23 Commits

Author SHA1 Message Date
07dd459705 Phase0_delete_old_gateway 2026-05-16 15:12:43 +08:00
fcd1d39641 v3.0_manual_with_gateway_assessment 2026-05-16 15:09:12 +08:00
65f4b793d3 Gateway_VolPro_API_manual 2026-05-16 14:47:33 +08:00
1c2d33b341 v3.0_final_tech_assessment 2026-05-16 14:41:18 +08:00
fe90fbea3a v3.0_manual_and_db 2026-05-16 14:20:21 +08:00
30e4187e34 v3.0_final_14_fixes_applied 2026-05-16 13:52:19 +08:00
eda8572005 Final_assessment_report 2026-05-16 13:34:03 +08:00
69a18d38d3 Phase0_gateway_api_specs 2026-05-16 12:56:12 +08:00
b5195aa4d4 Phase0_sync_plan_with_db_changes 2026-05-16 12:10:18 +08:00
7801cf5730 Phase0_PointId_replace_RegionId 2026-05-16 12:06:54 +08:00
bf3cbc71ae Phase0_6tables_ExtraData_DGroup 2026-05-16 11:42:23 +08:00
d669b56569 Phase0_dict_comment_details 2026-05-16 11:16:14 +08:00
8a9a9a7338 Phase0_lowercase_varchar_dict 2026-05-16 11:10:50 +08:00
d2071a610b Phase0_DeviceCategory_dict_val 2026-05-16 11:03:31 +08:00
a8507aaa12 Phase0_DeviceCategory_varchar 2026-05-16 10:48:44 +08:00
906700e0fe v2.0_final_synced_with_db_and_gateway 2026-05-16 10:45:20 +08:00
7c69d9c55f Phase0_gateway_nodes_table 2026-05-16 05:38:32 +08:00
839d05f75c Phase0_gateway_config 2026-05-16 04:48:29 +08:00
65cfa40e1f Phase0_remove_DeviceType 2026-05-16 04:42:27 +08:00
4cbe12ee5d Phase0_comment_fix 2026-05-16 04:26:29 +08:00
d204727a07 Phase0_7tables_drop_point_table 2026-05-16 03:24:29 +08:00
d92c3da89a Phase0_no_constraints_index_only 2026-05-16 02:30:39 +08:00
8d6cc6fd23 Phase0_alarm_generic 2026-05-16 02:18:02 +08:00
43 changed files with 2432 additions and 970 deletions

View File

@@ -1,24 +1,29 @@
-- ============================================
-- SecMPS v2.0 数据库建表脚本
-- SecMPS v3.0 数据库建表脚本6张表
-- 数据库: gljs_main
-- 所有主键: INT AUTO_INCREMENT
-- 扩展表已合并到 Base_Device.ExtraData(JSON)
-- ============================================
USE gljs_main;
-- ============================================
-- 1. 统一设备主表
DROP TABLE IF EXISTS Base_Device;
CREATE TABLE Base_Device (
-- 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 '来源适配器(owl/mc4)',
AdapterCode NVARCHAR(50) NOT NULL COMMENT '来源适配器(类型:实例)',
SourceId NVARCHAR(100) NOT NULL COMMENT '源系统设备ID',
DeviceCategory INT NOT NULL DEFAULT 1 COMMENT '设备类(1视频/2IoT/3门禁/4道闸/5报警)',
DeviceType NVARCHAR(50) COMMENT '设备细分类型(GB28181/TempSensor)',
RegionId INT NULL COMMENT '所属区域ID',
IsParent TINYINT NOT NULL DEFAULT 0 COMMENT '是否父设备(0叶子/1可展开)',
ParentDeviceId INT NULL COMMENT '父设备ID',
IsOnline TINYINT NOT NULL DEFAULT 0 COMMENT '在线状态(0离线/1在线)',
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 '安装位置',
@@ -27,11 +32,9 @@ CREATE TABLE Base_Device (
MapModelId NVARCHAR(100) COMMENT '三维地图模型ID',
MapModelScale FLOAT DEFAULT 1.0 COMMENT '模型缩放比例',
MapModelRotation NVARCHAR(100) COMMENT '模型旋转角度(JSON)',
ExtraData TEXT COMMENT '源系统原始数据JSON',
LocalOverrides TEXT COMMENT '本地覆盖字段JSON',
SyncVersion BIGINT DEFAULT 0 COMMENT '同步版本号',
ExtraData TEXT COMMENT '适配器扩展数据JSON(Owl/MC4/门禁字段均存于此)',
LastSyncTime DATETIME COMMENT '上次同步时间',
Enable TINYINT DEFAULT 1 COMMENT '启用(0禁用/1启用)',
Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)',
Remark NVARCHAR(500) COMMENT '备注',
CreateID INT COMMENT '创建人ID',
Creator NVARCHAR(50) COMMENT '创建人',
@@ -40,56 +43,40 @@ CREATE TABLE Base_Device (
Modifier NVARCHAR(50) COMMENT '修改人',
ModifyDate DATETIME COMMENT '修改时间',
PRIMARY KEY (DeviceId),
UNIQUE INDEX IX_Base_Device_Adapter_Source (AdapterCode, SourceId),
INDEX IX_Base_Device_RegionId (RegionId),
INDEX IX_Base_Device_ParentId (ParentDeviceId)
INDEX IX_Sync (AdapterCode, SourceId),
INDEX IX_Point (PointId),
INDEX IX_Parent (ParentDeviceId),
INDEX IX_Gateway (GatewayNodeId),
INDEX IX_Group (DeviceGroup)
) COMMENT '统一设备主表';
-- 2. 视频设备扩展表
DROP TABLE IF EXISTS Device_Video_Ext;
CREATE TABLE Device_Video_Ext (
ExtId INT AUTO_INCREMENT COMMENT '扩展记录ID',
DeviceId INT NOT NULL COMMENT '关联设备ID',
OwlDeviceId NVARCHAR(64) NOT NULL COMMENT 'Owl系统设备ID',
Protocol INT DEFAULT 1 COMMENT '接入协议(1GB28181/2ONVIF/3RTMP/4RTSP)',
Manufacturer NVARCHAR(100) COMMENT '厂商',
Model NVARCHAR(100) COMMENT '设备型号',
ChannelCount INT DEFAULT 0 COMMENT '通道数量',
OwlStatus NVARCHAR(500) COMMENT 'Owl原始状态JSON',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (ExtId),
UNIQUE INDEX IX_VideoExt_Owl (OwlDeviceId),
INDEX IX_VideoExt_Device (DeviceId)
) COMMENT '视频设备扩展表';
-- 3. 视频通道表
DROP TABLE IF EXISTS Video_Channel;
CREATE TABLE Video_Channel (
ChannelId INT AUTO_INCREMENT COMMENT '通道ID',
-- ============================================
-- 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 '关联设备ID',
ChannelName NVARCHAR(100) NOT NULL COMMENT '通道名称',
ChannelNo INT DEFAULT 0 COMMENT '通道编号',
DeviceId INT NOT NULL COMMENT '关联Base_Device设备ID',
OwlStreamApp NVARCHAR(50) COMMENT 'Owl流应用名',
OwlStreamName NVARCHAR(100) COMMENT 'Owl流名称',
HasPtz TINYINT DEFAULT 0 COMMENT '是否支持云台(0否/1是)',
HasRecording TINYINT DEFAULT 0 COMMENT '是否支持录像(0否/1是)',
RecordMode INT DEFAULT 0 COMMENT '录像模式(0不录像/1设备录像/2中心录像)',
IsOnline TINYINT DEFAULT 0 COMMENT '在线状态(0离线/1在线)',
HasPtz TINYINT DEFAULT 0 COMMENT '是否支持云台',
HasRecording TINYINT DEFAULT 0 COMMENT '是否支持录像',
RecordMode INT DEFAULT 0 COMMENT '录像模式',
SnapshotUrl NVARCHAR(500) COMMENT '快照地址',
Location NVARCHAR(200) COMMENT '安装位置',
Lat DOUBLE COMMENT '纬度',
Lng DOUBLE COMMENT '经度',
Enable TINYINT DEFAULT 1 COMMENT '启用(0禁用/1启用)',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (ChannelId),
UNIQUE INDEX IX_Channel_Owl (OwlChannelId),
INDEX IX_Channel_Device (DeviceId)
INDEX IX_Device (DeviceId),
INDEX IX_Owl (OwlChannelId)
) COMMENT '视频通道表';
-- 4. 录像记录表
DROP TABLE IF EXISTS Video_Record;
CREATE TABLE Video_Record (
-- ============================================
-- 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',
@@ -102,83 +89,77 @@ CREATE TABLE Video_Record (
FileSize BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (RecordId),
INDEX IX_Record_Channel (ChannelId),
INDEX IX_Record_Time (StartedAt)
INDEX IX_Channel (ChannelId),
INDEX IX_Time (StartedAt)
) COMMENT '录像记录表';
-- 5. IoT设备扩展表
DROP TABLE IF EXISTS Device_IoT_Ext;
CREATE TABLE Device_IoT_Ext (
ExtId INT AUTO_INCREMENT COMMENT '扩展记录ID',
DeviceId INT NOT NULL COMMENT '关联设备ID',
Mc4DeviceId INT NOT NULL COMMENT 'MC4.0设备ID',
ObjectType INT COMMENT 'MC4.0对象类型',
Tag NVARCHAR(100) COMMENT '设备标签',
ParentId INT COMMENT 'MC4.0父级ID',
Mc4Option NVARCHAR(500) COMMENT 'MC4.0原始配置JSON',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (ExtId),
UNIQUE INDEX IX_IoTExt_Mc4 (Mc4DeviceId),
INDEX IX_IoTExt_Device (DeviceId)
) COMMENT '采集设备扩展表';
-- 6. 设备点位表
DROP TABLE IF EXISTS IoT_DevicePoint;
CREATE TABLE IoT_DevicePoint (
PointId INT AUTO_INCREMENT COMMENT '点位ID',
DeviceId INT NOT NULL COMMENT '关联设备ID',
Mc4DeviceId INT NOT NULL COMMENT 'MC4.0设备ID',
PointIndex INT NOT NULL COMMENT '点位索引',
PointType INT COMMENT '点位类型',
PointTag NVARCHAR(100) COMMENT '点位标签',
PointName NVARCHAR(100) NOT NULL COMMENT '点位名称',
PointDesc NVARCHAR(200) COMMENT '点位描述',
Unit NVARCHAR(50) COMMENT '单位(℃/%/V)',
IsControlPoint TINYINT DEFAULT 0 COMMENT '是否控制点(0只读/1可写)',
Mc4Option NVARCHAR(500) COMMENT 'MC4.0原始配置JSON',
Enable TINYINT DEFAULT 1 COMMENT '启用(0禁用/1启用)',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (PointId),
UNIQUE INDEX IX_Point_Mc4 (Mc4DeviceId, PointIndex),
INDEX IX_Point_Device (DeviceId)
) COMMENT '设备点位表';
-- 7. 设备数据归档表
DROP TABLE IF EXISTS IoT_DeviceData;
CREATE TABLE IoT_DeviceData (
-- ============================================
-- 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',
PointId INT NOT NULL 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_Data_Device (DeviceId),
INDEX IX_Data_Time (CreateDate)
) COMMENT '设备数据归档表(仅存历史快照)';
INDEX IX_Device (DeviceId),
INDEX IX_Time (CreateDate)
) COMMENT '设备数据归档表';
-- 8. 告警记录表
DROP TABLE IF EXISTS IoT_Alarm;
CREATE TABLE IoT_Alarm (
-- ============================================
-- 5. 告警记录表(通用)
-- DeviceId(INT) → base_device.DeviceId
-- ============================================
DROP TABLE IF EXISTS iot_alarm;
CREATE TABLE iot_alarm (
AlarmId INT AUTO_INCREMENT COMMENT '告警ID',
Mc4AlarmId NVARCHAR(64) NOT NULL COMMENT 'MC4.0告警ID',
DeviceId INT COMMENT '关联设备ID',
PointId INT COMMENT '关联点位ID',
SourceAlarmId NVARCHAR(100) NOT NULL COMMENT '源系统告警ID',
DeviceId INT NOT NULL COMMENT '关联设备ID',
AlarmType INT DEFAULT 0 COMMENT '告警类型',
AlarmLevel INT DEFAULT 1 COMMENT '告警等级(1提示/2普通/3重要/4紧急)',
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 INT DEFAULT 1 COMMENT '状态(1未确认/2已确认/3已结束)',
State NVARCHAR(20) DEFAULT '未确认' COMMENT '状态(数据字典:未确认/已确认/已结束)',
AdapterCode NVARCHAR(50) COMMENT '来源适配器',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (AlarmId),
UNIQUE INDEX IX_Alarm_Mc4 (Mc4AlarmId),
INDEX IX_Alarm_Device (DeviceId),
INDEX IX_Alarm_Time (StartTime)
INDEX IX_Device (DeviceId),
INDEX IX_Source (SourceAlarmId),
INDEX IX_Time (StartTime),
INDEX IX_Level (AlarmLevel)
) COMMENT '告警记录表';
-- ============================================
-- 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 '网关节点注册表';

View File

@@ -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? | | 父设备第三方IDVol.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 更新
```
---
> **文档结束**

View File

@@ -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-2A3 设备同步中 `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-4Owl 管理端口错误
**问题描述**
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-1PTZ 接口仅实现 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-5video_record 同步策略缺失
**问题描述**
§六同步策略有设备同步、告警同步、反向控制,但没有录像记录的同步时机和频率。
**影响**:录像数据永远不会写入 video_record 表
**解决方案**
补充 §六Owl 录像数据同步策略 ——
- **方式一**(推荐):管理端点击"查看回放"时,网关实时调 Owl `GET /recordings` → 返回给管理端 → 同时写入 video_record 表
- **方式二**(备选):网关定时(每 10 分钟)调 Owl `GET /recordings` → 走 A3 扩展同步 → 写入 video_record
建议先用方式一,方式二在 Phase 3 优化时引入。
---
### P1-6AdapterCode 双段格式无约束
**问题描述**
`"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 同步时如实上报每个子设备的 isOnlineMC4.0 树中已有此信息Vol.Pro 不做推断,信任网关数据。在 §六同步策略中补充此行说明即可。
---
### P2-2数据字典初始化运维指引补充附录
**问题描述**
方案依赖 7 个数据字典DeviceCategory 18 值、DeviceGroup 5 值、IsParent/IsOnline/Enable/IsControlPoint/AlarmLevel/State但没有初始化指引。
**解决方案**
在文档末尾增加「附录 A字典初始化清单」列出每个字典的 Code/Name/Value 对照表,运维人员在 Vol.Pro 管理端按表创建。
---
### P2-3video_channel 流地址字段用途说明(补充注释)
**问题描述**
video_channel 有 `OwlStreamApp/OwlStreamName/SnapshotUrl`B6 取流实时调网关获取。这些字段的实际用途没说。
**解决方案**
注明为缓存——首次取流后写入 video_channel下次先查缓存缓存过期或 Owl 重启后)再调 B6 实时获取。在 video_channel 表注释中补充说明。
---
### P2-4Owl AI 事件可接入告警(建议纳入 Phase 1 可选范围)
**问题描述**
Owl 有 YOLO 本地检测 + `GET /events` 接口AI 事件可走 A4 告警同步。
**解决方案**
在 OwlAdapter 中可选实现 IHasAlarms`GET /events` 的 AI 检测事件映射为 StandardAlarmAlarmLevel=提示或普通)。在 §六同步策略中补充可选说明,建议 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。

View File

@@ -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 支持改名+PTZMC4.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 间无需 JWTNodeToken | ✅ |
> 唯一需要额外处理的SqlSugar 的精确列更新。方案 §四 的示例代码已给出 `_db.Update(entity)`,实际使用时建议改为 `_db.Updateable(entity).UpdateColumns(...)` 精确指定更新列。
---
## 四、安全性评估
| 评估项 | 结论 |
|--------|------|
| 网关认证NodeToken | 正确。不受 JWT 过期影响 |
| B 组 API 无认证 | 可行。内网部署 + IP 白名单 |
| Owl JWT Token 缓存 | TokenManager 用 MemoryCache3 天有效期 |
| 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

View File

@@ -0,0 +1,210 @@
# SecMPS 整合项目实施手册
> **版本**: v3.0
> **日期**: 2026-05-16
> **基于**: SecMPS_最终整合方案_v3.0.md
> **工期**: 18-20 个工作日
> **开发模式**: 单人 + AgentSquash 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<AdapterRegistry>();
var http = app.Services.GetRequiredService<IHttpClientFactory>();
await RegisterWithVolPro(registry, http, app.Configuration);
```
5. 新增 `GatewayClient.cs`:
网关调用 Vol.Pro API 的封装类(注册/心跳/同步设备/同步告警)
验证: `dotnet build` 零错误 + Gateway `/health` 200
**任务 1.2**: 执行 db_init.sql6张表。验证: 唯一索引存在。
**任务 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 1Owl 适配器 + 视频设备页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 2MC4.0 + IoTDay 7-11
同上 v2.1 手册,增加 MC4.0 skip/limit 分页转换。
---
## Phase 3warehouse 联调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

View File

@@ -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` | 心跳(每 15sVol.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<StandardDevice> 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 系列所有整合方案文档

View File

@@ -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 管理端口为 15123ZLMediaKit 由 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<StandardDevice> 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设备/门禁设备/道闸设备/报警设备 |
| 是否父设备 | 是/否 |
| 在线状态 | 在线/离线 |
| 启用状态 | 启用/禁用 |
| 是否控制点 | 只读/可写 |
| 告警等级 | 提示/普通/重要/紧急 |
| 告警状态 | 未确认/已确认/已结束 |
---
## 附录 BAdapterCode 格式规范
```
格式: {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 修复)

View File

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

View File

@@ -1,21 +0,0 @@
namespace IntegrationGateway.Core.Abstractions;
public interface IAcceptsMetadataPush : IIntegrationAdapter
{
Task<MetadataPushResult> 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<string> RejectedFields { get; set; } = new();
public string? Reason { get; set; }
}

View File

@@ -1,12 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
public interface IHasAlarms : IIntegrationAdapter
{
Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to,
int? confirmState = null, int? endState = null, List<int>? levels = null);
Task ConfirmAlarmAsync(string alarmId);
Task EndAlarmAsync(string alarmId);
Task<int> GetPendingAlarmCountAsync();
}

View File

@@ -1,12 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
public interface IHasFlatDevices : IIntegrationAdapter
{
Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null);
Task<StandardDevice?> GetDeviceAsync(string sourceDeviceId);
Task<List<StandardDevice>> GetAllDevicesAsync();
Task<PagedResult<StandardDevice>> GetChannelsAsync(int page, int size, string? parentDeviceId = null);
Task<List<StandardDevice>> GetAllChannelsAsync();
}

View File

@@ -1,8 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
public interface IHasOwnDeviceTree : IIntegrationAdapter
{
Task<List<DeviceTreeNode>> GetObjectTreeAsync();
}

View File

@@ -1,10 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
public interface IHasPoints : IIntegrationAdapter
{
Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId);
Task<List<PointValue>> GetMultiPointValuesAsync(List<(string DeviceId, int PointIndex)> points);
Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value);
}

View File

@@ -1,14 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
public interface IHasStreams : IIntegrationAdapter
{
Task<StreamUrls> GetLiveUrlAsync(string channelId);
Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end);
Task StopPlayAsync(string channelId);
Task<StreamUrls> GetSnapshotAsync(string channelId);
Task PtzControlAsync(string channelId, string direction, float speed);
Task PtzStopAsync(string channelId);
Task<PagedResult<StandardRecording>> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size);
}

View File

@@ -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<bool> HealthCheckAsync();
Task InitializeAsync();
}

View File

@@ -1,27 +0,0 @@
using IntegrationGateway.Core.Abstractions;
namespace IntegrationGateway.Core.Infrastructure;
public class AdapterRegistry
{
private readonly Dictionary<string, IIntegrationAdapter> _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<IIntegrationAdapter> GetAll() => _adapters.Values;
public async Task InitializeAllAsync()
{
foreach (var adapter in _adapters.Values)
await adapter.InitializeAsync();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<string?> 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}");
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
</ItemGroup>
</Project>

View File

@@ -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; }
}

View File

@@ -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<string, object?> Option { get; set; } = new();
public List<DeviceTreeNode> Children { get; set; } = new();
public string? ParentPath { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace IntegrationGateway.Core.Models;
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int Total { get; set; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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<string, object?> Extra { get; set; } = new();
public DateTime LastSyncTime { get; set; }
}

View File

@@ -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<string, object?>? RawOption { get; set; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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<string> Errors { get; set; } = new();
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
}

View File

@@ -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<IActionResult> 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<IActionResult> 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" });
}
}

View File

@@ -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<IActionResult> 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<IActionResult> 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);
}
}

View File

@@ -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<IActionResult> Get()
{
var status = new Dictionary<string, bool>();
foreach (var adapter in _registry.GetAll())
status[adapter.AdapterCode] = await adapter.HealthCheckAsync();
return Ok(status);
}
}

View File

@@ -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<IActionResult> 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<IActionResult> 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; }
}

View File

@@ -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<IActionResult> 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<IActionResult> 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;
}

View File

@@ -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<IActionResult> 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<SyncReport> SyncFlatDevices(IHasFlatDevices adapter, SyncReport report)
{
var devices = await adapter.GetAllDevicesAsync();
report.Added = devices.Count;
return report;
}
private async Task<SyncReport> SyncTreeDevices(IHasOwnDeviceTree adapter, SyncReport report)
{
var tree = await adapter.GetObjectTreeAsync();
int count = CountDeviceNodes(tree);
report.Added = count;
return report;
}
private int CountDeviceNodes(List<DeviceTreeNode> nodes)
{
int count = 0;
foreach (var n in nodes)
{
if (n.NodeType == 2) count++;
count += CountDeviceNodes(n.Children);
}
return count;
}
}

View File

@@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.27" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\IntegrationGateway.Core\IntegrationGateway.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +0,0 @@
@IntegrationGateway.Host_HostAddress = http://localhost:5294
GET {{IntegrationGateway.Host_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -1,14 +0,0 @@
using IntegrationGateway.Core.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<AdapterRegistry>();
builder.Services.AddSingleton<TokenManager>();
var app = builder.Build();
app.MapControllers();
app.Run();

View File

@@ -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"
}
}
}
}

View File

@@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 903 KiB