Compare commits

5 Commits

7 changed files with 2762 additions and 145 deletions

View File

@@ -1,184 +1,168 @@
-- ============================================
-- SecMPS v2.0 数据库建表脚本
-- 数据库: gljs_main
-- 所有主键: INT AUTO_INCREMENT
-- ============================================
USE gljs_main;
-- 1. 统一设备主表
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)',
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在线)',
IpAddress NVARCHAR(50) COMMENT 'IP地址',
Port INT COMMENT '端口',
Location NVARCHAR(200) COMMENT '安装位置',
Lat DOUBLE COMMENT '纬度',
Lng DOUBLE COMMENT '经度',
MapModelId NVARCHAR(100) COMMENT '三维地图模型ID',
MapModelScale FLOAT DEFAULT 1.0 COMMENT '模型缩放比例',
MapModelRotation NVARCHAR(100) COMMENT '模型旋转角度(JSON)',
ExtraData TEXT COMMENT '源系统原始数据JSON',
LocalOverrides TEXT COMMENT '本地覆盖字段JSON',
SyncVersion BIGINT DEFAULT 0 COMMENT '同步版本号',
LastSyncTime DATETIME COMMENT '上次同步时间',
Enable TINYINT DEFAULT 1 COMMENT '启用(0禁用/1启用)',
Remark NVARCHAR(500) COMMENT '备注',
CreateID INT COMMENT '创建人ID',
Creator NVARCHAR(50) COMMENT '创建人',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
ModifyID INT COMMENT '修改人ID',
Modifier NVARCHAR(50) COMMENT '修改人',
ModifyDate DATETIME COMMENT '修改时间',
PRIMARY KEY (DeviceId),
CREATE TABLE IF NOT EXISTS Base_Device (
DeviceId CHAR(36) NOT NULL PRIMARY KEY,
DeviceName NVARCHAR(100) NOT NULL,
AdapterCode NVARCHAR(50) NOT NULL,
SourceId NVARCHAR(100) NOT NULL,
DeviceCategory INT NOT NULL DEFAULT 1,
DeviceType NVARCHAR(50),
RegionId INT NULL,
IsParent TINYINT NOT NULL DEFAULT 0,
ParentDeviceId CHAR(36) NULL,
IsOnline TINYINT NOT NULL DEFAULT 0,
IpAddress NVARCHAR(50),
Port INT,
Location NVARCHAR(200),
Lat DOUBLE,
Lng DOUBLE,
MapModelId NVARCHAR(100),
MapModelScale FLOAT DEFAULT 1.0,
MapModelRotation NVARCHAR(100),
ExtraData TEXT,
LocalOverrides TEXT,
SyncVersion BIGINT DEFAULT 0,
LastSyncTime DATETIME,
Enable TINYINT DEFAULT 1,
Remark NVARCHAR(500),
CreateID INT,
Creator NVARCHAR(50),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
ModifyID INT,
Modifier NVARCHAR(50),
ModifyDate DATETIME,
UNIQUE INDEX IX_Base_Device_Adapter_Source (AdapterCode, SourceId),
INDEX IX_Base_Device_RegionId (RegionId),
INDEX IX_Base_Device_ParentId (ParentDeviceId)
) 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),
CREATE TABLE IF NOT EXISTS Device_Video_Ext (
ExtId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
OwlDeviceId NVARCHAR(64) NOT NULL,
Protocol INT DEFAULT 1,
Manufacturer NVARCHAR(100),
Model NVARCHAR(100),
ChannelCount INT DEFAULT 0,
OwlStatus NVARCHAR(500),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_VideoExt_Owl (OwlDeviceId),
INDEX IX_VideoExt_Device (DeviceId)
) COMMENT '视频设备扩展表';
);
-- 3. 视频通道表
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 '通道编号',
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在线)',
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),
CREATE TABLE IF NOT EXISTS Video_Channel (
ChannelId CHAR(36) NOT NULL PRIMARY KEY,
OwlChannelId NVARCHAR(64) NOT NULL,
DeviceId CHAR(36) NOT NULL,
ChannelName NVARCHAR(100) NOT NULL,
ChannelNo INT DEFAULT 0,
OwlStreamApp NVARCHAR(50),
OwlStreamName NVARCHAR(100),
HasPtz TINYINT DEFAULT 0,
HasRecording TINYINT DEFAULT 0,
RecordMode INT DEFAULT 0,
IsOnline TINYINT DEFAULT 0,
SnapshotUrl NVARCHAR(500),
Location NVARCHAR(200),
Lat DOUBLE,
Lng DOUBLE,
Enable TINYINT DEFAULT 1,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_Channel_Owl (OwlChannelId),
INDEX IX_Channel_Device (DeviceId)
) COMMENT '视频通道表';
);
-- 4. 录像记录表
DROP TABLE IF EXISTS Video_Record;
CREATE TABLE Video_Record (
RecordId INT AUTO_INCREMENT COMMENT '录像记录ID',
ChannelId INT NOT NULL COMMENT '关联通道ID',
OwlRecordId INT NOT NULL COMMENT 'Owl录像记录ID',
App NVARCHAR(50) COMMENT '应用名',
Stream NVARCHAR(100) COMMENT '流ID',
StartedAt DATETIME NOT NULL COMMENT '录像开始时间',
EndedAt DATETIME COMMENT '录像结束时间',
Duration DOUBLE DEFAULT 0 COMMENT '录像时长(秒)',
FilePath NVARCHAR(500) COMMENT '文件路径',
FileSize BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (RecordId),
CREATE TABLE IF NOT EXISTS Video_Record (
RecordId CHAR(36) NOT NULL PRIMARY KEY,
ChannelId CHAR(36) NOT NULL,
OwlRecordId INT NOT NULL,
App NVARCHAR(50),
Stream NVARCHAR(100),
StartedAt DATETIME NOT NULL,
EndedAt DATETIME,
Duration DOUBLE DEFAULT 0,
FilePath NVARCHAR(500),
FileSize BIGINT DEFAULT 0,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX IX_Record_Channel (ChannelId),
INDEX IX_Record_Time (StartedAt)
) 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),
CREATE TABLE IF NOT EXISTS Device_IoT_Ext (
ExtId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
Mc4DeviceId INT NOT NULL,
ObjectType INT,
Tag NVARCHAR(100),
ParentId INT,
Mc4Option NVARCHAR(500),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_IoTExt_Mc4 (Mc4DeviceId),
INDEX IX_IoTExt_Device (DeviceId)
) 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),
CREATE TABLE IF NOT EXISTS IoT_DevicePoint (
PointId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
Mc4DeviceId INT NOT NULL,
PointIndex INT NOT NULL,
PointType INT,
PointTag NVARCHAR(100),
PointName NVARCHAR(100) NOT NULL,
PointDesc NVARCHAR(200),
Unit NVARCHAR(50),
IsControlPoint TINYINT DEFAULT 0,
Mc4Option NVARCHAR(500),
Enable TINYINT DEFAULT 1,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_Point_Mc4 (Mc4DeviceId, PointIndex),
INDEX IX_Point_Device (DeviceId)
) COMMENT '设备点位表';
);
-- 7. 设备数据归档表
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',
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),
-- 7. 设备数据归档表(仅存快照,实时不入库)
CREATE TABLE IF NOT EXISTS IoT_DeviceData (
DataId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
PointId CHAR(36) NOT NULL,
PointValue DOUBLE,
UpdateTime DATETIME NOT NULL,
`Interval` INT DEFAULT 0,
ArchiveType INT DEFAULT 1,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX IX_Data_Device (DeviceId),
INDEX IX_Data_Time (CreateDate)
) COMMENT '设备数据归档表(仅存历史快照)';
);
-- 8. 告警记录表
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',
AlarmType INT DEFAULT 0 COMMENT '告警类型',
AlarmLevel INT DEFAULT 1 COMMENT '告警等级(1提示/2普通/3重要/4紧急)',
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已结束)',
AdapterCode NVARCHAR(50) COMMENT '来源适配器',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (AlarmId),
CREATE TABLE IF NOT EXISTS IoT_Alarm (
AlarmId CHAR(36) NOT NULL PRIMARY KEY,
Mc4AlarmId NVARCHAR(64) NOT NULL,
DeviceId CHAR(36),
PointId CHAR(36),
AlarmType INT DEFAULT 0,
AlarmLevel INT DEFAULT 1,
AlarmDesc NVARCHAR(500),
AlarmValue DOUBLE,
StartTime DATETIME NOT NULL,
EndTime DATETIME,
ConfirmTime DATETIME,
ConfirmUser NVARCHAR(50),
State INT DEFAULT 1,
AdapterCode NVARCHAR(50),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_Alarm_Mc4 (Mc4AlarmId),
INDEX IX_Alarm_Device (DeviceId),
INDEX IX_Alarm_Time (StartTime)
) COMMENT '告警记录表';
);

View File

