# Vol.Pro + MC4.0 采集网关整合方案
> **版本**: v1.0
> **日期**: 2026-04-29
> **编制**: 浮浮酱
> **状态**: 待确认(未实施)
---
## 一、项目概述
### 1.1 需求背景
MC4.0 是一款采集网关设备,负责接入温湿度传感器、空调控制器、485接口设备、开关量设备等,通过 HTTP API 对外提供设备管理、数据采集、反向控制和告警查询能力。
**目标**: 将 MC4.0 采集网关接入 Vol.Pro 后端,实现:
- 在 Vol.Pro 管理端中对采集设备进行增删改查管理
- warehouse 用户端实时展示设备数据(温湿度等)
- warehouse 用户端手动控制设备(如空调开关、温度设定)
- 接收并展示 MC4.0 的告警信息
### 1.2 MC4.0 API 能力分析
| 功能模块 | 接口 | 说明 |
|----------|------|------|
| **认证** | `/api/central/auth/login` | Token 认证(有效期需确认) |
| **对象树** | `/api/central/object/tree` | 获取区域+设备的树形结构 |
| **点表** | `/api/central/device/point/get` | 获取设备下的点(传感器/控制点) |
| **读数据** | `/api/central/device/point/value/get` | 按设备获取所有点的实时值 |
| **读数据** | `/api/central/point/multi/value/get` | 批量获取指定设备点的值 |
| **写控制** | `/api/central/point/value/set` | 设置设备点的值(反向控制) |
| **告警查询** | `/api/central/alarm/query` | 按条件查询告警(时间/设备/状态) |
| **告警处理** | `/api/central/alarm/confirm` | 确认告警 |
| **告警处理** | `/api/central/alarm/end` | 结束告警 |
| **历史告警** | `/api/central/his_alarm/query` | 查询历史告警记录 |
### 1.3 关键前提:所有修改独立于框架代码
> 与视频监控方案一致,所有自定义代码均写在 Vol.Pro 的扩展文件中,不修改任何框架自动生成的代码。
>
> - Controller 扩展写在 `Controllers/模块/Partial/`
> - Service 扩展写在 `Services/模块/Partial/`
> - Entity 扩展写在 `DomainModels/模块/partial/`
> - 前端扩展写在 `extension/` 目录
> - 新增独立服务实现 `IDependency`,Autofac 自动注入
---
## 二、系统架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ web.vite 管理端 │ │ warehouse 用户端 │ │
│ │ (设备管理+告警查看) │ │ (实时数据+设备控制) │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
└─────────────┼──────────────────────────┼────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Vol.Pro 后端 (api_sqlsugar) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 自动生成CRUD模块 │ │
│ │ ├─ Base_Device (统一设备主表,含视频监控+采集网关) │ │
│ │ ├─ Device_IoT_Ext (采集设备扩展信息) │ │
│ │ ├─ IoT_DevicePoint (设备点表/传感器点) │ │
│ │ ├─ IoT_DeviceData (实时/历史数据记录) │ │
│ │ └─ IoT_Alarm (告警记录管理) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 扩展API模块 (IoTStreamController) │ │
│ │ ├─ POST /api/IoTStream/syncTree → 同步MC4.0对象树 │ │
│ │ ├─ POST /api/IoTStream/syncPoints → 同步设备点表 │ │
│ │ ├─ POST /api/IoTStream/realtime → 获取实时数据 │ │
│ │ ├─ POST /api/IoTStream/control → 设备反向控制 │ │
│ │ ├─ POST /api/IoTStream/alarms → 查询告警列表 │ │
│ │ ├─ POST /api/IoTStream/confirmAlarm→ 确认告警 │ │
│ │ └─ POST /api/IoTStream/pollAlarms → 手动轮询最新告警 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MC4.0对接服务 (Mc4ApiService) │ │
│ │ ├─ Token管理 (登录/缓存/刷新) │ │
│ │ ├─ 对象树同步 (调用 /object/tree) │ │
│ │ ├─ 点表同步 (调用 /device/point/get) │ │
│ │ ├─ 实时数据获取 (调用 /device/point/value/get) │ │
│ │ ├─ 设备控制 (调用 /point/value/set) │ │
│ │ ├─ 告警查询 (调用 /alarm/query) │ │
│ │ └─ 告警确认/结束 (调用 /alarm/confirm, /alarm/end) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 告警轮询任务 (Quartz Job) │ │
│ │ ├─ 每30秒轮询MC4.0最新告警 │ │
│ │ ├─ 将新告警写入 IoT_Alarm 表 │ │
│ │ └─ 通过 SignalR 推送给在线前端 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────────────┘
│ HTTP API (端口3000)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ MC4.0 采集网关 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 设备接入层 (温湿度/空调/485/开关量) │ │
│ │ 数据缓存层 │ │
│ │ HTTP API层 (对象树/点表/读值/写值/告警) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.2 关键设计原则
1. **MC4.0 作为唯一设备源**: Vol.Pro 不直接管理设备接入,所有设备数据来自 MC4.0 的对象树
2. **数据双写**: MC4.0 中的设备、点表、告警通过同步机制写入 Vol.Pro 数据库
3. **定时轮询**: MC4.0 不支持主动推送,Vol.Pro 后端通过 Quartz 定时任务轮询实时数据和告警
4. **SignalR 推送**: Vol.Pro 后端通过 SignalR 将实时数据和告警主动推送给 warehouse 前端
---
## 三、数据库表设计
### 3.1 表结构说明
> **设备统一管理设计**:本方案与视频监控方案共用统一设备主表 `Base_Device`,实现视频监控设备和采集网关设备的统一管理,并支持在三维地图上统一标记展示。
#### 3.1.0 统一设备主表 (Base_Device) 【与视频监控方案共用】
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| DeviceId | uniqueidentifier | ✅ | 主键,Vol.Pro系统内部ID |
| DeviceName | nvarchar(100) | ✅ | 设备名称 |
| DeviceCategory | int | ✅ | 设备大类:1=视频监控, 2=采集网关 |
| DeviceType | int | | 设备细分类型(根据Category解释不同值) |
| SourceId | nvarchar(64) | ✅ | 源系统设备ID(OwlDeviceId或Mc4DeviceId) |
| IpAddress | nvarchar(50) | | IP地址 |
| Port | int | | 端口 |
| IsOnline | int | | 是否在线:0=离线, 1=在线 |
| Location | nvarchar(200) | | 安装位置描述 |
| Lat | float | | 纬度(WGS84坐标系) |
| Lng | float | | 经度(WGS84坐标系) |
| **MapModelId** | **nvarchar(100)** | | **三维地图模型ID(VgoMap中对应模型的唯一标识)** |
| MapModelScale | float | | 三维模型缩放比例,默认1.0 |
| MapModelRotation | nvarchar(100) | | 三维模型旋转角度(JSON格式:{"x":0,"y":0,"z":0}) |
| Enable | int | | 启用状态:0=禁用, 1=启用 |
| Remark | nvarchar(500) | | 备注 |
| CreateID / Creator / CreateDate | | | Vol.Pro标准审计字段 |
| ModifyID / Modifier / ModifyDate | | | Vol.Pro标准审计字段 |
> **MapModelId 字段说明**:
> - 用于在warehouse前端三维地图(VgoMap)中标记设备位置
> - 值由三维地图系统提供,标识对应三维模型的唯一ID
> - 管理端可在设备编辑页面中配置/修改此字段
> - 三维地图加载时,根据MapModelId将设备图标/模型绑定到地图对应位置
#### 3.1.1 采集设备扩展表 (Device_IoT_Ext)
> 存储采集网关设备特有的扩展信息,通过DeviceId关联Base_Device主表。
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| ExtId | uniqueidentifier | ✅ | 主键 |
| DeviceId | uniqueidentifier | ✅ | 关联Base_Device.DeviceId |
| Mc4DeviceId | int | ✅ | MC4.0 设备ID |
| ObjectType | int | | MC4.0 objectType 原始值 |
| Tag | nvarchar(100) | | 设备标签 |
| ParentId | int | | MC4.0 父级ID(区域层级) |
| Mc4Option | nvarchar(500) | | MC4.0 option 原始JSON |
#### 3.1.2 设备点表 (IoT_DevicePoint)
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| PointId | uniqueidentifier | ✅ | 主键 |
| DeviceId | uniqueidentifier | ✅ | 关联 Base_Device.DeviceId |
| Mc4DeviceId | int | ✅ | MC4.0 设备ID |
| PointIndex | int | ✅ | 点索引(MC4.0中的index) |
| PointType | int | | 点类型(MC4.0中的type) |
| PointTag | nvarchar(100) | | 点标签 |
| PointName | nvarchar(100) | ✅ | 点名称 |
| PointDesc | nvarchar(200) | | 点描述 |
| Unit | nvarchar(50) | | 单位(如 ℃, %, V) |
| IsControlPoint | int | | 是否是控制点:0=只读, 1=可写 |
| Mc4Option | nvarchar(500) | | MC4.0 option 原始JSON |
| Enable | int | | 启用状态 |
| CreateDate | datetime | | 创建时间 |
#### 3.1.3 设备数据记录表 (IoT_DeviceData) 【仅用于历史归档】
> **⚠️ 重要设计变更**:实时数据**不入库**,直接通过SignalR推送给前端展示。本表仅用于存储**历史归档数据**(如每小时/每天保存一次快照)。
>
> **原因**:若每次轮询都入库,100设备×10点×每30秒轮询 = 288万条/天,SQL Server单表性能会急剧下降。
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| DataId | uniqueidentifier | ✅ | 主键 |
| DeviceId | uniqueidentifier | ✅ | 关联设备 |
| PointId | uniqueidentifier | ✅ | 关联点 |
| PointValue | float | | 点值 |
| UpdateTime | datetime | ✅ | MC4.0 数据更新时间 |
| Interval | int | | 距离上一次采集间隔(毫秒) |
| CreateDate | datetime | | 记录入库时间 |
| ArchiveType | int | | 归档类型:1=小时归档, 2=日归档 |
#### 3.1.4 告警记录表 (IoT_Alarm)
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| AlarmId | uniqueidentifier | ✅ | 主键 |
| Mc4AlarmId | nvarchar(64) | ✅ | MC4.0 告警ID |
| DeviceId | uniqueidentifier | | 关联设备 |
| AlarmType | int | | 告警类型 |
| AlarmLevel | int | | 告警等级:1=提示, 2=普通, 3=重要, 4=紧急 |
| AlarmDesc | nvarchar(500) | | 告警描述 |
| AlarmValue | float | | 触发值 |
| StartTime | datetime | ✅ | 告警开始时间 |
| EndTime | datetime | | 告警结束时间 |
| ConfirmTime | datetime | | 确认时间 |
| ConfirmUser | nvarchar(50) | | 确认人 |
| State | int | | 状态:1=未确认, 2=已确认, 3=已结束 |
| IsDischarge | int | | 是否是放电告警 |
| Mc4Option | nvarchar(500) | | MC4.0 原始JSON |
| CreateDate | datetime | | 入库时间 |
### 3.2 建表SQL (SQL Server)
```sql
-- ============================================
-- 统一设备主表(与视频监控方案共用)
-- ============================================
CREATE TABLE Base_Device (
DeviceId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
DeviceName NVARCHAR(100) NOT NULL,
DeviceCategory INT NOT NULL, -- 1=视频监控, 2=采集网关
DeviceType INT, -- 细分类型(根据Category解释)
SourceId NVARCHAR(64) NOT NULL, -- 源系统设备ID(OwlDeviceId或Mc4DeviceId)
IpAddress NVARCHAR(50),
Port INT,
IsOnline INT DEFAULT 0,
Location NVARCHAR(200), -- 安装位置描述
Lat FLOAT, -- 纬度
Lng FLOAT, -- 经度
MapModelId NVARCHAR(100), -- 三维地图模型ID(VgoMap)
MapModelScale FLOAT DEFAULT 1.0, -- 三维模型缩放比例
MapModelRotation NVARCHAR(100), -- 三维模型旋转角度(JSON)
Enable INT DEFAULT 1,
Remark NVARCHAR(500),
CreateID INT,
Creator NVARCHAR(30),
CreateDate DATETIME DEFAULT GETDATE(),
ModifyID INT,
Modifier NVARCHAR(30),
ModifyDate DATETIME
);
CREATE INDEX IX_Base_Device_Category ON Base_Device(DeviceCategory);
CREATE INDEX IX_Base_Device_SourceId ON Base_Device(SourceId);
CREATE INDEX IX_Base_Device_IsOnline ON Base_Device(IsOnline);
CREATE INDEX IX_Base_Device_MapModelId ON Base_Device(MapModelId);
-- ============================================
-- 采集网关设备扩展表
-- ============================================
CREATE TABLE Device_IoT_Ext (
ExtId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
DeviceId UNIQUEIDENTIFIER NOT NULL,
Mc4DeviceId INT NOT NULL,
ObjectType INT,
Tag NVARCHAR(100),
ParentId INT,
Mc4Option NVARCHAR(500),
FOREIGN KEY (DeviceId) REFERENCES Base_Device(DeviceId)
);
CREATE INDEX IX_Device_IoT_Ext_DeviceId ON Device_IoT_Ext(DeviceId);
CREATE INDEX IX_Device_IoT_Ext_Mc4DeviceId ON Device_IoT_Ext(Mc4DeviceId);
-- 设备点表
CREATE TABLE IoT_DevicePoint (
PointId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
DeviceId UNIQUEIDENTIFIER 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 INT DEFAULT 0,
Mc4Option NVARCHAR(500),
Enable INT DEFAULT 1,
CreateDate DATETIME DEFAULT GETDATE()
);
CREATE INDEX IX_IoT_DevicePoint_DeviceId ON IoT_DevicePoint(DeviceId);
CREATE INDEX IX_IoT_DevicePoint_Mc4DeviceId ON IoT_DevicePoint(Mc4DeviceId);
-- 设备数据记录表
CREATE TABLE IoT_DeviceData (
DataId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
DeviceId UNIQUEIDENTIFIER NOT NULL,
PointId UNIQUEIDENTIFIER NOT NULL,
PointValue FLOAT,
UpdateTime DATETIME NOT NULL,
Interval INT,
CreateDate DATETIME DEFAULT GETDATE()
);
CREATE INDEX IX_IoT_DeviceData_DeviceId ON IoT_DeviceData(DeviceId);
CREATE INDEX IX_IoT_DeviceData_PointId ON IoT_DeviceData(PointId);
CREATE INDEX IX_IoT_DeviceData_UpdateTime ON IoT_DeviceData(UpdateTime);
-- 告警记录表
CREATE TABLE IoT_Alarm (
AlarmId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
Mc4AlarmId NVARCHAR(64) NOT NULL,
DeviceId UNIQUEIDENTIFIER,
AlarmType INT,
AlarmLevel INT,
AlarmDesc NVARCHAR(500),
AlarmValue FLOAT,
StartTime DATETIME NOT NULL,
EndTime DATETIME,
ConfirmTime DATETIME,
ConfirmUser NVARCHAR(50),
State INT DEFAULT 1,
IsDischarge INT DEFAULT 0,
Mc4Option NVARCHAR(500),
CreateDate DATETIME DEFAULT GETDATE()
);
CREATE INDEX IX_IoT_Alarm_Mc4AlarmId ON IoT_Alarm(Mc4AlarmId);
CREATE INDEX IX_IoT_Alarm_DeviceId ON IoT_Alarm(DeviceId);
CREATE INDEX IX_IoT_Alarm_State ON IoT_Alarm(State);
CREATE INDEX IX_IoT_Alarm_StartTime ON IoT_Alarm(StartTime);
```
---
## 四、Vol.Pro 后端扩展方案
### 4.1 代码生成步骤
使用 Vol.Pro 代码生成器自动生成模块的 CRUD 代码:
1. **Base_Device**(统一设备主表,含视频监控+采集网关设备)
2. **Device_IoT_Ext**(采集设备扩展信息)
3. **IoT_DevicePoint**(设备点表)
4. **IoT_DeviceData**(数据记录)
5. **IoT_Alarm**(告警记录)
### 4.2 MC4.0 对接服务 (Mc4ApiService)
**新建独立服务文件**(不修改任何框架代码):
```csharp
// 文件位置:api_sqlsugar/Warehouse/Services/Mc4/Mc4ApiService.cs
///
/// MC4.0 采集网关对接服务
///
public interface IMc4ApiService : IDependency
{
/// 获取/刷新 MC4.0 Token
Task GetTokenAsync();
/// 同步 MC4.0 对象树(区域+设备)
Task> SyncObjectTreeAsync();
/// 同步指定设备的点表
Task> SyncDevicePointsAsync(int mc4DeviceId);
/// 获取设备实时数据(所有点)
Task> GetDeviceRealtimeDataAsync(int mc4DeviceId);
/// 批量获取指定点的实时数据
Task> GetMultiPointValuesAsync(List points);
/// 控制设备点值(反向控制)
Task SetPointValueAsync(int mc4DeviceId, int pointIndex, double value);
/// 查询告警列表
Task QueryAlarmsAsync(Mc4AlarmQueryInput input);
/// 确认告警
Task ConfirmAlarmAsync(string alarmId);
/// 结束告警
Task EndAlarmAsync(string alarmId);
}
```
**实现要点**:
1. **新建文件**,实现 `IDependency`,Autofac 自动注入
2. 使用 `IHttpClientFactory` 创建指向 MC4.0(端口3000)的 HTTP 客户端
3. Token 获取后缓存到内存,失效前自动刷新
4. 所有返回值统一转换为 Vol.Pro 的 `WebResponseContent` 格式
### 4.3 扩展 API 控制器 (IoTStreamController)
**新建独立 Controller**(不修改任何框架代码):
```csharp
// 文件位置:api_sqlsugar/Warehouse/Controllers/IoTStreamController.cs
[Route("api/IoTStream")]
[JWTAuthorize]
public class IoTStreamController : VolController
{
private readonly IMc4ApiService _mc4Api;
private readonly IBase_DeviceService _deviceService;
public IoTStreamController(
IMc4ApiService mc4Api,
IBase_DeviceService deviceService)
{
_mc4Api = mc4Api;
_deviceService = deviceService;
}
///
/// 同步 MC4.0 对象树(区域+设备)
///
[HttpPost, Route("SyncTree")]
public async Task SyncTree()
{
var nodes = await _mc4Api.SyncObjectTreeAsync();
// 将对象树写入 Base_Device 主表(DeviceCategory=2采集网关,DeviceType=1区域/2设备)
// ... 同步逻辑
return Json(new WebResponseContent().OK(null, $"同步完成,共{nodes.Count}个节点"));
}
///
/// 同步指定设备的点表
///
[HttpPost, Route("SyncPoints")]
public async Task SyncPoints([FromBody] SyncPointsInput input)
{
var device = await _deviceService.GetDeviceByIdAsync(input.DeviceId);
if (device == null) return Json(new WebResponseContent().Error("设备不存在"));
var points = await _mc4Api.SyncDevicePointsAsync(device.Mc4DeviceId);
return Json(new WebResponseContent().OK(null, points));
}
///
/// 获取设备实时数据
///
[HttpPost, Route("GetRealtime")]
public async Task GetRealtime([FromBody] GetRealtimeInput input)
{
var device = await _deviceService.GetDeviceByIdAsync(input.DeviceId);
if (device == null) return Json(new WebResponseContent().Error("设备不存在"));
var data = await _mc4Api.GetDeviceRealtimeDataAsync(device.Mc4DeviceId);
return Json(new WebResponseContent().OK(null, data));
}
///
/// 设备反向控制
///
[HttpPost, Route("Control")]
[ApiActionPermission("Base_Device", ActionPermissionOptions.Update)]
public async Task Control([FromBody] ControlInput input)
{
var device = await _deviceService.GetDeviceByIdAsync(input.DeviceId);
if (device == null) return Json(new WebResponseContent().Error("设备不存在"));
var success = await _mc4Api.SetPointValueAsync(
device.Mc4DeviceId, input.PointIndex, input.Value);
return Json(success
? new WebResponseContent().OK(null, "控制指令已下发")
: new WebResponseContent().Error("控制失败"));
}
///
/// 查询告警列表
///
[HttpPost, Route("GetAlarms")]
public async Task GetAlarms([FromBody] AlarmQueryInput input)
{
var result = await _mc4Api.QueryAlarmsAsync(new Mc4AlarmQueryInput
{
Sids = input.DeviceIds,
From = input.StartTime,
To = input.EndTime,
ConfirmState = input.ConfirmState,
EndState = input.EndState,
Skip = (input.Page - 1) * input.Rows,
Limit = input.Rows
});
return Json(new WebResponseContent().OK(null, new {
total = result.Total,
rows = result.List
}));
}
///
/// 确认告警
///
[HttpPost, Route("ConfirmAlarm")]
public async Task ConfirmAlarm([FromBody] AlarmActionInput input)
{
var success = await _mc4Api.ConfirmAlarmAsync(input.AlarmId);
return Json(success
? new WebResponseContent().OK()
: new WebResponseContent().Error("确认失败"));
}
///
/// 手动触发告警轮询(用于前端主动刷新)
///
[HttpPost, Route("PollAlarms")]
public async Task PollAlarms()
{
// 触发一次告警轮询任务
// ... 调用后台Job或直接查询MC4.0
return Json(new WebResponseContent().OK(null, "轮询已触发"));
}
}
```
### 4.4 告警轮询任务 (Quartz Job)
MC4.0 不支持主动推送告警,Vol.Pro 后端通过 **Quartz 定时任务** 定时轮询:
```csharp
// 文件位置:api_sqlsugar/Warehouse/Jobs/AlarmPollJob.cs
// (新建文件,实现 IDependency)
///
/// MC4.0 告警轮询定时任务(每10秒执行一次)
///
public class AlarmPollJob : IDependency
{
private readonly IMc4ApiService _mc4Api;
private readonly IIoT_AlarmService _alarmService;
private readonly IHubContext _hubContext;
public AlarmPollJob(
IMc4ApiService mc4Api,
IIoT_AlarmService alarmService,
IHubContext hubContext)
{
_mc4Api = mc4Api;
_alarmService = alarmService;
_hubContext = hubContext;
}
public async Task ExecuteAsync()
{
// 1. 查询最近30秒内的告警(缩短窗口匹配轮询间隔)
var result = await _mc4Api.QueryAlarmsAsync(new Mc4AlarmQueryInput
{
From = DateTime.Now.AddSeconds(-30).ToString("yyyy-MM-dd HH:mm:ss"),
To = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
Limit = 100
});
// 2. 过滤已入库的新告警(按Mc4AlarmId去重)
var newAlarms = await FilterNewAlarmsAsync(result.List);
// 3. 写入 IoT_Alarm 表
foreach (var alarm in newAlarms)
{
await _alarmService.AddAsync(alarm);
}
// 4. 通过 SignalR 按设备分组推送给前端(避免全量广播)
foreach (var alarm in newAlarms)
{
if (alarm.DeviceId.HasValue)
{
await _hubContext.Clients.Group($"device_{alarm.DeviceId}")
.SendAsync("NewAlarm", alarm);
}
}
// 5. 同时推送给告警管理员组
if (newAlarms.Any())
{
await _hubContext.Clients.Group("alarm_admin")
.SendAsync("NewAlarms", newAlarms);
}
}
}
```
### 4.5 实时数据轮询任务(Quartz Job)
> **设计变更**:实时数据**不入库**,直接通过SignalR推送给前端展示。
```csharp
// 文件位置:api_sqlsugar/Warehouse/Jobs/RealtimeDataPollJob.cs
// (新建文件,实现 IDependency)
///
/// 实时数据轮询任务(每5秒执行一次)
/// 注意:数据不入库,仅通过SignalR推送给前端
///
public class RealtimeDataPollJob : IDependency
{
private readonly IMc4ApiService _mc4Api;
private readonly IBase_DeviceService _deviceService;
private readonly IHubContext _hubContext;
public RealtimeDataPollJob(
IMc4ApiService mc4Api,
IBase_DeviceService deviceService,
IHubContext hubContext)
{
_mc4Api = mc4Api;
_deviceService = deviceService;
_hubContext = hubContext;
}
public async Task ExecuteAsync()
{
// 1. 获取所有启用的采集网关设备
var devices = await _deviceService.GetDevicesAsync(
d => d.DeviceCategory == 2 && d.Enable == 1);
// 2. 批量获取实时数据(使用批量接口减少调用次数)
foreach (var device in devices)
{
var data = await _mc4Api.GetDeviceRealtimeDataAsync(device.Mc4DeviceId);
// 3. 直接SignalR推送给订阅了该设备的客户端(不入库!)
await _hubContext.Clients.Group($"device_{device.DeviceId}")
.SendAsync("ReceiveRealtimeData", new {
deviceId = device.DeviceId,
deviceName = device.DeviceName,
points = data,
updateTime = DateTime.Now
});
}
}
}
```
> **数据归档策略**(可选):
> 如需保存历史数据,可另建一个**每小时执行一次**的归档Job,将数据写入`IoT_DeviceData`表(ArchiveType=1小时归档)。
### 4.6 MC4.0 请求频率控制
为防止高频轮询触发MC4.0限流,在`Mc4ApiService`中实现请求频率控制:
```csharp
public class Mc4ApiService : IMc4ApiService
{
private static readonly SemaphoreSlim _rateLimiter = new SemaphoreSlim(2, 2); // 每秒最多2个并发请求
private async Task ExecuteWithRateLimitAsync(Func> action)
{
await _rateLimiter.WaitAsync();
try
{
return await action();
}
finally
{
// 500ms后释放,确保每秒不超过2次请求
await Task.Delay(500);
_rateLimiter.Release();
}
}
public async Task> GetDeviceRealtimeDataAsync(int mc4DeviceId)
{
return await ExecuteWithRateLimitAsync(async () =>
{
// 实际HTTP调用
});
}
}
```
### 4.7 SignalR 实时数据推送
warehouse 前端需要实时接收数据更新和告警推送,Vol.Pro 后端通过 SignalR 实现:
```csharp
// 文件位置:api_sqlsugar/Warehouse/Hubs/IoTDataHub.cs
// (新建文件)
public interface IIoTDataHub
{
Task ReceiveRealtimeData(List data);
Task ReceiveNewAlarm(IoTAlarmDto alarm); // 单条告警推送(按设备分组)
Task ReceiveNewAlarms(List alarms); // 批量告警推送(管理员组)
Task DeviceControlResult(string deviceId, bool success, string message);
}
[Authorize]
public class IoTDataHub : Hub
{
/// 订阅指定设备的实时数据和告警
public async Task SubscribeDevice(string deviceId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"device_{deviceId}");
}
public async Task UnsubscribeDevice(string deviceId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"device_{deviceId}");
}
/// 订阅告警管理员组(接收所有告警)
public async Task SubscribeAlarmAdmin()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "alarm_admin");
}
public async Task UnsubscribeAlarmAdmin()
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "alarm_admin");
}
}
```
---
## 五、前端对接方案
### 5.1 warehouse 用户端改造
warehouse 需要实现以下功能模块:
1. **实时数据看板**(新增页面或扩展现有页面):
- 展示设备列表和当前实时数据(温湿度、电压等)
- 使用 SignalR 实时接收数据更新
- 支持按区域筛选设备
2. **设备控制面板**(新增):
- 显示可控制的设备点(如空调开关、温度设定)
- 提供开关、滑块、输入框等控制组件
- 调用 `/api/IoTStream/Control` 下发控制指令
3. **告警信息面板**(新增或扩展现有告警模块):
- 实时展示最新告警(通过 SignalR 推送)
- 支持告警确认、结束操作
- 告警历史查询
**warehouse 前端 API 调用示例**:
```typescript
import http from '@/api/http'
// 获取设备列表(统一设备主表,过滤DeviceCategory=2的采集网关设备)
const getDevices = () => {
return http.post('/api/Base_Device/GetPageData', {
page: 1, rows: 100, sort: 'CreateDate', order: 'desc',
wheres: JSON.stringify([{ name: 'DeviceCategory', value: '2', displayType: 'int' }])
})
}
// 获取设备实时数据
const getRealtimeData = (deviceId: string) => {
return http.post('/api/IoTStream/GetRealtime', { deviceId })
}
// 控制设备
const controlDevice = (deviceId: string, pointIndex: number, value: number) => {
return http.post('/api/IoTStream/Control', { deviceId, pointIndex, value })
}
// 查询告警
const getAlarms = (params: any) => {
return http.post('/api/IoTStream/GetAlarms', params)
}
// 确认告警
const confirmAlarm = (alarmId: string) => {
return http.post('/api/IoTStream/ConfirmAlarm', { alarmId })
}
// SignalR 连接
import * as signalR from '@microsoft/signalr'
const connection = new signalR.HubConnectionBuilder()
.withUrl('/hubs/iotData')
.withAutomaticReconnect()
.build()
// 连接成功后订阅当前页面展示的设备
connection.start().then(() => {
// 订阅设备实时数据(按设备分组)
const deviceIds = ['guid1', 'guid2'] // 当前页面展示的设备ID
deviceIds.forEach(id => {
connection.invoke('SubscribeDevice', id)
})
// 告警管理页面订阅管理员组
connection.invoke('SubscribeAlarmAdmin')
})
connection.on('ReceiveRealtimeData', (data) => {
// 更新对应设备的实时数据到页面
console.log(`设备${data.deviceName}数据更新:`, data.points)
})
connection.on('ReceiveNewAlarm', (alarm) => {
// 单条告警通知(来自订阅的设备)
ElNotification({
title: '新告警',
message: `${alarm.deviceName}: ${alarm.alarmDesc}`,
type: 'warning'
})
})
connection.on('ReceiveNewAlarms', (alarms) => {
// 批量告警(管理员组)
// 更新告警列表
})
```
### 5.2 管理端 (web.vite) 扩展
通过 Vol.Pro 代码生成器生成的 CRUD 页面,扩展以下功能:
1. **设备列表页扩展**(`extension/warehouse/Base_Device.jsx`):
- 添加"同步对象树"按钮(仅对DeviceCategory=2的设备显示)
- 添加"同步点表"操作列
- 添加"三维地图定位"操作(可跳转地图并高亮对应模型)
- 添加"查看实时数据"按钮
2. **告警列表页扩展**:
- 添加"确认"和"结束"操作按钮
- 告警等级颜色标识(紧急=红色,重要=橙色)
---
## 六、关键 API 接口文档
### 6.1 Vol.Pro 后端对外接口
#### 6.1.1 同步对象树
```
POST /api/IoTStream/SyncTree
Authorization: Bearer
Response:
{
"status": true,
"message": "同步完成,共15个节点",
"data": { "total": 15, "devices": 8, "areas": 7 }
}
```
#### 6.1.2 获取设备实时数据
```
POST /api/IoTStream/GetRealtime
Authorization: Bearer
Request:
{
"deviceId": "guid"
}
Response:
{
"status": true,
"message": "ok",
"data": [
{ "index": 1, "name": "温度", "value": 24.5, "unit": "℃", "time": "2026-04-29 10:30:00" },
{ "index": 2, "name": "湿度", "value": 65.0, "unit": "%", "time": "2026-04-29 10:30:00" }
]
}
```
#### 6.1.3 设备反向控制
```
POST /api/IoTStream/Control
Authorization: Bearer
Request:
{
"deviceId": "guid",
"pointIndex": 10,
"value": 1
}
Response:
{
"status": true,
"message": "控制指令已下发"
}
```
#### 6.1.4 查询告警
```
POST /api/IoTStream/GetAlarms
Authorization: Bearer
Request:
{
"deviceIds": [],
"startTime": "2026-04-28 00:00:00",
"endTime": "2026-04-29 23:59:59",
"confirmState": 1,
"endState": 0,
"page": 1,
"rows": 20
}
Response:
{
"status": true,
"message": "ok",
"data": {
"total": 5,
"rows": [
{
"alarmId": "guid",
"deviceName": "机房空调A",
"alarmDesc": "温度过高告警",
"alarmValue": 32.5,
"level": 3,
"startTime": "2026-04-29 10:15:00",
"state": 1
}
]
}
}
```
### 6.2 MC4.0 内部接口(Vol.Pro 后端调用)
| 接口 | 说明 | 用途 |
|------|------|------|
| `POST /api/central/auth/login` | 登录获取 Token | 认证 |
| `POST /api/central/object/tree` | 对象树 | 同步设备 |
| `POST /api/central/device/point/get` | 设备点表 | 同步点表 |
| `POST /api/central/device/point/value/get` | 设备实时数据 | 读数据 |
| `POST /api/central/point/multi/value/get` | 批量点值 | 批量读 |
| `POST /api/central/point/value/set` | 设置点值 | 反向控制 |
| `POST /api/central/alarm/query` | 告警查询 | 告警同步 |
| `POST /api/central/alarm/confirm` | 确认告警 | 告警处理 |
| `POST /api/central/alarm/end` | 结束告警 | 告警处理 |
---
## 七、实施计划
### 阶段一:数据库与代码生成(2天)
1. 执行建表 SQL 创建 4 张表
2. 使用 Vol.Pro 代码生成器生成 CRUD 代码
3. 验证生成后的增删改查功能正常
### 阶段二:MC4.0 对接服务(3天)
**新建独立文件**:
1. 创建 `Warehouse/Services/Mc4/IMc4ApiService.cs`(接口)
2. 创建 `Warehouse/Services/Mc4/Mc4ApiService.cs`(实现)
3. 实现 Token 管理(登录/缓存/刷新)
4. 实现对象树同步、点表同步、实时数据获取
5. 实现设备控制、告警查询
6. 创建 `Warehouse/Controllers/IoTStreamController.cs`(全新 Controller)
### 阶段三:实时数据与告警推送(2天)
1. 创建 `Warehouse/Hubs/IoTDataHub.cs`(SignalR Hub,支持按设备分组)
2. 创建 `Warehouse/Jobs/RealtimeDataPollJob.cs`(实时数据轮询,每5秒,**不入库仅推送**)
3. 创建 `Warehouse/Jobs/AlarmPollJob.cs`(告警轮询,每10秒)
4. 配置定时任务和频率控制(每秒最多2次MC4.0请求)
5. 实现SignalR分组推送逻辑
### 阶段四:管理端前端扩展(1天)
1. 扩展设备列表页(同步按钮、实时数据查看)
2. 扩展告警列表页(确认/结束操作)
### 阶段五:warehouse 用户端开发(3天)
1. 开发实时数据看板页面(设备列表 + 实时数值展示)
2. 开发设备控制面板(开关、滑块、输入框)
3. 开发告警信息面板(实时告警 + 历史查询)
4. 集成 SignalR 客户端,实现实时数据接收
### 阶段六:联调测试(2天)
1. MC4.0 设备接入测试(温湿度、空调等)
2. 实时数据读取测试(验证SignalR推送,不入库)
3. 反向控制测试(空调开关、温度设定)
4. 告警轮询与推送测试(验证10秒轮询+分组推送)
5. 请求频率控制测试(验证每秒不超过2次请求)
6. 多设备并发测试
**总计预计工期**: 16个工作日(含3天缓冲)
---
## 八、风险与注意事项
### 8.1 MC4.0 API 限制
1. **轮询频率限制**: MC4.0 可能对接口调用频率有限制,已通过`SemaphoreSlim`实现每秒最多2次请求的频率控制
2. **Token 有效期**: 文档未明确 Token 有效期,需实测确认,建议每次请求前检查 Token 是否过期
3. **无主动推送**: MC4.0 仅支持查询式接口,实时数据和告警都需要轮询获取
### 8.2 数据同步策略
1. **对象树同步**: 建议在管理端提供手动同步按钮,MC4.0 设备变更时手动触发
2. **点表同步**: 在对象树同步后,对每个设备自动同步点表
3. **⚠️ 实时数据不入库**: 实时数据直接通过SignalR推送给前端,**不写入`IoT_DeviceData`表**
4. **历史归档**(可选): 如需保存历史数据,可配置每小时/每天归档Job,将数据写入`IoT_DeviceData`表(ArchiveType标记归档类型)
### 8.3 性能建议
1. **批量查询**: 使用 `/api/central/point/multi/value/get` 批量获取点值,减少接口调用次数
2. **数据缓存**: 对不频繁变化的数据(如设备列表、点表)做 Redis 缓存
3. **SignalR 分组**: 前端只订阅当前页面展示的设备,避免不必要的推送
4. **告警去重**: 轮询到的告警通过 `Mc4AlarmId` 去重,避免重复入库
5. **请求频率控制**: `Mc4ApiService`中已实现每秒最多2次并发请求的限制
### 8.4 安全建议
1. MC4.0 的登录密码建议配置在 `appsettings.json` 的机密配置段
2. 设备控制接口需要权限校验(`[ApiActionPermission]`)
3. MC4.0 的 Token 缓存需加密存储
---
## 附录A:统一设备管理与三维地图标记
### A.1 统一设备管理设计
本方案与视频监控方案共用 **`Base_Device` 统一设备主表**,实现所有设备的集中管理:
| 设备大类 (DeviceCategory) | 细分类型 (DeviceType) | 扩展表 | 对接系统 |
|--------------------------|----------------------|--------|----------|
| 1 = 视频监控 | 1=IPC, 2=NVR, 3=Platform | `Device_Video_Ext` | Owl + ZLMediaKit |
| 2 = 采集网关 | 1=区域, 2=设备(见MC4.0类型表) | `Device_IoT_Ext` | MC4.0 采集网关 |
**统一查询示例**:
```sql
-- 查询所有设备(含视频监控+采集网关)
SELECT * FROM Base_Device WHERE Enable = 1
-- 查询所有视频监控设备
SELECT * FROM Base_Device WHERE DeviceCategory = 1
-- 查询所有采集网关设备
SELECT * FROM Base_Device WHERE DeviceCategory = 2
-- 联查设备+视频扩展信息
SELECT d.*, v.ChannelCount, v.Protocol
FROM Base_Device d
LEFT JOIN Device_Video_Ext v ON d.DeviceId = v.DeviceId
WHERE d.DeviceCategory = 1
-- 联查设备+采集扩展信息
SELECT d.*, i.Mc4DeviceId, i.ObjectType
FROM Base_Device d
LEFT JOIN Device_IoT_Ext i ON d.DeviceId = i.DeviceId
WHERE d.DeviceCategory = 2
```
### A.2 三维地图标记 (VgoMap)
warehouse 前端使用 **VgoMap** 三维地图引擎,设备需要在地图上进行三维标记。
#### A.2.1 MapModelId 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| **MapModelId** | nvarchar(100) | 三维地图中对应模型的唯一标识符 |
| **MapModelScale** | float | 模型缩放比例,默认1.0 |
| **MapModelRotation** | nvarchar(100) | 模型旋转角度,JSON格式 `{"x":0,"y":90,"z":0}` |
**配置流程**:
1. 在VgoMap三维地图编辑器中,为每个设备位置放置一个模型(如摄像头图标、传感器图标)
2. 记录每个模型的ID(VgoMap提供的唯一标识)
3. 在Vol.Pro管理端的设备编辑页面中,填写对应设备的`MapModelId`
4. 可选配置缩放比例和旋转角度
#### A.2.2 前端三维地图集成示例
```typescript
// warehouse 前端加载设备标记
const loadDeviceMarkers = async () => {
// 1. 从Vol.Pro后端获取所有设备(含MapModelId)
const res = await http.post('/api/Base_Device/GetPageData', {
page: 1, rows: 1000, wheres: JSON.stringify([{ name: 'Enable', value: '1' }])
});
const devices = res.rows;
// 2. 在VgoMap中根据MapModelId绑定设备数据
devices.forEach(device => {
if (device.MapModelId) {
// 绑定设备数据到地图模型
window.$map.setModelData(device.MapModelId, {
deviceId: device.DeviceId,
deviceName: device.DeviceName,
category: device.DeviceCategory,
isOnline: device.IsOnline
});
// 设置模型样式(在线/离线不同颜色)
const color = device.IsOnline === 1 ? '#67c23a' : '#f56c6c';
window.$map.setModelColor(device.MapModelId, color);
}
});
};
// 点击地图模型时显示设备详情
window.$map.on('modelClick', (e) => {
const deviceData = e.modelData;
if (deviceData) {
showDeviceDetail(deviceData.deviceId);
}
});
// 实时更新设备状态(通过SignalR)
connection.on('DeviceStatusChanged', (deviceId, isOnline) => {
const device = devices.find(d => d.DeviceId === deviceId);
if (device && device.MapModelId) {
const color = isOnline ? '#67c23a' : '#f56c6c';
window.$map.setModelColor(device.MapModelId, color);
}
});
```
#### A.2.3 管理端地图配置界面
在Vol.Pro管理端的设备编辑页面中,扩展地图配置表单:
```javascript
// extension/warehouse/Base_Device.jsx 中的扩展
methods: {
modelOpenAfter(row) {
// 在编辑表单中添加三维地图配置字段
this.editFormOptions.push([
{ title: '三维地图模型ID', field: 'MapModelId', type: 'text' },
{ title: '模型缩放比例', field: 'MapModelScale', type: 'number' },
{ title: '模型旋转角度', field: 'MapModelRotation', type: 'text' }
]);
}
}
```
### A.3 设备数据流汇总
```
┌─────────────────────────────────────────────────────────────────┐
│ 统一设备数据流 │
├─────────────────────────────────────────────────────────────────┤
│ Base_Device (统一主表) │
│ ├─ DeviceCategory=1 → Device_Video_Ext → Owl/ZLM (视频流) │
│ └─ DeviceCategory=2 → Device_IoT_Ext → MC4.0 (温湿度/控制) │
├─────────────────────────────────────────────────────────────────┤
│ 三维地图 (VgoMap) │
│ └─ 所有设备通过 MapModelId 在地图上统一标记展示 │
└─────────────────────────────────────────────────────────────────┘
```
---
> **文档结束**
> **版本**: v1.0
> **最后更新**: 2026-04-29
> **状态**: 待主人确认后实施