@@ -0,0 +1,424 @@
# SecMPS 整合方案 v3.0
> 版本: 3.0
> 日期: 2025-05-17
> 状态: 草稿
---
## 1. 背景与目标
SecMPS 需要将多套异构子系统视频监控、IoT动环、门禁道闸、报警统一整合到 Vol.Pro 管理端。v2.0 采用 Vol.Pro 直接对接各子系统 API存在紧耦合、协议适配散落、扩展困难等问题。
v3.0 引入 **IntegrationGateway整合网关** 作为统一中间层,面向子系统做协议适配,面向 Vol.Pro 提供标准化 REST API实现"适配一次,多处复用"。
### 1.1 核心原则
| 原则 | 说明 |
|------|------|
| 适配器隔离 | 每个子系统一个 Adapter实现统一接口互不干扰 |
| 懒加载 | 适配器按需初始化,不阻塞网关启动 |
| 分页语义统一 | 统一 page/size 分页,适配器内部转换 skip/limit |
| 字段分治 | 网关负责"来源标识+在线状态"Vol.Pro 负责"设备名称+扩展属性",首次入库全量,后续仅更新网关字段 |
### 1.2 子系统清单
| 子系统 | 对接方式 | 适配器 | 核心能力 |
|--------|----------|--------|----------|
| Owl + ZLMediaKit | REST API | OwlAdapter | 视频设备发现、实时流、PTZ、录像回放、截图 |
| MC4.0 | REST API | MC4Adapter | 对象树、实时点值、设备控制、告警查询/确认/结束 |
---
## 2. 架构总览
```
┌──────────────────────────────────────────────────────────┐
│ Vol.Pro 管理端 │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ gateway_nodes│ │ base_device │ │ device_manager │ │
│ │ Controller │ │ Controller │ │ (前端页面) │ │
│ │ A1-A4 │ │ GetRegion..│ │ RegionTree + │ │
│ │ │ │ GetDevices │ │ DeviceTable │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │ │
│ │ GatewayClient │ │ HTTP │
│ └────────┬────────┘ │ │
│ │ B3 (REST) │ │
└──────────────────┼─────────────────────────────┼──────────┘
│ │
┌──────────────────┼─────────────────────────────┼──────────┐
│ IntegrationGateway (net8.0) │ │
│ │ │ │
│ ┌───────────────┴──────────────────────────────┴───────┐ │
│ │ AdapterRegistry │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ OwlAdapter │ │ MC4Adapter │ ... │ │
│ │ │ IHasFlat.. │ │ IHasOwnTree │ │ │
│ │ │ IHasStreams │ │ IHasPoints │ │ │
│ │ │ IHasPtz │ │ IHasAlarms │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ │ │
│ └─────────┼────────────────┼───────────────────────────┘ │
│ │ │ │
│ ┌────────┴────────┐ ┌───┴──────────┐ │
│ │ Owl + ZLMediaKit│ │ MC4.0 │ │
│ │ (视频监控) │ │ (动环/IoT) │ │
│ └─────────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
```
### 2.1 数据流
```
MC4.0 对象树 → Mc4Adapter.GetObjectTree()
→ SyncEngine.ProcessMc4Tree()
→ type=1 节点 → warehouse_regions (区域匹配)
→ type=2 节点 → base_device (Upsert, 字段分治)
```
### 2.2 网关端口约定
| 服务 | 端口 | 说明 |
|------|------|------|
| IntegrationGateway | 5100 | 设备管理 REST API |
| Owl + ZLMediaKit | 15123 | 视频流/录像 |
| MC4.0 | 3000 | 动环数据/告警 |
| Vol.Pro | 9991 | 管理端后端 |
---
## 3. 核心接口体系
### 3.1 能力接口Capability Interfaces
```csharp
// 适配器按需实现,网关通过反射或接口判断自动发现能力
interface IHasFlatDevices { Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword); }
interface IHasOwnDeviceTree { Task<List<DeviceTreeNode>> GetObjectTreeAsync(); }
interface IHasStreams { Task<StreamUrls> GetLiveUrlAsync(string channelId); Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end); }
interface IHasPtz { Task PtzControlAsync(string channelId, string direction, float speed); }
interface IHasRecordings { Task<PagedResult<StandardRecording>> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size); }
interface IHasPoints { Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId); Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value); }
interface IHasAlarms { Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, ...); Task ConfirmAlarmAsync(string alarmId); Task EndAlarmAsync(string alarmId); }
interface IAcceptsMetadataPush { Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes); }
```
### 3.2 网关统一模型
```csharp
class StandardDevice {
int DeviceId; // Vol.Pro 侧主键(同步后回填)
string AdapterCode; // "Owl:main" / "MC4:31ku"
string SourceId; // 子系统原始 ID
string DeviceName;
string DeviceCategory; // 摄像机/温湿度变送器/...
string DeviceGroup; // 视频设备/IoT设备/门禁设备/...
bool IsParent;
string ParentSourceId;
bool IsOnline;
string IpAddress;
int? Port;
Dictionary<string,object?> Extra; // 子系统扩展属性
}
class StandardAlarm {
string AlarmId;
string DeviceId;
string AdapterCode;
string Level; // 提示/普通/重要/紧急
string Title;
string Content;
DateTime OccurTime;
string Status; // 未确认/已确认/已结束
double? ActualValue;
double? ThresholdValue;
}
class DeviceTreeNode {
string SourceId;
string Name;
int NodeType; // 1=区域, 2=设备
int ObjectType;
string Tag;
Dictionary<string,object?> Option;
List<DeviceTreeNode> Children;
}
```
### 3.3 订阅接口B3供 Vol.Pro 回调)
| 路由 | 方法 | 说明 |
|------|------|------|
| `/api/gateway/register` | POST | 网关注册 (Upsert) |
| `/api/gateway/heartbeat` | POST | 心跳 |
| `/api/gateway/sync/devices` | POST | 设备数据同步 |
| `/api/gateway/sync/alarms` | POST | 告警同步 |
### 3.4 网关暴露接口
| 路由 | 适配器能力 | 说明 |
|------|-----------|------|
| `GET /devices?adapter={code}&page=&size=` | IHasFlatDevices | 分页获取设备列表 |
| `GET /tree?adapter={code}` | IHasOwnDeviceTree | 获取对象树 |
| `GET /streams/{adapter}/{deviceId}/live` | IHasStreams | 获取实时流地址 |
| `GET /streams/{adapter}/{deviceId}/playback` | IHasStreams | 获取回放地址 |
| `POST /streams/{adapter}/{deviceId}/snapshot` | IHasStreams | 获取截图 |
| `POST /streams/{adapter}/{deviceId}/ptz` | IHasPtz | 云台控制 |
| `GET /recordings/{adapter}/{deviceId}` | IHasRecordings | 获取录像列表 |
| `GET /points/{adapter}/{deviceId}` | IHasPoints | 获取实时点值 |
| `POST /points/{adapter}/{deviceId}` | IHasPoints | 控制写值 |
| `GET /alarms/{adapter}` | IHasAlarms | 分页获取告警 |
| `POST /alarms/{adapter}/{id}/confirm` | IHasAlarms | 确认告警 |
| `POST /alarms/{adapter}/{id}/end` | IHasAlarms | 结束告警 |
| `GET /health` | - | 适配器健康状态 |
---
## 4. 数据库设计
### 4.1 新增表5 张Vol.Pro 侧)
```sql
-- 网关节点表
CREATE TABLE gateway_nodes (
NodeId INT IDENTITY PRIMARY KEY,
NodeCode NVARCHAR(50) NOT NULL,
NodeName NVARCHAR(100),
NodeToken NVARCHAR(200),
AdapterTypes NVARCHAR(200), -- "Owl:main,MC4:31ku"
BaseUrl NVARCHAR(500),
IsOnline NVARCHAR(10), -- 在线/离线
Enable NVARCHAR(10), -- 启用/禁用
LastHeartbeat DATETIME,
CreateDate DATETIME,
CreateID INT,
Creator NVARCHAR(50),
ModifyDate DATETIME,
ModifyID INT,
Modifier NVARCHAR(50)
);
-- 统一设备表
CREATE TABLE base_device (
DeviceId INT IDENTITY,
AdapterCode NVARCHAR(50) NOT NULL, -- 联合主键
DeviceName NVARCHAR(100),
SourceId NVARCHAR(100) NOT NULL, -- 联合主键 (AdapterCode + SourceId = 唯一)
DeviceCategory NVARCHAR(50), -- 摄像机/硬盘录像机/温湿度变送器/...
DeviceGroup NVARCHAR(50), -- 视频设备/IoT设备/门禁设备/...
PointId INT NULL, -- FK → warehouse_devicepoint
GatewayNodeId INT NULL, -- FK → gateway_nodes
IsParent NVARCHAR(10),
ParentDeviceId INT NULL,
IsOnline NVARCHAR(10),
Enable NVARCHAR(10),
Protocol NVARCHAR(50),
IpAddress NVARCHAR(50),
Port INT NULL,
LastSyncTime DATETIME,
MapModelId NVARCHAR(100),
MapModelScale NVARCHAR(50),
MapModelRotation NVARCHAR(200),
ExtraData NVARCHAR(MAX), -- JSON 扩展属性
CreateDate DATETIME,
CreateID INT,
Creator NVARCHAR(50),
ModifyDate DATETIME,
ModifyID INT,
Modifier NVARCHAR(50),
PRIMARY KEY (DeviceId, AdapterCode)
);
-- 数据归档表
CREATE TABLE iot_devicedata (
DataId INT IDENTITY PRIMARY KEY,
DeviceId INT NULL,
AdapterCode NVARCHAR(50),
PointIndex INT,
Value FLOAT,
UpdateTime DATETIME,
CreateDate DATETIME
);
-- 告警记录表
CREATE TABLE iot_alarm (
AlarmId INT IDENTITY PRIMARY KEY,
SourceAlarmId NVARCHAR(100),
DeviceId INT NULL,
AdapterCode NVARCHAR(50),
AlarmLevel NVARCHAR(20),
AlarmDesc NVARCHAR(500),
AlarmValue FLOAT,
ThresholdValue FLOAT,
StartTime DATETIME,
EndTime DATETIME,
ConfirmTime DATETIME,
State NVARCHAR(20), -- 未确认/已确认/已结束
ConfirmUser NVARCHAR(50),
CreateDate DATETIME
);
-- 视频通道表base_device 子表)
CREATE TABLE video_channel (
ChannelId INT NOT NULL,
DeviceId INT NOT NULL, -- FK → base_device
ChannelNo NVARCHAR(50),
SourceId NVARCHAR(100),
ChannelName NVARCHAR(100),
IsOnline NVARCHAR(10),
CreateDate DATETIME,
PRIMARY KEY (ChannelId, DeviceId)
);
-- 录像文件表
CREATE TABLE video_record (
RecordId INT IDENTITY PRIMARY KEY,
ChannelId INT NULL,
FilePath NVARCHAR(500),
Size BIGINT,
StartTime DATETIME,
EndTime DATETIME,
Duration FLOAT,
CreateDate DATETIME
);
```
### 4.2 字典初始化
```sql
-- 设备分类字典
INSERT INTO Sys_Dictionary (DicName, DicValue, DicNo, Config) VALUES
('设备分组', '视频设备', 'device_group_1', '视频设备'),
('设备分组', 'IoT设备', 'device_group_2', 'IoT设备'),
('设备分组', '门禁设备', 'device_group_3', '门禁设备'),
('设备分组', '道闸设备', 'device_group_4', '道闸设备'),
('设备分组', '报警设备', 'device_group_5', '报警设备');
```
---
## 5. 管理端前端
### 5.1 页面结构
```
/web.vite/src/views/warehouse/DeviceManager/
├── index.vue 左右分栏主页面
├── api/
│ └── deviceManager.js API 封装
└── components/
├── RegionTree.vue 区域→点位树 (左侧)
├── DeviceTable.vue 设备列表 (右侧)
├── VideoDeviceActions.vue 视频设备操作按钮组
├── DeviceLivePreview.vue 实时预览弹窗
├── PtzControlPanel.vue 云台方向键面板
├── MapBindingPanel.vue 地图绑定面板
└── DeviceEditDialog.vue 设备编辑弹窗
```
### 5.2 路由
```
/device-manager → DeviceManager/index.vue
```
### 5.3 功能矩阵
| 功能 | 触发条件 | 组件 | 说明 |
|------|---------|------|------|
| 区域树展开 | 页面加载 | RegionTree | 调 GetRegionTree API |
| 设备列表 | 点击点位 | DeviceTable | 调 GetDevicesByPoint API |
| 实时预览 | 视频设备→预览 | DeviceLivePreview | WS-FLV 播放 |
| 云台控制 | 视频设备→云台 | PtzControlPanel | ↑↓←→ + ZOOM |
| 查看回放 | 视频设备→回放 | (待实现) | 录像时间轴 |
| 获取快照 | 视频设备→快照 | (待实现) | JPEG 快照 |
| 地图绑定 | 任意设备→地图 | MapBindingPanel | 模型 ID/缩放/旋转 |
| 编辑设备 | 非视频设备→编辑 | DeviceEditDialog | 名称/种类/分组/启用 |
---
## 6. 实施计划
### Phase 0: 基础设施Day 1-2
| Day | 内容 | 产出 |
|-----|------|------|
| 1 | 网关项目骨架 (net8.0) | IntegrationGateway.sln, Core/Host 项目, 7 个能力接口, 10 个统一模型, 3 个基础设施 (Registry/RateLimiter/HttpClientFactory) |
| 2 | Vol.Pro 侧集成 | GatewayClient, gateway_nodesController(A1-A4骨架), base_deviceController(骨架), Quartz Job, db_init.sql, 代码生成器产物 |
### Phase 1: Owl 适配器 + 管理端Day 3-5
| Day | 内容 | 产出 |
|-----|------|------|
| 3 | OwlAdapter | RSA 加密登录, 3 个接口实现 (IHasFlatDevices + IHasStreams + IAcceptsMetadataPush) |
| 4 | DeviceManager 页面框架 | RegionTree + DeviceTable + router |
| 5 | 视频组件 | 预览/云台/地图绑定/编辑弹窗 |
### Phase 2: MC4 适配器 + 联调Day 6-11
| Day | 内容 | 产出 |
|-----|------|------|
| 6 | 联调验证 (Owl + MC4) | 需子系统就绪 |
| 7 | MC4Adapter | Token 认证, IHasOwnDeviceTree + IHasPoints + IHasAlarms |
| 8 | 区域自动匹配 | SyncEngine, 字段分治, parentSourceId 映射 |
| 9 | 物联网操作接口 | 实时值读取, 控制写值 API |
| 10 | 告警集成 + SignalR | 告警查询/确认/结束, SignalR 实时推送 |
| 11 | 地图绑定 + Quartz | MapBindingPanel, SyncDevicesJob, HeartbeatMonitorJob |
### Phase 3: Warehouse 端Day 12-17
| Day | 内容 | 产出 |
|-----|------|------|
| 12 | 钥匙柜管理 | 钥匙柜设备 + 钥匙借还流程 |
| 13 | 巡更管理 | 巡更路径/点位/排班 |
| 14 | 门禁管理 | 门禁一体机 + 授权 |
| 15 | 道闸管理 | 人行道闸 + 车辆道闸 |
| 16 | 报警管理 | 紧急报警 + 离线报警 |
| 17 | 仓库页面整合 | 菜单 + 权限 |
### Phase 4: 验证发布Day 18-20
| Day | 内容 | 产出 |
|-----|------|------|
| 18 | 系统测试 | 功能测试 + 边界测试 |
| 19 | 性能优化 | 流性能 + 并发 + 缓存 |
| 20 | 文档 + 发布 | 部署手册, Dockerfile |
---
## 7. 风险与约束
| 风险 | 缓解措施 |
|------|----------|
| Vol.Pro 框架 API 与自主代码不兼容 | 网关层独立编译dotnet buildVol.Pro 后端逻辑骨架化,联调时一边调一边补 |
| Owl 接口不稳定 | 统一 2 QPS 限流 + 重试 |
| MC4 字段映射复杂 | 对象树 Option 弹性 JSON 字段,扩容不破坏现有映射 |
| 设备数量大导致同步慢 | 增量同步 + 批量 Upsert |
---
## 8. 环境配置
### 8.1 网关 appsettings.json
```json
{
"Owl": {
"BaseUrl": "http://owl_host:15123",
"Username": "admin",
"Password": "your_owl_password"
},
"MC4": {
"BaseUrl": "http://mc4_host:3000"
}
}
```
### 8.2 启动顺序
1. Owl + ZLMediaKit → 端口 15123
2. MC4.0 → 端口 3000
3. IntegrationGateway → 端口 5100 (`dotnet run --project src/IntegrationGateway.Host`)
4. Vol.Pro 后端 → 端口 9991
5. web.vite 前端 → 端口 9000 (`npm run dev`)

View File

@@ -0,0 +1,531 @@
# SecMPS 整合方案最终版IntegrationGateway + 统一设备管理
> **版本**: v3.0
> **日期**: 2026-05-16
> **状态**: 待实施
> **基准**: 基于 v2.0 + Owl/ZLMediaKit 双文档验证 + 14 项修正
---
## 一、总体架构
```
前端层
web.vite 管理端(设备管理+网关管理) warehouse 大屏Map/Live/IoT/Alarm
| HTTP REST | HTTP REST + SignalR
v v
Vol.Pro 后端 (api_sqlsugar)
DeviceManagerController / GatewayNodeController / SignalR Hubs
Quartz: SyncDevicesJob / RealtimePollJob / AlarmPollJob
数据库: 6 张表 (base_device / video_channel / video_record / iot_devicedata / iot_alarm / gateway_nodes)
| 注册/下发设备/心跳/同步数据
v
IntegrationGateway 实例A (:5100) IntegrationGateway 实例B (:5101)
NodeCode: gw-31ku NodeCode: gw-11ku
Adapters: MC4 / Owl Adapters: MC4 / HikvisionISC
| HTTP | HTTP
v v
MC4.0 (:3000) Owl (:15123) MC4.0 (:3000) 海康ISC (:80)
注: Owl 管理端口为 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();
}
```
---
## 五、管理端设备操作集成
> **设计决策**: 不再需要独立的设备管理页面。Vol.Pro 框架自带三级主从表显示能力warehouse_regions → warehouse_devicepoint → base_device可直接在框架生成的 base_device 列表页面中嵌入操作按钮,按 DeviceGroup 动态渲染。
### 5.1 操作按钮矩阵
| DeviceGroup | 操作按钮 | 说明 |
|:---:|------|------|
| 视频设备 | 实时预览 / 云台控制(仅方向键) / 查看回放 / 获取快照 / 同步通道 | 全部通过网关 B 组接口代理到 Owl |
| IoT设备 | 查看实时数据 / 设备控制 / 刷新点位 / 查看告警 | 通过网关 B4/B5 代理到 MC4.0 |
| 门禁设备 | 远程开门 / 查看记录 / 查看告警 | Phase 3 接入海康ISC后启用 |
| 道闸设备 | 抬杆 / 落杆 / 查看记录 | Phase 3 接入 |
| 报警设备 | 查看告警 / 布防撤防 | Phase 3 接入 |
### 5.2 前端按钮嵌入方案
框架生成的 base_device 列表页面的"操作"列默认只有"编辑/删除"。在 **base_deviceController Partial** 中扩展前端分组逻辑,替换默认操作列为自定义渲染。
```javascript
// web.vite/src/extension/warehouse/base_device.jsx 或 views/warehouse/base_device/components/ActionColumn.vue
const actionMap = {
'视频设备': VideoDeviceActions, // 实时预览/云台/回放/快照/同步通道
'IoT设备': IoTDeviceActions, // 实时数据/控制/刷新/告警
'门禁设备': AccessDeviceActions, // 远程开门/记录/告警
'道闸设备': BarrierDeviceActions,// 抬杆/落杆/记录
'报警设备': AlarmDeviceActions, // 告警/布防撤防
}
// 操作列模板中根据 row.deviceGroup 动态渲染对应组件
<template v-for="(row, idx) in data">
<component :is="actionMap[row.deviceGroup] || DefaultActions" :row="row" />
</template>
```
**要点**:
- 框架"操作"列通过自定义插槽替换,不修改框架生成的 Vue 文件本体
- 组件放在 `views/warehouse/base_device/components/` 下,防代码生成覆盖
- 五个分组组件各自独立(~80-150 行),新增分组仅加一个文件
### 5.3 前端操作组件清单
| 文件 | 大小 | 说明 |
|------|:---:|------|
| `VideoDeviceActions.vue` | ~120行 | 预览/云台/回放/快照按钮组,预览弹窗内嵌 Jessibuca 播放器(或 <video> 标签回退),云台仅 ↑↓←→+ZOOM+停止 |
| `IoTDeviceActions.vue` | ~100行 | 实时数据表格弹窗(轮询 B4控制面板B5 写值),告警快捷入口 |
| `AccessDeviceActions.vue` | ~60行 | 远程开门按钮 + 记录查询入口 |
| `BarrierDeviceActions.vue` | ~60行 | 抬杆/落杆按钮 |
| `AlarmDeviceActions.vue` | ~60行 | 告警列表 + 布防/撤防开关 |
### 5.4 后端操作 API
> 以下 API 由网关 B 组接口代理Vol.Pro 前端不直连子系统,统一经网关中转。
| 接口 | 方法 | 说明 | 对应网关接口 |
|------|:---:|------|------|
| `/api/gateway/streams/{adapter}/{sourceId}/live` | GET | 获取实时流地址 | B6a |
| `/api/gateway/streams/{adapter}/{sourceId}/playback?start=&end=` | GET | 获取回放地址 | B6b |
| `/api/gateway/streams/{adapter}/{sourceId}/snapshot` | POST | 获取截图 | — |
| `/api/gateway/streams/{adapter}/{sourceId}/ptz` | POST | 云台方向控制 | B7 |
| `/api/gateway/realtime/{adapter}/{sourceId}` | GET | 获取实时点位值 | B4 |
| `/api/gateway/realtime/{adapter}/control` | POST | 设备控制写值 | B5 |
| `/api/gateway/alarms/{adapter}` | GET | 分页查询告警 | B8 |
| `/api/gateway/alarms/{adapter}/{alarmId}/confirm` | POST | 确认告警 | B9 |
> Vol.Pro 前端直接请求网关(:5100不经过 Vol.Pro 后端中转。跨域问题通过网关 CORS 或 nginx 反向代理解决。
---
## 六、同步策略
### 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 + base_device操作按钮组件(视频) + [可选]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 修复)
> - v3.1 (2026-05-17) — 第五章修订:取消独立设备管理页面,改为框架主从表嵌入操作按钮

View File

@@ -0,0 +1,512 @@
# Vol.Pro 框架前后端改造方案
> **版本**: 1.0
> **日期**: 2025-05-17
> **基准**: SecMPS 整合方案 v3.1 + Vol.Pro 框架官方文档
> **核心原则**: 所有改动必须在 Partial/extension 目录中,严禁修改框架生成代码
---
## 1. 改造总览
### 1.1 改造清单
| 层面 | 改造项 | 位置 | 是否破坏可升级性 |
|------|--------|------|:---:|
| 数据库 | 5 张新表 + 字典数据 | SQL 脚本 | ❌ |
| 后端-Entity | 6 个实体 Partial 类 | `DomainModels/*/partial/` | ❌ |
| 后端-Service | 6 个 Service Partial 类 | `Services/*/Partial/` | ❌ |
| 后端-Controller | 3 个 Controller Partial 类 | `Controllers/*/Partial/` | ❌ |
| 后端-Job | 3 个 Quartz 定时任务 | `Warehouse/Services/` | ❌ |
| 后端-Config | 注册网关 HttpClient | `Startup.cs` | ⚠️ 需手动合并 |
| 前端-主表 | 自定义操作列插槽 | `extension/warehouse/` | ❌ |
| 前端-组件 | 5 个设备操作组件 | `views/warehouse/base_device/components/` | ❌ |
| 网关 | GatewayClient HTTP 客户端 | `Warehouse/Services/` | ❌ |
### 1.2 框架扩展点总览
```
Vol.Pro 框架约定:
自动生成代码 (代码生成器覆盖) 自定义代码 (不被覆盖)
───────────────────────────── ──────────────────────────
Controllers/xxxController.cs Controllers/xxx/Partial/xxxController.cs
Services/xxxService.cs Services/xxx/Partial/xxxService.cs
IServices/IxxxService.cs IServices/xxx/Partial/IxxxService.cs
Repositories/xxxRepository.cs (无需自定义)
IRepositories/IxxxRepository.cs (无需自定义)
DomainModels/xxx/xxx.cs DomainModels/xxx/partial/xxx.cs
前端 views/xxx/xxx.vue extension/xxx.jsx + views/xxx/components/
```
---
## 2. 数据库改造
### 2.1 新增表
执行 `doc/db_init.sql`,创建 5 张表:
| 表名 | 说明 | 层级 |
|------|------|------|
| `gateway_nodes` | 网关节点 | 顶层 |
| `base_device` | 统一设备主表 | 核心AdapterCode+SourceId 联合唯一 |
| `video_channel` | 视频通道扩展 | base_device 子表 |
| `video_record` | 录像文件 | video_channel 子表 |
| `iot_alarm` | 告警记录 | base_device 子表 |
| `iot_devicedata` | 数据归档 | base_device 子表 |
### 2.2 字典初始化
Phase 0 需在 Vol.Pro 管理端 → 字典管理 中创建 8 组数据字典:
| 字典名称 | 字典编号 | 字典值 |
|----------|:---:|------|
| 设备种类 | — | 摄像机/硬盘录像机/温湿度变送器/空调控制器/... (18 项) |
| 设备分组 | device_group | 视频设备/IoT设备/门禁设备/道闸设备/报警设备 |
| 是否父设备 | — | 是/否 |
| 在线状态 | — | 在线/离线 |
| 启用状态 | — | 启用/禁用 |
| 是否控制点 | — | 只读/可写 |
| 告警等级 | — | 提示/普通/重要/紧急 |
| 告警状态 | — | 未确认/已确认/已结束 |
### 2.3 区-点位-设备 三级关联
框架支持主从表三级显示。利用现有表关系:
```
warehouse_regions (区域) ← 代码生成器已有
└── warehouse_devicepoint (点位) ← 代码生成器已有
└── base_device (设备) ← 新建,用 PointId 关联点位
├── video_channel ← 新建子表
├── iot_devicedata ← 新建子表
└── iot_alarm ← 新建子表
```
**关键**:代码生成器配置 base_device 的 `DetailTable` 属性,关联 video_channel/iot_devicedata/iot_alarm 作为子表,框架自动渲染主从表 Tab。
---
## 3. 后端改造
### 3.1 Entity 扩展Partial 类)
**原则**:框架生成的 Entity 在 `DomainModels/device_manager/base_device.cs`,不可修改。仅在 `partial/` 中添加。
```
VolPro.Entity/DomainModels/device_manager/partial/
├── base_device.cs # 添加导航属性 + AdapterCode/SourceId 唯一约束注解
├── gateway_nodes.cs # 网关特有业务属性
├── video_channel.cs # 通道流信息缓存
├── video_record.cs # (留空,框架生成即够用)
├── iot_devicedata.cs # (留空)
└── iot_alarm.cs # 告警确认/结束方法
```
**base_device Partial 示例**
```csharp
namespace VolPro.Entity.DomainModels
{
public partial class base_device
{
// 导航属性(用于主从表)
[Navigate(NavigateType.OneToOne, nameof(DeviceId), nameof(video_channel.DeviceId))]
public video_channel? VideoChannel { get; set; }
[Navigate(NavigateType.OneToMany, nameof(DeviceId), nameof(iot_alarm.DeviceId))]
public List<iot_alarm>? Alarms { get; set; }
[Navigate(NavigateType.OneToMany, nameof(DeviceId), nameof(iot_devicedata.DeviceId))]
public List<iot_devicedata>? DeviceData { get; set; }
// 网关字段标识(供同步时判断哪些字段可覆盖)
public static readonly HashSet<string> GatewayFields = new()
{
nameof(IsOnline), nameof(IsParent), nameof(ParentDeviceId),
nameof(ExtraData), nameof(IpAddress), nameof(Port), nameof(LastSyncTime)
};
}
}
```
### 3.2 Service 扩展Partial 类)
```
Warehouse/Services/device_manager/Partial/
├── base_deviceService.cs # GetRegionTree / GetDevicesByPoint / 字典查询辅助
├── gateway_nodesService.cs # 网关注册/心跳/同步入口
├── video_channelService.cs # (留空)
├── video_recordService.cs # (留空)
├── iot_devicedataService.cs # (留空)
└── iot_alarmService.cs # (留空)
```
**gateway_nodesService Partial**(核心同步入口):
```csharp
namespace Warehouse.Services
{
public partial class gateway_nodesService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Igateway_nodesRepository _repository;
[ActivatorUtilitiesConstructor]
public gateway_nodesService(
Igateway_nodesRepository dbRepository,
IHttpContextAccessor httpContextAccessor
) : base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
}
// 网关注册 (Upsert) — 被 Controller A1 调用
public async Task<gateway_nodes> RegisterNodeAsync(string nodeCode, string token, string adapterTypes, string baseUrl)
{
// 实现: Upsert 逻辑
}
// 心跳更新 — 被 Controller A2 调用
public async Task UpdateHeartbeatAsync(string nodeCode, string token)
{
// 实现: 更新 LastHeartbeat + IsOnline
}
// 设备同步 — 被 Controller A3 调用
public async Task<(int added, int updated)> SyncDevicesAsync(int gatewayNodeId, List<StandardDevice> devices)
{
// 实现: 字段分治 + parentSourceId 映射
}
}
}
```
### 3.3 Controller 扩展Partial 类)
```
VolPro.WebApi/Controllers/Warehouse/Partial/
├── base_deviceController.cs # GetRegionTree / GetDevicesByPoint / 操作代理
├── gateway_nodesController.cs # A1-A4 网关 API
├── video_channelController.cs # (留空)
├── video_recordController.cs # (留空)
├── iot_devicedataController.cs # (留空)
└── iot_alarmController.cs # (留空)
```
#### 3.3.1 gateway_nodesControllerA 组 API
**核心改造**:在框架生成的 `gateway_nodesController` 基础上Partial 中添加 4 个不受权限控制的网关回调接口。
```csharp
namespace Warehouse.Controllers
{
public partial class gateway_nodesController
{
private readonly IHttpContextAccessor _httpContextAccessor;
[ActivatorUtilitiesConstructor]
public gateway_nodesController(
Igateway_nodesService service,
IHttpContextAccessor httpContextAccessor
) : base(service)
{
_httpContextAccessor = httpContextAccessor;
}
/// <summary>A1: 网关注册 (Upsert)</summary>
[HttpPost, Route("/api/gateway/register"), AllowAnonymous]
public async Task<IActionResult> RegisterGateway([FromBody] GatewayRegisterRequest req)
{
// 实现
}
/// <summary>A2: 心跳</summary>
[HttpPost, Route("/api/gateway/heartbeat"), AllowAnonymous]
public async Task<IActionResult> GatewayHeartbeat([FromBody] GatewayHeartbeatRequest req)
{
// 实现
}
/// <summary>A3: 设备数据同步 (字段分治)</summary>
[HttpPost, Route("/api/gateway/sync/devices"), AllowAnonymous]
public async Task<IActionResult> SyncDevices([FromBody] SyncDevicesRequest req)
{
// 实现
}
/// <summary>A4: 告警同步</summary>
[HttpPost, Route("/api/gateway/sync/alarms"), AllowAnonymous]
public async Task<IActionResult> SyncAlarms([FromBody] SyncAlarmsRequest req)
{
// 实现
}
}
}
```
**权限模型**A 组接口加 `[AllowAnonymous]`,内部通过 `NodeToken` 二次认证。B 组接口走框架 JWT 权限。
#### 3.3.2 base_deviceControllerB 组代理 + 设备树)
```csharp
namespace Warehouse.Controllers
{
public partial class base_deviceController
{
/// <summary>区域→点位→设备树(管理端左侧树形控件)</summary>
[HttpGet, Route("/api/DeviceManager/GetRegionTree")]
public async Task<IActionResult> GetRegionTree()
{
// SELECT warehouse_regions JOIN warehouse_devicepoint → 树形结构
// 每个点位下统计 base_device 数量
}
/// <summary>点位下设备列表(含子设备)</summary>
[HttpGet, Route("/api/DeviceManager/GetDevicesByPoint")]
public async Task<IActionResult> GetDevicesByPoint(int pointId, int page = 1, int size = 20)
{
// SELECT base_device WHERE PointId = pointId
// Include 子设备ParentDeviceId
}
}
}
```
### 3.4 网关 HTTP 客户端GatewayClient
```
Warehouse/Services/GatewayClient.cs
```
```csharp
public class GatewayClient
{
private readonly IHttpClientFactory _httpFactory;
private readonly IConfiguration _config;
public GatewayClient(IHttpClientFactory httpFactory, IConfiguration config)
{
_httpFactory = httpFactory;
_config = config;
}
/// <summary>调网关 B3: 手动触发全量同步</summary>
public async Task TriggerFullSyncAsync(string baseUrl, string adapterTypes)
{
var http = _httpFactory.CreateClient();
// POST {baseUrl}/api/gateway/devices/sync?adapter={adapterTypes}
}
/// <summary>调网关 B4: 获取实时点位值</summary>
public async Task<List<PointValue>> GetRealtimeAsync(string baseUrl, string adapter, string deviceId)
{
// GET {baseUrl}/api/gateway/realtime/{adapter}/{deviceId}
}
/// <summary>调网关 B5: 设备控制</summary>
public async Task ControlDeviceAsync(string baseUrl, string adapter, string deviceId, int pointIndex, double value)
{
// POST {baseUrl}/api/gateway/realtime/{adapter}/control
}
}
```
### 3.5 Quartz 定时任务
| Job | 触发器 | 职责 |
|-----|:---:|------|
| `SyncDevicesJob` | 每 5 分钟 | 遍历所有在线网关 → 调 GatewayClient.TriggerFullSyncAsync() |
| `RealtimePollJob` | 每 10 秒 | 轮询 MC4.0 IoT 设备实时值 → 更新 iot_devicedata |
| `HeartbeatMonitorJob` | 每 15 秒 | 扫描 gateway_nodes 心跳超时 → 标记离线 + 级联设备离线 |
**注册方式**:在 Vol.Pro 管理端 → Quartz 任务管理 → 新建任务,指定 Job 类全名。
### 3.6 Startup.cs 注册
```csharp
// 在 Startup.cs 或 Program.cs 中注册
builder.Services.AddHttpClient("VolPro", c =>
{
c.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddSingleton<GatewayClient>();
```
---
## 4. 前端改造
### 4.1 架构决策
| 方案 | 描述 | 是否破坏可升级性 |
|------|------|:---:|
| ❌ 修改生成的 .vue | 直接改 views/warehouse/base_device/ 下框架生成文件 | ✅ 破坏 |
| ✅ extension + components | 通过扩展注入 + 自定义组件实现操作列 | ❌ 不破坏 |
### 4.2 自定义操作列插槽
框架生成的 base_device 列表页面默认操作列只有"编辑/删除"。通过 **前端扩展文件** 自定义替换:
```
web.vite/src/extension/warehouse/base_device.jsx
```
```javascript
// 自定义操作列渲染
import VideoDeviceActions from '@/views/warehouse/base_device/components/VideoDeviceActions.vue'
import IoTDeviceActions from '@/views/warehouse/base_device/components/IoTDeviceActions.vue'
// 注册自定义组件
export default {
components: { VideoDeviceActions, IoTDeviceActions },
// 替换框架默认操作列
slots: {
// 操作列插槽
'col-action': (h, { row }) => {
const comp = row.deviceGroup === '视频设备' ? 'VideoDeviceActions'
: row.deviceGroup === 'IoT设备' ? 'IoTDeviceActions'
: null
if (comp) return h(comp, { props: { row } })
// fallback 到框架默认按钮
return null
}
}
}
```
### 4.3 组件目录
```
web.vite/src/views/warehouse/base_device/components/
├── VideoDeviceActions.vue # 实时预览/云台/回放/快照/同步通道
├── IoTDeviceActions.vue # 实时数据/控制/刷新/告警
├── AccessDeviceActions.vue # 远程开门 (Phase 3)
├── BarrierDeviceActions.vue # 抬杆/落杆 (Phase 3)
├── AlarmDeviceActions.vue # 告警/布防撤防 (Phase 3)
├── DeviceLivePreview.vue # Jessibuca 播放器弹窗
├── PtzControlPanel.vue # ↑↓←→+ZOOM+停止
├── RealtimeDataPanel.vue # 实时点位值表格弹窗
├── DeviceControlPanel.vue # 控制写值面板
├── DeviceEditDialog.vue # 设备编辑弹窗 (扩展字段)
└── MapBindingPanel.vue # VgoMap 模型绑定
```
### 4.4 组件设计要点
**VideoDeviceActions.vue**(核心组件):
```
Props: row (base_device)
按钮组:
[实时预览] → 打开 DeviceLivePreview.vue 弹窗
→ GET /api/gateway/streams/{adapterCode}/{sourceId}/live → 流地址
→ 内嵌 Jessibuca 播放器fallback: <video> 标签 + flv.js
[云台控制] → 打开 PtzControlPanel.vue 弹窗
→ POST /api/gateway/streams/{adapterCode}/{sourceId}/ptz
→ 仅方向键: ↑↓←→ + ZOOM +/- + 停止
→ mousedown 开始移动, mouseup 发送停止
[查看回放] → 打开录像时间轴弹窗
→ GET /api/gateway/streams/{adapterCode}/{sourceId}/playback?start=&end=
[获取快照] → 下载快照图片
[同步通道] → 触发网关 B3 重新同步此 NVR 的通道
限制:
- channel 设备显示前 4 个按钮(无"同步通道"
- NVR(IsParent=是) 显示全部 5 个按钮
```
**IoTDeviceActions.vue**
```
[查看实时数据] → GET /api/gateway/realtime/{adapterCode}/{sourceId}
→ 仪表盘或数值显示(温度/湿度/电压...
→ 5s 自动轮询
[设备控制] → 仅 isControlPoint=true 的设备显示
→ 空调温度设定/开关/模式切换
[刷新点位] → 强制刷新当前点位所有设备数据
[查看告警] → GET /api/gateway/alarms/{adapterCode}
→ 跳转或内嵌告警列表
```
### 4.5 API 封装
```
web.vite/src/api/deviceManager.js # (已存在于框架中)
web.vite/src/api/gateway.js # 新增:网关 B 组接口封装
```
```javascript
// gateway.js
import request from '@/uitils/request'
// 基础: 直连网关
const gwBase = 'http://localhost:5100/api/gateway'
export const getStreamUrl = (adapter, deviceId) =>
request({ url: `${gwBase}/streams/${adapter}/${deviceId}/live`, method: 'get' })
export const ptzControl = (adapter, deviceId, direction, speed = 0.5) =>
request({ url: `${gwBase}/streams/${adapter}/${deviceId}/ptz`, method: 'post', data: { direction, speed } })
export const getRealtime = (adapter, deviceId) =>
request({ url: `${gwBase}/realtime/${adapter}/${deviceId}`, method: 'get' })
export const controlDevice = (adapter, deviceId, pointIndex, value) =>
request({ url: `${gwBase}/realtime/${adapter}/control`, method: 'post', data: { deviceId, pointIndex, value } })
export const getAlarms = (adapter, params) =>
request({ url: `${gwBase}/alarms/${adapter}`, method: 'get', params })
export const confirmAlarm = (adapter, alarmId) =>
request({ url: `${gwBase}/alarms/${adapter}/${alarmId}/confirm`, method: 'post' })
```
> 前端直连网关(:5100跨域问题通过 Vol.Pro nginx 反向代理 `/api/gateway/*` → `http://localhost:5100` 解决,或网关配置 CORS。
---
## 5. 可升级性保障清单
| 检查项 | 状态 |
|--------|:---:|
| 不修改框架生成的 C# 基础类 | ✅ |
| 所有自定义代码在 Partial 目录 | ✅ |
| 不修改框架生成的 .vue 文件 | ✅ |
| 前端业务逻辑在 extension 目录 | ✅ |
| 新增组件在 views/xxx/components/ 下 | ✅ |
| 不修改框架 NuGet 包版本 | ✅ |
| 网关调用通过独立 GatewayClient 类 | ✅ |
| A 组 API 用 `[AllowAnonymous]` + Token 二次认证 | ✅ |
| Dictionary/Entity 属性通过代码生成器配置,不手写 | ✅ |
| Startup.cs 注册仅用 `AddHttpClient`/`AddSingleton` | ✅ |
---
## 6. 实施顺序
| 步骤 | 内容 | 依赖 |
|:---:|------|------|
| 1 | 代码生成器生成 device_manager 模块 6 张表 | Phase 0 |
| 2 | 创建 Entity Partial 类 | 步骤 1 |
| 3 | 创建 Service Partial 类 | 步骤 1 |
| 4 | 创建 Controller Partial 类 (A1-A4 + GetRegionTree) | 步骤 3 |
| 5 | 注册 GatewayClient + HttpClient | 步骤 4 |
| 6 | 创建 Quartz Job (3 个) | 步骤 5 |
| 7 | 创建前端组件 (5 个按钮组 + 5 个弹窗) | 步骤 4 |
| 8 | 编写 extension/warehouse/base_device.jsx | 步骤 7 |
| 9 | nginx 配置网关反向代理 | 步骤 8 |
---
> **版本历史**:
> - v1.0 (2025-05-17) — 初版,基于整合方案 v3.1 + Vol.Pro 框架规范

View File

@@ -0,0 +1,260 @@
# Vol.Pro 框架前后端改造 — 任务清单
> **基准文档**: VolPro框架改造方案 v1.0
> **分支**: phase/0-infrastructure
> **原则**: 所有改动在 Partial/extension 目录,严禁修改框架生成文件
---
## Phase V0: 数据库与代码生成(预计 0.5 天)
### V0.1 建表
- [ ] 在 SQL Server 执行 `doc/db_init.sql`,创建 6 张表:
- `gateway_nodes`(网关节点)
- `base_device`统一设备主表AdapterCode+SourceId 联合主键)
- `video_channel`(视频通道扩展)
- `video_record`(录像文件)
- `iot_alarm`(告警记录)
- `iot_devicedata`(数据归档)
### V0.2 字典初始化
- [ ] 在 Vol.Pro 管理端 → 字典管理,创建 8 组数据字典:
- 设备分组:视频设备/IoT设备/门禁设备/道闸设备/报警设备
- 设备种类:摄像机/硬盘录像机/温湿度变送器/空调控制器/...18 项)
- 在线状态:在线/离线
- 启用状态:启用/禁用
- 是否父设备:是/否
- 是否控制点:只读/可写
- 告警等级:提示/普通/重要/紧急
- 告警状态:未确认/已确认/已结束
### V0.3 代码生成
- [ ] 在 Vol.Pro 代码生成器中选择 `device_manager` 数据源,生成 6 张表的全套代码:
- `VolPro.Entity/DomainModels/device_manager/` — 6 个 Entity
- `Warehouse/IRepositories/device_manager/` — 6 个 Repository 接口
- `Warehouse/Repositories/device_manager/` — 6 个 Repository 实现
- `Warehouse/IServices/device_manager/` — 6 个 Service 接口 + 6 个 Partial 接口
- `Warehouse/Services/device_manager/` — 6 个 Service 实现 + 6 个 Partial Service
- `VolPro.WebApi/Controllers/Warehouse/` — 6 个 Controller
### V0.4 配置主从表
- [ ] 代码生成器配置 base_device 的 DetailTable
- 关联 video_channelDeviceId=DeviceId
- 关联 iot_devicedataDeviceId=DeviceId
- 关联 iot_alarmDeviceId=DeviceId
- [ ] 代码生成器配置 base_device 的 ParentId 自引用字段ParentDeviceId
### V0.5 构建验证
- [ ] VS 编译 Vol.Pro 解决方案,确认 0 错误
- [ ] 管理端访问 base_device 页面 → 框架默认主从表三 Tab 渲染正常
> **V0 提交点**: `PhaseV0_db_codegen — 6 张表建表 + 字典 + 代码生成完毕,框架默认页面可访问`
---
## Phase V1: Entity 与 Service 扩展(预计 0.5 天)
### V1.1 Entity Partial
- [ ] 创建 `VolPro.Entity/DomainModels/device_manager/partial/base_device.cs`
- 添加导航属性VideoChannel、Alarms、DeviceData
- 添加网关字段白名单常量 `GatewayFields`
- [ ] 创建 `VolPro.Entity/DomainModels/device_manager/partial/gateway_nodes.cs`
- 添加 `AdapterList` 属性(从 AdapterTypes 逗号分隔解析)
- [ ] 其他 4 个 Entity Partial 留空(框架生成即够用)
### V1.2 Service Partial — gateway_nodesService
- [ ] 编辑 `Warehouse/Services/device_manager/Partial/gateway_nodesService.cs`
- 注入 `Igateway_nodesRepository` + `IHttpContextAccessor``[ActivatorUtilitiesConstructor]`
- 实现 `RegisterNodeAsync(nodeCode, token, adapterTypes, baseUrl)` — Upsert 逻辑
- 实现 `UpdateHeartbeatAsync(nodeCode, token)` — 更新心跳
- 实现 `SyncDevicesAsync(gatewayNodeId, List<StandardDevice>)` — 字段分治 + parentSourceId 映射
### V1.3 Service Partial — base_deviceService
- [ ] 编辑 `Warehouse/Services/device_manager/Partial/base_deviceService.cs`
- 注入 `Ibase_deviceRepository`
- 实现 `GetDevicesByGatewayNodeAsync(gatewayNodeId)` — 网关注册时返回设备列表
- 实现 `UpsertDeviceAsync(StandardDevice, gatewayNodeId, existingIds)` — 字段分治
### V1.4 Service Partial — iot_alarmService
- [ ] 编辑 `Warehouse/Services/device_manager/Partial/iot_alarmService.cs`
- 注入 `Iiot_alarmRepository`
- 实现 `UpsertAlarmAsync(StandardAlarm)` — SourceAlarmId 去重
### V1.5 构建验证
- [ ] VS 编译 → 0 错误
> **V1 提交点**: `PhaseV1_entity_service — Entity 导航属性 + Service 同步方法全部就绪`
---
## Phase V2: Controller 扩展(预计 1 天)
### V2.1 gateway_nodesController — A1 网关注册
- [ ] 编辑 `Controllers/Warehouse/Partial/gateway_nodesController.cs`
- 添加 `[HttpPost, Route("/api/gateway/register"), AllowAnonymous]`
- 认证NodeToken 验证
- Upsert存在则更新 AdapterTypes/BaseUrl/IsOnline不存在且 Token 有效则 Insert
- 返回:`{ nodeId, devices }` — 当前网关的顶层设备列表
### V2.2 gateway_nodesController — A2 心跳
- [ ] 添加 `[HttpPost, Route("/api/gateway/heartbeat"), AllowAnonymous]`
- 认证NodeToken
- 更新IsOnline="在线" + LastHeartbeat=now
- 返回:`{ status: "ok", serverTime }`
### V2.3 gateway_nodesController — A3 设备同步
- [ ] 添加 `[HttpPost, Route("/api/gateway/sync/devices"), AllowAnonymous]`
- 认证NodeToken
- 批量查已有 DeviceId 映射表
- 遍历设备:字段分治写入(首次全量,后续仅网关字段)
- parentSourceId → ParentDeviceId 解析
- 返回:`{ added, updated, removed }`
### V2.4 gateway_nodesController — A4 告警同步
- [ ] 添加 `[HttpPost, Route("/api/gateway/sync/alarms"), AllowAnonymous]`
- 认证NodeToken
- 批量查 deviceSourceId → DeviceId 映射
- SourceAlarmId 去重
- 写入 iot_alarmState="未确认"
- 返回:`{ added }`
### V2.5 base_deviceController — 设备树
- [ ] 编辑 `Controllers/Warehouse/Partial/base_deviceController.cs`
- [ ] 添加 `[HttpGet, Route("/api/DeviceManager/GetRegionTree")]`
- 查询warehouse_regions JOIN warehouse_devicepoint
- 构建树形结构region → point含 deviceCount
- 返回:`[{ id, label, type, children, deviceCount }]`
- [ ] 添加 `[HttpGet, Route("/api/DeviceManager/GetDevicesByPoint")]`
- 查询base_device WHERE PointId=pointId含子设备递归
- 分页返回:`{ items, total }`
### V2.6 权限配置
- [ ] A1-A4 接口加 `[AllowAnonymous]`(内部 Token 二次认证)
- [ ] GetRegionTree/GetDevicesByPoint 走框架 JWT 权限
### V2.7 构建验证
- [ ] VS 编译 → 0 错误
- [ ] Postman 测试 A1-A4Mock Token
- [ ] 管理端访问 GetRegionTree → 返回 JSON
> **V2 提交点**: `PhaseV2_controller — 6 个 API 全部就绪A1-A4 AllowAnonymous设备树可查`
---
## Phase V3: 基础设施与定时任务(预计 0.5 天)
### V3.1 GatewayClient
- [ ] 创建 `Warehouse/Services/GatewayClient.cs`
- 实现 `TriggerFullSyncAsync(baseUrl, adapterTypes)` — 调网关 B3
- 实现 `GetRealtimeAsync(baseUrl, adapter, deviceId)` — 调网关 B4
- 实现 `ControlDeviceAsync(baseUrl, adapter, deviceId, pointIndex, value)` — 调网关 B5
### V3.2 Quartz Job — SyncDevicesJob
- [ ] 创建 `Warehouse/Services/SyncDevicesJob.cs`
- 实现 `IJob` 接口
- 遍历 `gateway_nodes WHERE IsOnline=在线 AND Enable=启用`
- 逐个调 `GatewayClient.TriggerFullSyncAsync`
### V3.3 Quartz Job — HeartbeatMonitorJob
- [ ] 创建 `Warehouse/Services/HeartbeatMonitorJob.cs`
- 扫描心跳超时 30s 的网关 → 标记离线
- 级联:`base_device WHERE GatewayNodeId=离线节点Id → IsOnline=离线`
### V3.4 Quartz Job — RealtimePollJobPhase 2
- [ ] 创建 `Warehouse/Services/RealtimePollJob.cs`骨架Phase 2 完善)
### V3.5 Startup.cs 注册
- [ ] 注册 `IHttpClientFactory`Named: "VolPro"
- [ ] 注册 `GatewayClient` 为 Singleton
- [ ] 确认 Quartz Job 在管理端可配置JobDataMap 传入 ServiceProvider
### V3.6 Job 注册
- [ ] 在 Vol.Pro 管理端 → Quartz 管理 → 新建 3 个 Job
- SyncDevicesJobCron "0 */5 * * * ?"
- HeartbeatMonitorJobCron "0/15 * * * * ?"
- RealtimePollJobCron "0/10 * * * * ?"
### V3.7 构建验证
- [ ] VS 编译 → 0 错误
- [ ] GatewayClient 可通过 Swagger 测试
> **V3 提交点**: `PhaseV3_infrastructure — GatewayClient + 3 Job + Startup 注册完毕`
---
## Phase V4: 前端改造(预计 1.5 天)
### V4.1 目录创建
- [ ] 创建 `web.vite/src/views/warehouse/base_device/components/` 目录
- [ ] 创建 `web.vite/src/extension/warehouse/` 目录
- [ ] 创建 `web.vite/src/api/gateway.js` — 网关 B 组 API 封装
### V4.2 gateway.js API 封装
- [ ] `getStreamUrl(adapter, deviceId)` → GET 流地址
- [ ] `ptzControl(adapter, deviceId, direction)` → POST 云台控制
- [ ] `getRealtime(adapter, deviceId)` → GET 实时点值
- [ ] `controlDevice(adapter, deviceId, pointIndex, value)` → POST 控制
- [ ] `getAlarms(adapter, params)` → GET 告警列表
- [ ] `confirmAlarm(adapter, alarmId)` → POST 告警确认
### V4.3 视频操作组件
- [ ] 创建 `VideoDeviceActions.vue` — 按钮组(预览/云台/回放/快照/同步通道)
- [ ] 创建 `DeviceLivePreview.vue` — Jessibuca 播放器弹窗(<video> 回退)
-`getStreamUrl` → 获取 WS-FLV/HLS 地址
- [ ] 创建 `PtzControlPanel.vue` — 方向键面板(↑↓←→+ZOOM+停止)
- mousedown 开始移动mouseup 停止
### V4.4 IoT 操作组件
- [ ] 创建 `IoTDeviceActions.vue` — 按钮组(实时数据/控制/刷新/告警)
- [ ] 创建 `RealtimeDataPanel.vue` — 实时数值弹窗5s 自动轮询 B4
- [ ] 创建 `DeviceControlPanel.vue` — 控制写值面板B5
### V4.5 通用操作组件
- [ ] 创建 `DeviceEditDialog.vue` — 设备编辑弹窗(管理员字段)
- [ ] 创建 `MapBindingPanel.vue` — 地图模型绑定面板模型ID/缩放/旋转)
### V4.6 未来组件(骨架)
- [ ] 创建 `AccessDeviceActions.vue` — 门禁按钮组(远程开门)
- [ ] 创建 `BarrierDeviceActions.vue` — 道闸按钮组(抬杆/落杆)
- [ ] 创建 `AlarmDeviceActions.vue` — 报警按钮组(告警/布防撤防)
### V4.7 扩展注入
- [ ] 创建 `web.vite/src/extension/warehouse/base_device.jsx`
- 注册操作列插槽 `col-action`
-`row.deviceGroup` 动态渲染对应按钮组件
- 视频设备 → VideoDeviceActions
- IoT 设备 → IoTDeviceActions
- 其他 → 框架默认编辑/删除按钮
### V4.8 构建验证
- [ ] 前端 `npm run dev` 启动
- [ ] 访问 base_device 页面 → 操作列根据 DeviceGroup 显示不同按钮
- [ ] 点击"预览"→ 弹窗打开 → 获取流地址
> **V4 提交点**: `PhaseV4_frontend — 11 个组件 + extension 注入完成,操作列动态渲染`
---
## Phase V5: 联调验证(需网关+子系统就绪,预计 1 天)
### V5.1 端到端同步
- [ ] 启动网关 → 调 A1 注册 → gateway_nodes 表新增记录
- [ ] 网关调 A3 上送设备 → base_device 表有新设备
- [ ] 管理端 base_device 页面 → 列表中显示新设备
### V5.2 视频操作
- [ ] 视频设备行 → 点击"实时预览" → 弹窗播放 Owl 视频流
- [ ] 视频设备行 → 云台方向键操作 → Owl PTZ 响应
### V5.3 IoT 操作
- [ ] IoT 设备行 → 点击"实时数据" → 显示 MC4.0 温湿度值
### V5.4 告警确认
- [ ] MC4.0 触发告警 → 网关 A4 上送 → iot_alarm 表新增
- [ ] 管理端确认告警 → 通过 B9 写回 MC4.0
> **V5 提交点**: `PhaseV5_integration — 端到端同步 + 视频操作 + IoT 数据 + 告警确认全链路跑通`
---
> **总周期**: V0-V5 预计 5 个工作日(不含联调等待时间)

View File

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

View File

@@ -0,0 +1,203 @@
# IntegrationGateway 对接网关详细设计 — 任务清单
> **基准文档**: 对接网关设计文档 v1.0
> **分支**: phase/0-infrastructure
> **原则**: 每阶段产出可编译、可提交的独立成果
---
## Phase G0: 项目骨架与核心抽象(预计 1 天)
### G0.1 解决方案与项目结构
- [ ] 创建 `gateway/IntegrationGateway.sln`dotnet new sln
- [ ] 创建 `src/IntegrationGateway.Core/` 类库项目net8.0
- [ ] 创建 `src/IntegrationGateway.Host/` Web 项目net8.0, ASP.NET Core Minimal API
- [ ] Host 引用 Core 项目
- [ ] 确认 `dotnet build` 0 错误
### G0.2 能力接口体系7 个接口)
- [ ] 创建 `Core/Abstractions/IGatewayAdapter.cs` — 基础接口AdapterCode/DisplayName/Capabilities/InitializeAsync/HealthCheckAsync
- [ ] 创建 `Core/Abstractions/IHasOwnDeviceTree.cs` — 对象树接口
- [ ] 创建 `Core/Abstractions/IHasFlatDevices.cs` — 扁平设备列表接口
- [ ] 创建 `Core/Abstractions/IHasPoints.cs` — 实时点位值 + 控制接口
- [ ] 创建 `Core/Abstractions/IHasStreams.cs` — 视频流 + PTZ + 截图接口
- [ ] 创建 `Core/Abstractions/IHasAlarms.cs` — 告警查询 + 确认 + 结束接口
- [ ] 创建 `Core/Abstractions/IHasRecordings.cs` — 录像回放接口
- [ ] 创建 `Core/Abstractions/IAcceptsMetadataPush.cs` — 元数据回写接口
- [ ] 确认 `dotnet build` 0 错误
### G0.3 统一模型10 个类)
- [ ] 创建 `Core/Models/StandardDevice.cs`
- [ ] 创建 `Core/Models/StandardAlarm.cs`
- [ ] 创建 `Core/Models/StandardRecording.cs`
- [ ] 创建 `Core/Models/DeviceTreeNode.cs`
- [ ] 创建 `Core/Models/PointValue.cs`
- [ ] 创建 `Core/Models/StreamUrls.cs`
- [ ] 创建 `Core/Models/PagedResult.cs`(泛型分页容器)
- [ ] 创建 `Core/Models/AdapterCapabilities.cs`
- [ ] 创建 `Core/Models/MetadataChangeSet.cs`
- [ ] 创建 `Core/Models/MetadataPushResult.cs`
- [ ] 确认 `dotnet build` 0 错误
### G0.4 基础设施3 个类)
- [ ] 创建 `Core/Infrastructure/AdapterRegistry.cs` — 注册/查找/并行初始化
- [ ] 创建 `Core/Infrastructure/RateLimiter.cs` — 令牌桶限流器
- [ ] 创建 `Core/Infrastructure/GatewayClientFactory.cs` — Vol.Pro HTTP 客户端工厂
- [ ] 确认 `dotnet build` 0 错误
> **G0 提交点**: `PhaseG0_gateway_core — Core 项目编译通过7 接口 + 10 模型 + 3 基础设施`
---
## Phase G1: Host 宿主与路由注册(预计 0.5 天)
### G1.1 配置文件
- [ ] 创建 `appsettings.json` — Owl/MC4/Gateway 三段配置
- [ ] 配置验证启动时检查必填项VolProBaseUrl/NodeCode/NodeToken/Owl/MC4
### G1.2 启动与注册
- [ ] 实现 `Program.cs` — IHttpClientFactory 注册("VolPro" 命名客户端)
- [ ] 实现适配器初始化(从配置读取生成 OwlAdapter/MC4Adapter 实例 → 注册到 AdapterRegistry
- [ ] 实现并行初始化(`Task.WhenAll`,单个失败不影响其他)
### G1.3 B 组路由14 个端点)
- [ ] `GET /api/gateway/health` — 返回所有适配器健康状态
- [ ] `GET /api/gateway/devices?adapter=&page=&size=&keyword=` — 扁平设备列表
- [ ] `GET /api/gateway/tree?adapter=` — 对象树
- [ ] `GET /api/gateway/streams/{adapter}/{deviceId}/live` — 实时流地址
- [ ] `GET /api/gateway/streams/{adapter}/{deviceId}/playback?start=&end=` — 回放地址
- [ ] `POST /api/gateway/streams/{adapter}/{deviceId}/snapshot` — 截图
- [ ] `POST /api/gateway/streams/{adapter}/{deviceId}/ptz` — 云台控制
- [ ] `GET /api/gateway/realtime/{adapter}/{deviceId}` — 实时点位值
- [ ] `POST /api/gateway/realtime/{adapter}/control` — 设备控制写值
- [ ] `GET /api/gateway/alarms/{adapter}?from=&to=&page=&size=` — 告警查询
- [ ] `POST /api/gateway/alarms/{adapter}/{alarmId}/confirm` — 告警确认
- [ ] `POST /api/gateway/alarms/{adapter}/{alarmId}/end` — 告警结束
- [ ] `GET /api/gateway/recordings/{adapter}/{deviceId}` — 录像列表
- [ ] `POST /api/gateway/devices/sync?adapter=` — 手动触发同步
- [ ] 确认 `dotnet build` 0 错误
### G1.4 错误处理中间件
- [ ] 统一错误响应格式:`{ error, message }`
- [ ] 适配器未找到 → 404 `ADAPTER_NOT_FOUND`
- [ ] 能力不支持 → 404 `CAPABILITY_NOT_SUPPORTED`
- [ ] 第三方超时 → 504 `UPSTREAM_TIMEOUT`
> **G1 提交点**: `PhaseG1_gateway_host — Host 编译通过14 个路由骨架就绪,健康检查可响应`
---
## Phase G2: OwlAdapter预计 1 天)
### G2.1 项目创建
- [ ] 创建 `src/IntegrationGateway.Adapters.Owl/` 类库项目net8.0
- [ ] 引用 Core 项目
- [ ] Host 引用 Adapters.Owl
- [ ] 加入解决方案
### G2.2 OwlAuthHelper
- [ ] 实现 RSA 公钥获取 → 加密登录 → JWT Token 缓存
- [ ] Token 过期前自动刷新(懒刷新)
- [ ] 提供 `GetAuthenticatedClientAsync()` 方法
### G2.3 OwlAdapter 核心实现
- [ ] 实现 `IGatewayAdapter` — AdapterCode="Owl:main"
- [ ] 实现 `IHasFlatDevices``GetDevicesAsync` 分页 + `GetAllDevicesAsync` 全量
- [ ] 实现通道管理 — `GetChannelsAsync` + `GetAllChannelsAsync`
- [ ] 设备映射 — `OwlDevice → StandardDevice`, `OwlChannel → StandardDevice`
### G2.4 OwlAdapter 视频流实现
- [ ] 实现 `IHasStreams.GetLiveUrlAsync` — POST /channels/:id/play → StreamUrls
- [ ] 实现 `IHasStreams.GetPlaybackUrlAsync` — 构建 HLS VOD URL
- [ ] 实现 `IHasStreams.PtzControlAsync` — continuous + stop仅方向键
- [ ] 实现 `IHasStreams.GetSnapshotAsync` — POST /channels/:id/snapshot
### G2.5 OwlAdapter 录像与 Metadata
- [ ] 实现 `IHasRecordings.GetRecordingsAsync` — GET /recordings
- [ ] 实现 `IAcceptsMetadataPush.PushMetadataAsync` — PUT /devices/:id
### G2.6 HealthCheck
- [ ] 实现 `HealthCheckAsync` — 调 /health 端点
- [ ] Owl 认证失败 → HealthCheck = false
### G2.7 构建验证
- [ ] 确认 `dotnet build` 0 错误
- [ ] 确认 `POST /register` 返回 OwlAdapter 在适配器列表中
> **G2 提交点**: `PhaseG2_owl_adapter — OwlAdapter 编译通过3 个接口实现完整FlatDevices+Streams+Recordings+MetadataPush`
---
## Phase G3: MC4Adapter预计 1 天)
### G3.1 项目创建
- [ ] 创建 `src/IntegrationGateway.Adapters.MC4/` 类库项目net8.0
- [ ] 引用 Core 项目
- [ ] Host 引用 Adapters.MC4
- [ ] 加入解决方案
### G3.2 Mc4AuthHelper
- [ ] 实现 POST /api/central/auth/conf/get → Token 缓存8 小时)
- [ ] 提供 `GetAuthenticatedClientAsync()` 方法header["token"]
### G3.3 MC4Adapter 对象树
- [ ] 实现 `IGatewayAdapter` — AdapterCode="MC4:31ku"
- [ ] 实现 `IHasOwnDeviceTree.GetObjectTreeAsync` — POST /api/central/object/tree → DeviceTreeNode[]
- [ ] MC4 节点映射:`Mc4TreeNode → DeviceTreeNode`Type=1区域/2设备
### G3.4 MC4Adapter 实时数据
- [ ] 实现 `IHasPoints.GetRealtimeValuesAsync` — POST /api/central/device/point/value/get → PointValue[]
- [ ] 实现 `IHasPoints.GetMultiPointValuesAsync` — POST /api/central/point/multi/value/get
- [ ] 实现 `IHasPoints.SetPointValueAsync` — POST /api/central/point/value/set
### G3.5 MC4Adapter 告警
- [ ] 实现 `IHasAlarms.GetAlarmsAsync` — POST /api/central/alarm/queryskip/limit 转 page/size
- [ ] 实现 `IHasAlarms.ConfirmAlarmAsync` — POST /api/central/alarm/confirm
- [ ] 实现 `IHasAlarms.EndAlarmAsync` — POST /api/central/alarm/end
- [ ] 告警去重:`SourceAlarmId` 唯一
### G3.6 限流与验证
- [ ] RateLimiter = new RateLimiter(2)MC4.0 QPS 限制)
- [ ] 确认 `dotnet build` 0 错误
> **G3 提交点**: `PhaseG3_mc4_adapter — MC4Adapter 编译通过3 个接口实现完整OwnDeviceTree+Points+Alarms`
---
## Phase G4: 联调验证(需子系统就绪,预计 1 天)
### G4.1 Owl 联调
- [ ] 启动 Owl+ZLM → 网关 RSA 登录成功
- [ ] `GET /api/gateway/devices?adapter=Owl:main` → 返回设备列表
- [ ] `GET /api/gateway/streams/Owl:main/{channelId}/live` → 返回 WS-FLV 地址
- [ ] `POST /api/gateway/streams/Owl:main/{channelId}/ptz` → 云台响应
### G4.2 MC4.0 联调
- [ ] `GET /api/gateway/tree?adapter=MC4:31ku` → 返回对象树 JSON
- [ ] `GET /api/gateway/realtime/MC4:31ku/{deviceId}` → 返回点位值
- [ ] `GET /api/gateway/alarms/MC4:31ku` → 返回告警列表
### G4.3 故障隔离验证
- [ ] 停止 MC4.0 → `GET /health` 中 Owl=OK, MC4=FAIL
- [ ] Owl 设备查询仍正常返回
> **G4 提交点**: `PhaseG4_integration — 联调通过,健康检查报告真实状态`
---
## Phase G5: 部署与文档(预计 0.5 天)
### G5.1 发布
- [ ] `dotnet publish` Release 模式
- [ ] Dockerfile 编写与构建
### G5.2 运维文档
- [ ] 启动脚本(单机/Docker
- [ ] 配置说明(环境变量覆盖)
- [ ] 健康检查命令
> **G5 提交点**: `PhaseG5_deploy — 可发布的网关二进制 + 运维文档`
---
> **总周期**: G0-G5 预计 4 个工作日(不含联调等待时间)