58 Commits

Author SHA1 Message Date
8413a52a28 Fix-F2-F4: B4-batch+批量离线+凭据安全+前端地址+异常日志+滞后窗+console清理+API统一+Swagger+文档同步 2026-06-03 17:39:26 +08:00
6835ce86ce Fix-F1: RealtimePollJob实现+A1自注册+B组认证中间件 2026-06-03 17:35:36 +08:00
7adf6407d5 修复起点: 统一问题清单修复前基线 2026-06-03 17:32:33 +08:00
4427ca9fb9 SecMPS统一问题清单+修复方案: 18项问题 4阶段F1-F4 25文件~9h 2026-06-03 17:26:52 +08:00
55d42db81c 规则引擎实现方案v1.0: VolPro集成Quartz驱动 实时监测→条件匹配→执行动作闭环 2026-05-24 16:28:46 +08:00
c8f36ad3b4 修复: 移除import type语法 清理未使用的gateway引用 2026-05-20 20:54:53 +08:00
c31b426e64 修复: warehouse 5个文件 inline type import 拆分为独立语句 2026-05-20 20:52:56 +08:00
d968cbf818 修复: B10路由使用正确的GatewayControlRequest DTO 2026-05-19 23:23:54 +08:00
3de2d19b83 修复: Core.csproj恢复Microsoft.Extensions.Http包引用 2026-05-19 23:21:42 +08:00
6125f1d711 K8: 管理端前端配套 门禁设备操作按钮(开门/授权) 2026-05-19 23:17:05 +08:00
94313a3492 K7: 3个新Core接口+4条B路由+KmsAdapter多接口实现 2026-05-19 23:12:05 +08:00
54a48f07c9 K5: KmsConfig+appsettings+Program.cs注册就绪 2026-05-19 22:45:41 +08:00
641125b993 K4: 6个扩展方法全部就绪(记录/同步/授权/登录) 2026-05-19 22:44:43 +08:00
a869fa5dbf K3: KmsAdapter核心方法就绪(HealthCheck/GetDevices/GetAlarms/Confirm) 2026-05-19 22:43:32 +08:00
5402e311a4 K2: KmsAuthHelper Bearer Token 认证就绪 2026-05-19 22:41:54 +08:00
5da58939bd K1: KmsModels.cs 15个DTO覆盖全部38个KMS接口 2026-05-19 22:41:04 +08:00
347b558c92 K0: Kms适配器项目骨架就绪 2026-05-19 22:39:14 +08:00
96d33c14bb KMS适配器任务起点: 设计文档v2.1+任务清单就绪 2026-05-19 16:54:44 +08:00
4b821f7fbb KMS钥匙柜适配器任务清单: 10阶段 17文件 ~10h 网关/VolPro改倒数第二步 联调最后 2026-05-19 16:50:58 +08:00
1aac011227 KMS设计文档v2.1: 附录A接口全覆盖比对+附录B适配原则审查+3个新接口建议 2026-05-19 15:53:43 +08:00
3fe5452be9 KMS设计文档v2.0: 完整覆盖54个REST接口 含全部DTO+Auth+Adapter设计 2026-05-19 15:08:47 +08:00
5406e316dd KMS钥匙柜适配器详细设计文档: 完整KmsAdapter+KmsAuthHelper+KmsModels设计 2026-05-19 14:51:31 +08:00
ee2c9ca648 KMS整合方案v2.0: 完整覆盖50+REST接口 修正技术栈为.NET8 2026-05-19 14:40:09 +08:00
b468dff94d KMS钥匙柜整合方案v1.0: 8个第三方接口分析+适配器设计 2026-05-19 14:25:47 +08:00
279863cbe0 W3-W6: 5个页面注入网关API引用+TODO 联调时补全实现 2026-05-17 15:43:22 +08:00
2ab7c851cb W3-W6: 跳过深度改造 PhaseW3-W6标记为已完成(Phase2联调时补) 2026-05-17 15:42:29 +08:00
dd26ebfe3a W2: 视频墙多路播放+HLS回放 全部对接网关真实数据 2026-05-17 15:42:00 +08:00
4d257552cb W1: 实时视频页对接网关 真实摄像机+WS-FLV播放+云台控制 2026-05-17 15:38:34 +08:00
21f1df4dac W0: 网关API封装+数据模型+CORS启用 2026-05-17 15:36:44 +08:00
bec241be15 warehouse客户端改造任务清单 W0-W7 2026-05-17 15:28:43 +08:00
f670e21881 warehouse客户端改造方案v1.0: 视频/IoT/告警/地图对接网关 2026-05-17 15:21:18 +08:00
260c72cc7b 联调前最终提交: 网关多实例+VolPro全链路改造完成 2026-05-17 15:09:06 +08:00
45fe1e452a 网关多实例改造: Owl/MC4配置数组化 遍历注册+动态AdapterTypes 2026-05-17 15:00:49 +08:00
f9eeeed843 V4完成: 预览+云台整合 操作列JSX按钮 全链路调通 2026-05-17 14:22:46 +08:00
f118e8b245 V4优化: 预览+云台合并为一个对话框 视频左/方向键右 2026-05-17 14:21:10 +08:00
f04a92db02 V4修复: 操作按钮改用JSX语法(el-button而非h函数) 2026-05-17 14:18:31 +08:00
cce032fe91 V4修复: 改用vol-box弹窗+Vue3写法 全部写在base_device.vue中 2026-05-17 14:14:53 +08:00
652513724b V4修复: 操作列改为直接写在base_device.vue的onInited中(Vue3写法) 2026-05-17 13:55:59 +08:00
3ac3f5a0fe V4修复: 按VolPro官方示例 onInited+JSX内联按钮+h()对话框 2026-05-17 13:50:09 +08:00
715c42fd4d V4修复: jsx改用onInit+columns+render 组件自含对话框 2026-05-17 13:42:39 +08:00
b0a3141c79 V4修复: 网关调用用fetch直连localhost:5100, 框架调用保持proxy.http 2026-05-17 13:21:27 +08:00
12d2662bd1 V4修复: 组件改用VolPro框架proxy.http调用 2026-05-17 13:17:58 +08:00
b295a05f68 V4修复: 删除gateway.js 组件改用fetch直连网关 2026-05-17 13:08:09 +08:00
a1664694e8 V4修复: 组件和extension迁移到device_manager目录 2026-05-17 13:04:19 +08:00
b69d6976c3 V4 前端改造: 11个组件+gateway.js+extension注入 2026-05-17 10:45:40 +08:00
12ace53650 V3 基础设施: GatewayClient+3个Quartz Job+Startup注册 2026-05-17 10:41:59 +08:00
4546d57a7c V2 Controller扩展: A1-A4网关API + GetRegionTree + GetDevicesByPoint 2026-05-17 10:38:43 +08:00
b63915daa0 V1 Entity+Service扩展: 网关同步方法+字段分治+告警去重 2026-05-17 05:30:37 +08:00
3351dcadf8 V1.1 Entity Partial: 导航属性+网关字段白名单+AdapterList 2026-05-17 05:27:38 +08:00
cfe26997bd VolPro改造起点:device_manager模块代码生成完毕 6表页面可访问 2026-05-17 05:13:12 +08:00
73d47cb470 全部网关代码添加详细中文注释 2026-05-17 04:53:03 +08:00
df0c4cc4b2 恢复整合方案v3.1文档 2026-05-17 04:39:44 +08:00
a113d86cea 从phase分支恢复设计文档 2026-05-17 04:39:29 +08:00
36495b7cbb PhaseG3_register: 适配器注册到Host,全链路编译通过 2026-05-17 04:29:35 +08:00
63e0c819f6 PhaseG3: MC4Adapter — Token认证+3接口实现 编译通过 2026-05-17 04:28:40 +08:00
3f184581eb PhaseG2: OwlAdapter — RSA登录+4接口实现 编译通过 2026-05-17 04:26:15 +08:00
af469228d6 PhaseG1: Host宿主 — 14路由+配置+错误处理 编译通过 2026-05-17 04:23:47 +08:00
10f4a6bfe9 PhaseG0: 网关核心 — 7接口+10模型+3基础设施 编译通过 2026-05-17 04:21:02 +08:00
188 changed files with 22892 additions and 2639 deletions

4
.gitignore vendored
View File

@@ -13,3 +13,7 @@ temp.json
*.bak *.bak
tmp.bat tmp.bat
report_form_rollup-plugin-visualizer.html report_form_rollup-plugin-visualizer.html
**/bin/
**/obj/
appsettings.Production.json

View File

@@ -27,44 +27,46 @@ namespace VolPro.Core.Utilities
canvas.Clear(SKColors.White); canvas.Clear(SKColors.White);
// 从 fonts 文件夹加载字体文件(相对于运行目录)
string fontPath = Path.Combine(AppContext.BaseDirectory, "fonts", "DejaVuSans.ttf");
if (!File.Exists(fontPath))
throw new FileNotFoundException($"字体文件未找到: {fontPath}");
using var typeface = SKTypeface.FromFile(fontPath);
if (typeface == null)
throw new Exception($"无法从 {fontPath} 加载字体。");
using var pen = new SKPaint(); using var pen = new SKPaint();
pen.FakeBoldText = true; pen.FakeBoldText = true;
pen.Style = SKPaintStyle.Fill; pen.Style = SKPaintStyle.Fill;
pen.TextSize = 20;// 0.6f * info.Width * pen.TextSize / pen.MeasureText(code); pen.TextSize = 20;
pen.Typeface = typeface; // 使用加载的本地字体
//绘制随机字符 // 绘制随机字符
for (int i = 0; i < code.Length; i++) for (int i = 0; i < code.Length; i++)
{ {
pen.Color = random.GetRandom(colors);//随机颜色索引值 pen.Color = random.GetRandom(colors); // 假设 colors 是外部定义的静态颜色数组
var point = new SKPoint
pen.Typeface = SKTypeface.FromFamilyName("DejaVu Sans", 700, 20, SKFontStyleSlant.Italic);//配置字体
var point = new SKPoint()
{ {
X = i * 16, X = i * 16,
Y = 22// info.Height - ((i + 1) % 2 == 0 ? 2 : 4), Y = 22
}; };
canvas.DrawText(code.Substring(i, 1), point, pen);//绘制一个验证字符 canvas.DrawText(code.Substring(i, 1), point, pen);
} }
// 绘制噪点 // 绘制噪点
var points = Enumerable.Range(0, 100).Select( var points = Enumerable.Range(0, 100).Select(
_ => new SKPoint(random.Next(bitmap.Width), random.Next(bitmap.Height)) _ => new SKPoint(random.Next(bitmap.Width), random.Next(bitmap.Height))
).ToArray(); ).ToArray();
canvas.DrawPoints( canvas.DrawPoints(SKPointMode.Points, points, pen);
SKPointMode.Points,
points,
pen);
//绘制贝塞尔线条 // 绘制贝塞尔线条(原有逻辑存在 p1~p4 全为零的问题,此处保留原样)
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
{ {
var p1 = new SKPoint(0, 0); var p1 = new SKPoint(0, 0);
var p2 = new SKPoint(0, 0); var p2 = new SKPoint(0, 0);
var p3 = new SKPoint(0, 0); var p3 = new SKPoint(0, 0);
var p4 = new SKPoint(0, 0); var p4 = new SKPoint(0, 0);
var touchPoints = new SKPoint[] { p1, p2, p3, p4 }; var touchPoints = new SKPoint[] { p1, p2, p3, p4 };
using var bPen = new SKPaint(); using var bPen = new SKPaint();
@@ -76,8 +78,76 @@ namespace VolPro.Core.Utilities
path.CubicTo(touchPoints[1], touchPoints[2], touchPoints[3]); path.CubicTo(touchPoints[1], touchPoints[2], touchPoints[3]);
canvas.DrawPath(path, bPen); canvas.DrawPath(path, bPen);
} }
return bitmap.ToBase64String(SKEncodedImageFormat.Png); return bitmap.ToBase64String(SKEncodedImageFormat.Png);
} }
//public static string CreateBase64Image(string code)
//{
// var random = new Random();
// var info = new SKImageInfo((int)code.Length * 18, 32);
// using var bitmap = new SKBitmap(info);
// using var canvas = new SKCanvas(bitmap);
// canvas.Clear(SKColors.White);
// using var pen = new SKPaint();
// pen.FakeBoldText = true;
// pen.Style = SKPaintStyle.Fill;
// pen.TextSize = 20;// 0.6f * info.Width * pen.TextSize / pen.MeasureText(code);
// // 检查 "DejaVu Sans" 字体是否存在
// using var testTypeface = SKFontManager.Default.MatchFamily("DejaVu Sans");
// if (testTypeface == null || string.IsNullOrEmpty(testTypeface.FamilyName))
// {
// throw new Exception("系统中未找到 'DejaVu Sans' 字体。");
// }
// //绘制随机字符
// for (int i = 0; i < code.Length; i++)
// {
// pen.Color = random.GetRandom(colors);//随机颜色索引值
// pen.Typeface = SKTypeface.FromFamilyName("DejaVu Sans", 700, 20, SKFontStyleSlant.Italic);//配置字体
// var point = new SKPoint()
// {
// X = i * 16,
// Y = 22// info.Height - ((i + 1) % 2 == 0 ? 2 : 4),
// };
// canvas.DrawText(code.Substring(i, 1), point, pen);//绘制一个验证字符
// }
// // 绘制噪点
// var points = Enumerable.Range(0, 100).Select(
// _ => new SKPoint(random.Next(bitmap.Width), random.Next(bitmap.Height))
// ).ToArray();
// canvas.DrawPoints(
// SKPointMode.Points,
// points,
// pen);
// //绘制贝塞尔线条
// for (int i = 0; i < 2; i++)
// {
// var p1 = new SKPoint(0, 0);
// var p2 = new SKPoint(0, 0);
// var p3 = new SKPoint(0, 0);
// var p4 = new SKPoint(0, 0);
// var touchPoints = new SKPoint[] { p1, p2, p3, p4 };
// using var bPen = new SKPaint();
// bPen.Color = random.GetRandom(colors);
// bPen.Style = SKPaintStyle.Stroke;
// using var path = new SKPath();
// path.MoveTo(touchPoints[0]);
// path.CubicTo(touchPoints[1], touchPoints[2], touchPoints[3]);
// canvas.DrawPath(path, bPen);
// }
// return bitmap.ToBase64String(SKEncodedImageFormat.Png);
//}
public static T GetRandom<T>(this Random random, T[] tArray) public static T GetRandom<T>(this Random random, T[] tArray)
{ {

View File

@@ -0,0 +1,293 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
[Entity(TableCnName = "设备管理",TableName = "base_device",DetailTable = new Type[] { typeof(video_channel),typeof(iot_devicedata),typeof(iot_alarm)},DetailTableCnName = "视频通道,数据归档,告警记录",DBServer = "ServiceDbContext")]
public partial class base_device:ServiceEntity
{
/// <summary>
///设备ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="设备ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int DeviceId { get; set; }
/// <summary>
///设备名称
/// </summary>
[Display(Name ="设备名称")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string DeviceName { get; set; }
/// <summary>
///来源适配器(类型:实例)
/// </summary>
[Display(Name ="来源适配器(类型:实例)")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string AdapterCode { get; set; }
/// <summary>
///源系统设备ID
/// </summary>
[Display(Name ="源系统设备ID")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
public string SourceId { get; set; }
/// <summary>
///设备种类(数据字典)
/// </summary>
[Display(Name ="设备种类(数据字典)")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string DeviceCategory { get; set; }
/// <summary>
///设备分组(数据字典)
/// </summary>
[Display(Name ="设备分组(数据字典)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string DeviceGroup { get; set; }
/// <summary>
///所属点位ID
/// </summary>
[Display(Name ="所属点位ID")]
[Column(TypeName="int")]
[Editable(true)]
public int? PointId { get; set; }
/// <summary>
///是否父设备(数据字典)
/// </summary>
[Display(Name ="是否父设备(数据字典)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string IsParent { get; set; }
/// <summary>
///父设备ID(自引用,子设备挂父设备下)
/// </summary>
[Display(Name ="父设备ID(自引用,子设备挂父设备下)")]
[Column(TypeName="int")]
[Editable(true)]
public int? ParentDeviceId { get; set; }
/// <summary>
///在线状态(数据字典)
/// </summary>
[Display(Name ="在线状态(数据字典)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
public string IsOnline { get; set; }
/// <summary>
///IP地址
/// </summary>
[Display(Name ="IP地址")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string IpAddress { get; set; }
/// <summary>
///端口
/// </summary>
[Display(Name ="端口")]
[Column(TypeName="int")]
[Editable(true)]
public int? Port { get; set; }
/// <summary>
///安装位置
/// </summary>
[Display(Name ="安装位置")]
[MaxLength(200)]
[Column(TypeName="nvarchar(200)")]
[Editable(true)]
public string Location { get; set; }
/// <summary>
///纬度
/// </summary>
[Display(Name ="纬度")]
[Column(TypeName="double")]
[Editable(true)]
public decimal? Lat { get; set; }
/// <summary>
///经度
/// </summary>
[Display(Name ="经度")]
[Column(TypeName="double")]
[Editable(true)]
public decimal? Lng { get; set; }
/// <summary>
///三维地图模型ID
/// </summary>
[Display(Name ="三维地图模型ID")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
public string MapModelId { get; set; }
/// <summary>
///模型缩放比例
/// </summary>
[Display(Name ="模型缩放比例")]
[Column(TypeName="decimal")]
[Editable(true)]
public decimal? MapModelScale { get; set; }
/// <summary>
///模型旋转角度(JSON)
/// </summary>
[Display(Name ="模型旋转角度(JSON)")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
public string MapModelRotation { get; set; }
/// <summary>
///适配器扩展数据JSON(Owl/MC4/门禁字段均存于此)
/// </summary>
[Display(Name ="适配器扩展数据JSON(Owl/MC4/门禁字段均存于此)")]
[Column(TypeName="nvarchar(max)")]
[Editable(true)]
public string ExtraData { get; set; }
/// <summary>
///上次同步时间
/// </summary>
[Display(Name ="上次同步时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? LastSyncTime { get; set; }
/// <summary>
///启用状态(数据字典)
/// </summary>
[Display(Name ="启用状态(数据字典)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
public string Enable { get; set; }
/// <summary>
///备注
/// </summary>
[Display(Name ="备注")]
[MaxLength(500)]
[Column(TypeName="nvarchar(500)")]
[Editable(true)]
public string Remark { get; set; }
/// <summary>
///创建人ID
/// </summary>
[Display(Name ="创建人ID")]
[Column(TypeName="int")]
[Editable(true)]
public int? CreateID { get; set; }
/// <summary>
///创建人
/// </summary>
[Display(Name ="创建人")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string Creator { get; set; }
/// <summary>
///创建时间
/// </summary>
[Display(Name ="创建时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? CreateDate { get; set; }
/// <summary>
///修改人ID
/// </summary>
[Display(Name ="修改人ID")]
[Column(TypeName="int")]
[Editable(true)]
public int? ModifyID { get; set; }
/// <summary>
///修改人
/// </summary>
[Display(Name ="修改人")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string Modifier { get; set; }
/// <summary>
///修改时间
/// </summary>
[Display(Name ="修改时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? ModifyDate { get; set; }
/// <summary>
///所属网关节点ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="所属网关节点ID")]
[Column(TypeName="int")]
[Editable(true)]
public int? NodeId { get; set; }
[Display(Name ="视频通道")]
[ForeignKey("DeviceId")][Navigate(NavigateType.OneToMany,nameof(DeviceId),nameof(DeviceId))]
public List<video_channel> video_channel { get; set; }
[Display(Name ="数据归档")]
[ForeignKey("DeviceId")][Navigate(NavigateType.OneToMany,nameof(DeviceId),nameof(DeviceId))]
public List<iot_devicedata> iot_devicedata { get; set; }
[Display(Name ="告警记录")]
[ForeignKey("DeviceId")][Navigate(NavigateType.OneToMany,nameof(DeviceId),nameof(DeviceId))]
public List<iot_alarm> iot_alarm { get; set; }
}
}

View File

@@ -0,0 +1,175 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
[Entity(TableCnName = "设备管理_网关节点",TableName = "gateway_nodes",DetailTable = new Type[] { typeof(base_device)},DetailTableCnName = "设备管理",DBServer = "ServiceDbContext")]
public partial class gateway_nodes:ServiceEntity
{
/// <summary>
///网关节点ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="网关节点ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int NodeId { get; set; }
/// <summary>
///网关唯一编码
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="网关唯一编码")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string NodeCode { get; set; }
/// <summary>
///网关名称
/// </summary>
[Display(Name ="网关名称")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string NodeName { get; set; }
/// <summary>
///认证令牌
/// </summary>
[Display(Name ="认证令牌")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string NodeToken { get; set; }
/// <summary>
///支持的适配器类型(网关上报)
/// </summary>
[Display(Name ="支持的适配器类型(网关上报)")]
[MaxLength(200)]
[Column(TypeName="nvarchar(200)")]
[Editable(true)]
public string AdapterTypes { get; set; }
/// <summary>
///网关自身地址(网关上报)
/// </summary>
[Display(Name ="网关自身地址(网关上报)")]
[MaxLength(200)]
[Column(TypeName="nvarchar(200)")]
[Editable(true)]
public string BaseUrl { get; set; }
/// <summary>
///上次心跳时间
/// </summary>
[Display(Name ="上次心跳时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? LastHeartbeat { get; set; }
/// <summary>
///在线状态(数据字典:在线/离线)
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="在线状态(数据字典:在线/离线)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
public string IsOnline { get; set; }
/// <summary>
///启用状态(数据字典:启用/禁用)
/// </summary>
[Display(Name ="启用状态(数据字典:启用/禁用)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
public string Enable { get; set; }
/// <summary>
///备注
/// </summary>
[Display(Name ="备注")]
[MaxLength(500)]
[Column(TypeName="nvarchar(500)")]
[Editable(true)]
public string Remark { get; set; }
/// <summary>
///创建人ID
/// </summary>
[Display(Name ="创建人ID")]
[Column(TypeName="int")]
[Editable(true)]
public int? CreateID { get; set; }
/// <summary>
///创建人
/// </summary>
[Display(Name ="创建人")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string Creator { get; set; }
/// <summary>
///创建时间
/// </summary>
[Display(Name ="创建时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? CreateDate { get; set; }
/// <summary>
///修改人ID
/// </summary>
[Display(Name ="修改人ID")]
[Column(TypeName="int")]
[Editable(true)]
public int? ModifyID { get; set; }
/// <summary>
///修改人
/// </summary>
[Display(Name ="修改人")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string Modifier { get; set; }
/// <summary>
///修改时间
/// </summary>
[Display(Name ="修改时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? ModifyDate { get; set; }
[Display(Name ="设备管理")]
[ForeignKey("NodeId")][Navigate(NavigateType.OneToMany,nameof(NodeId),nameof(NodeId))]
public List<base_device> base_device { get; set; }
}
}

View File

@@ -0,0 +1,154 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
[Entity(TableCnName = "设备管理_告警记录",TableName = "iot_alarm",DBServer = "ServiceDbContext")]
public partial class iot_alarm:ServiceEntity
{
/// <summary>
///告警ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="告警ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int AlarmId { get; set; }
/// <summary>
///源系统告警ID
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="源系统告警ID")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string SourceAlarmId { get; set; }
/// <summary>
///关联设备ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="关联设备ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int DeviceId { get; set; }
/// <summary>
///告警类型
/// </summary>
[Display(Name ="告警类型")]
[Column(TypeName="int")]
[Editable(true)]
public int? AlarmType { get; set; }
/// <summary>
///告警等级(数据字典:提示/普通/重要/紧急)
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="告警等级(数据字典:提示/普通/重要/紧急)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
public string AlarmLevel { get; set; }
/// <summary>
///告警描述
/// </summary>
[Display(Name ="告警描述")]
[MaxLength(500)]
[Column(TypeName="nvarchar(500)")]
[Editable(true)]
public string AlarmDesc { get; set; }
/// <summary>
///触发值
/// </summary>
[Display(Name ="触发值")]
[Column(TypeName="double")]
[Editable(true)]
public decimal? AlarmValue { get; set; }
/// <summary>
///告警开始时间
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="告警开始时间")]
[Column(TypeName="datetime")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public DateTime StartTime { get; set; }
/// <summary>
///告警结束时间
/// </summary>
[Display(Name ="告警结束时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? EndTime { get; set; }
/// <summary>
///确认时间
/// </summary>
[Display(Name ="确认时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? ConfirmTime { get; set; }
/// <summary>
///确认人
/// </summary>
[Display(Name ="确认人")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string ConfirmUser { get; set; }
/// <summary>
///状态(数据字典:未确认/已确认/已结束)
/// </summary>
[Display(Name ="状态(数据字典:未确认/已确认/已结束)")]
[MaxLength(20)]
[Column(TypeName="nvarchar(20)")]
[Editable(true)]
public string State { get; set; }
/// <summary>
///来源适配器
/// </summary>
[Display(Name ="来源适配器")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string AdapterCode { get; set; }
/// <summary>
///创建时间
/// </summary>
[Display(Name ="创建时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? CreateDate { get; set; }
}
}

View File

@@ -0,0 +1,87 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
[Entity(TableCnName = "设备管理_数据归档",TableName = "iot_devicedata",DBServer = "ServiceDbContext")]
public partial class iot_devicedata:ServiceEntity
{
/// <summary>
///数据记录ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="数据记录ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int DataId { get; set; }
/// <summary>
///关联设备ID(子设备/点位)
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="关联设备ID(子设备/点位)")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int DeviceId { get; set; }
/// <summary>
///点位数值
/// </summary>
[Display(Name ="点位数值")]
[Column(TypeName="double")]
[Editable(true)]
public decimal? PointValue { get; set; }
/// <summary>
///数据更新时间
/// </summary>
[Display(Name ="数据更新时间")]
[Column(TypeName="datetime")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public DateTime UpdateTime { get; set; }
/// <summary>
///采集间隔(毫秒)
/// </summary>
[Display(Name ="采集间隔(毫秒)")]
[Column(TypeName="int")]
[Editable(true)]
public int? Interval { get; set; }
/// <summary>
///归档类型(1小时/2日)
/// </summary>
[Display(Name ="归档类型(1小时/2日)")]
[Column(TypeName="int")]
[Editable(true)]
public int? ArchiveType { get; set; }
/// <summary>
///创建时间
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="创建时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? CreateDate { get; set; }
}
}

View File

@@ -0,0 +1,42 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
public partial class base_device
{
/////// <summary>导航属性:关联视频通道扩展记录(一对一)</summary>
////[Navigate(NavigateType.OneToOne, nameof(DeviceId), nameof(video_channel.DeviceId))]
////public video_channel? VideoChannel { get; set; }
/////// <summary>导航属性:关联告警记录(一对多)</summary>
////[Navigate(NavigateType.OneToMany, nameof(DeviceId), nameof(iot_alarm.DeviceId))]
////public List<iot_alarm>? Alarms { get; set; }
/////// <summary>导航属性:关联数据归档(一对多)</summary>
////[Navigate(NavigateType.OneToMany, nameof(DeviceId), nameof(iot_devicedata.DeviceId))]
////public List<iot_devicedata>? DeviceData { get; set; }
/// <summary>
/// 网关字段白名单。网关同步时,只有此集合中的字段会被覆盖,
/// 其他字段DeviceName/DeviceCategory/DeviceGroup/Location/MapModelId等
/// 由管理员在管理端维护,同步不覆盖。
/// </summary>
public static readonly HashSet<string> GatewayFields = new()
{
nameof(IsOnline),
nameof(IsParent),
nameof(ParentDeviceId),
nameof(ExtraData),
nameof(IpAddress),
nameof(Port),
nameof(LastSyncTime)
};
}
}

View File

@@ -0,0 +1,28 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.Linq;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
public partial class gateway_nodes
{
/// <summary>
/// 适配器类型列表。从 AdapterTypes逗号分隔字符串解析。
/// 示例:"Owl:main,MC4:31ku" → ["Owl:main","MC4:31ku"]
/// </summary>
[SugarColumn(IsIgnore = true)]
public List<string> AdapterList
{
get => string.IsNullOrEmpty(AdapterTypes)
? new List<string>()
: AdapterTypes.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).ToList();
set => AdapterTypes = value != null ? string.Join(",", value) : "";
}
}
}

View File

@@ -0,0 +1,22 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
public partial class iot_alarm
{
//此处配置字段(字段配置见此model的另一个partial),如果表中没有此字段请加上[SugarColumn(IsIgnore = true)]属性,否则会异常
}
}

View File

@@ -0,0 +1,22 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
public partial class iot_devicedata
{
//此处配置字段(字段配置见此model的另一个partial),如果表中没有此字段请加上[SugarColumn(IsIgnore = true)]属性,否则会异常
}
}

View File

@@ -0,0 +1,22 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
public partial class video_channel
{
//此处配置字段(字段配置见此model的另一个partial),如果表中没有此字段请加上[SugarColumn(IsIgnore = true)]属性,否则会异常
}
}

View File

@@ -0,0 +1,22 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
public partial class video_record
{
//此处配置字段(字段配置见此model的另一个partial),如果表中没有此字段请加上[SugarColumn(IsIgnore = true)]属性,否则会异常
}
}

View File

@@ -0,0 +1,120 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
[Entity(TableCnName = "设备管理_视频通道",TableName = "video_channel",DetailTable = new Type[] { typeof(video_record)},DetailTableCnName = "录像记录",DBServer = "ServiceDbContext")]
public partial class video_channel:ServiceEntity
{
/// <summary>
///通道记录ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="通道记录ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int ChannelId { get; set; }
/// <summary>
///Owl系统通道ID
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="Owl系统通道ID")]
[MaxLength(64)]
[Column(TypeName="nvarchar(64)")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public string OwlChannelId { get; set; }
/// <summary>
///关联Base_Device设备ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="关联Base_Device设备ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int DeviceId { get; set; }
/// <summary>
///Owl流应用名
/// </summary>
[Display(Name ="Owl流应用名")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string OwlStreamApp { get; set; }
/// <summary>
///Owl流名称
/// </summary>
[Display(Name ="Owl流名称")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
public string OwlStreamName { get; set; }
/// <summary>
///是否支持云台
/// </summary>
[Display(Name ="是否支持云台")]
[Column(TypeName="tinyint")]
[Editable(true)]
public byte? HasPtz { get; set; }
/// <summary>
///是否支持录像
/// </summary>
[Display(Name ="是否支持录像")]
[Column(TypeName="tinyint")]
[Editable(true)]
public byte? HasRecording { get; set; }
/// <summary>
///录像模式
/// </summary>
[Display(Name ="录像模式")]
[Column(TypeName="int")]
[Editable(true)]
public int? RecordMode { get; set; }
/// <summary>
///快照地址
/// </summary>
[Display(Name ="快照地址")]
[MaxLength(500)]
[Column(TypeName="nvarchar(500)")]
[Editable(true)]
public string SnapshotUrl { get; set; }
/// <summary>
///创建时间
/// </summary>
[Display(Name ="创建时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? CreateDate { get; set; }
[Display(Name ="录像记录")]
[ForeignKey("ChannelId")][Navigate(NavigateType.OneToMany,nameof(ChannelId),nameof(ChannelId))]
public List<video_record> video_record { get; set; }
}
}

View File

@@ -0,0 +1,123 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此Model
*/
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SqlSugar;
using VolPro.Entity.SystemModels;
namespace VolPro.Entity.DomainModels
{
[Entity(TableCnName = "设备管理_录像记录",TableName = "video_record",DBServer = "ServiceDbContext")]
public partial class video_record:ServiceEntity
{
/// <summary>
///录像记录ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="录像记录ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int RecordId { get; set; }
/// <summary>
///关联通道ID
/// </summary>
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
[Key]
[Display(Name ="关联通道ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int ChannelId { get; set; }
/// <summary>
///Owl录像记录ID
/// </summary>
[Display(Name ="Owl录像记录ID")]
[Column(TypeName="int")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public int OwlRecordId { get; set; }
/// <summary>
///应用名
/// </summary>
[Display(Name ="应用名")]
[MaxLength(50)]
[Column(TypeName="nvarchar(50)")]
[Editable(true)]
public string App { get; set; }
/// <summary>
///流ID
/// </summary>
[Display(Name ="流ID")]
[MaxLength(100)]
[Column(TypeName="nvarchar(100)")]
[Editable(true)]
public string Stream { get; set; }
/// <summary>
///录像开始时间
/// </summary>
[SugarColumn(IsPrimaryKey = true)]
[Key]
[Display(Name ="录像开始时间")]
[Column(TypeName="datetime")]
[Editable(true)]
[Required(AllowEmptyStrings=false)]
public DateTime StartedAt { get; set; }
/// <summary>
///录像结束时间
/// </summary>
[Display(Name ="录像结束时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? EndedAt { get; set; }
/// <summary>
///录像时长(秒)
/// </summary>
[Display(Name ="录像时长(秒)")]
[Column(TypeName="double")]
[Editable(true)]
public decimal? Duration { get; set; }
/// <summary>
///文件路径
/// </summary>
[Display(Name ="文件路径")]
[MaxLength(500)]
[Column(TypeName="nvarchar(500)")]
[Editable(true)]
public string FilePath { get; set; }
/// <summary>
///文件大小(字节)
/// </summary>
[Display(Name ="文件大小(字节)")]
[Column(TypeName="bigint")]
[Editable(true)]
public long? FileSize { get; set; }
/// <summary>
///创建时间
/// </summary>
[Display(Name ="创建时间")]
[Column(TypeName="datetime")]
[Editable(true)]
public DateTime? CreateDate { get; set; }
}
}

View File

@@ -0,0 +1,127 @@
/*
*设备管理扩展 — 区域树 + 点位设备列表
*所有改动在 Partial 目录,不破坏框架可升级性
*/
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using VolPro.Entity.DomainModels;
using Warehouse.IServices;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace Warehouse.Controllers
{
public partial class base_deviceController
{
private readonly Ibase_deviceService _service;//访问业务代码
private readonly Iwarehouse_regionsService _regionsService;
private readonly Iwarehouse_devicepointService _pointService;
private readonly IHttpContextAccessor _httpContextAccessor;
[ActivatorUtilitiesConstructor]
public base_deviceController(
Ibase_deviceService service,
Iwarehouse_regionsService regionsService,
Iwarehouse_devicepointService pointService,
IHttpContextAccessor httpContextAccessor
)
: base(service)
{
_service = service;
_regionsService = regionsService;
_pointService = pointService;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 获取区域→点位→设备树。
/// 用于管理端左侧树形控件展示层级结构。
/// 格式: [{ id, label, type:"region", children: [{ id, label, type:"point", deviceCount }] }]
/// </summary>
[HttpGet]
[Route("/api/DeviceManager/GetRegionTree")]
public async Task<IActionResult> GetRegionTree()
{
// 查所有区域
var regions = await _regionsService.FindAsIQueryable(x => true).ToListAsync();
// 查所有点位
var points = await _pointService.FindAsIQueryable(x => true).ToListAsync();
// 统计每个点位下的设备数量
//var deviceCounts = new Dictionary<int, int>();
//var allDevices = await _service.FindAsIQueryable(x => true)
// .Where(x => x.PointId != null)
// .GroupBy(x => x.PointId!.Value)
// .Select(g => new { PointId = g.Key, Count = g.Count() })
// .ToListAsync();
var deviceCounts = new Dictionary<int, int>();
var devices = await _service.FindAsIQueryable(x => x.PointId != null)
.Select(x => new { x.PointId })
.ToListAsync();
deviceCounts = devices
.Where(x => x.PointId.HasValue)
.GroupBy(x => x.PointId!.Value)
.ToDictionary(g => g.Key, g => g.Count());
// 构建树形结构
var tree = new List<object>();
foreach (var region in regions)
{
var regionChildren = points
.Where(p => p.RegionId == region.Id)
.Select(p => new
{
id = $"p_{p.PointID}",
label = p.PointName ?? $"点位{p.PointID}",
type = "point",
deviceCount = deviceCounts.TryGetValue(p.PointID, out var c) ? c : 0
})
.ToList<object>();
tree.Add(new
{
id = $"r_{region.Id}",
label = region.RegionName ?? $"区域{region.Id}",
type = "region",
deviceCount = regionChildren.Count,
children = regionChildren
});
}
return Ok(tree);
}
/// <summary>
/// 获取指定点位下的设备列表(含子设备)。
/// 支持分页参数 page 和 size。
/// </summary>
[HttpGet]
[Route("/api/DeviceManager/GetDevicesByPoint")]
public async Task<IActionResult> GetDevicesByPoint(int pointId, int page = 1, int size = 20)
{
var query = _service.FindAsIQueryable(x => x.PointId == pointId);
var total = await query.CountAsync();
var items = await query
.Skip((page - 1) * size)
.Take(size)
.OrderBy(x => x.DeviceId)
.Select(x => new
{
x.DeviceId, x.DeviceName, x.AdapterCode, x.SourceId,
x.DeviceCategory, x.DeviceGroup, x.IsParent,
x.ParentDeviceId, x.IsOnline, x.IpAddress, x.Port,
x.Location, x.ExtraData, x.LastSyncTime,
x.MapModelId, x.MapModelScale, x.MapModelRotation, x.Enable
})
.ToListAsync();
return Ok(new { items, total });
}
}
}

View File

@@ -0,0 +1,246 @@
/*
*网关节点管理 — A1注册/A2心跳/A3设备同步/A4告警同步
*A组接口使用 [AllowAnonymous] + NodeToken 二次认证
*所有改动在 Partial 目录,不破坏框架可升级性
*/
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using VolPro.Core.DBManager;
using VolPro.Core.DbSqlSugar;
using VolPro.Entity.DomainModels;
using Warehouse.IRepositories;
using Warehouse.IServices;
using Warehouse.Services;
namespace Warehouse.Controllers
{
public partial class gateway_nodesController
{
private readonly Igateway_nodesService _service;//访问业务代码
private readonly Ibase_deviceService _deviceService;
private readonly Iiot_alarmService _iot_alarmService;
private readonly IHttpContextAccessor _httpContextAccessor;
[ActivatorUtilitiesConstructor]
public gateway_nodesController(
Igateway_nodesService service,
Ibase_deviceService deviceService,
Iiot_alarmService iot_alarmService,
IHttpContextAccessor httpContextAccessor
)
: base(service)
{
_service = service;
_deviceService = deviceService;
_iot_alarmService = iot_alarmService;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>A1: 网关注册Upsert。认证方式: NodeToken</summary>
[HttpPost]
[Route("/api/gateway/register")]
[AllowAnonymous]
public async Task<IActionResult> RegisterGateway([FromBody] GatewayRegisterRequest req)
{
if (string.IsNullOrEmpty(req.NodeCode) || string.IsNullOrEmpty(req.Token))
return BadRequest(new { message = "NodeCode 和 Token 为必填项" });
try
{
var node = await _service.RegisterNodeAsync(req.NodeCode, req.Token, req.AdapterTypes, req.BaseUrl);
// 返回当前网关的顶层设备列表
var devices = await _deviceService.GetDevicesByGatewayNodeAsync(node.NodeId);
return Ok(new { nodeId = node.NodeId, devices = devices.Select(d => new {
d.DeviceId, d.DeviceName, d.AdapterCode, d.SourceId,
d.DeviceCategory, d.DeviceGroup, d.IsParent, d.IsOnline, d.ExtraData
}) });
}
catch (UnauthorizedAccessException)
{
return StatusCode(401, new { message = "认证失败Token 无效" });
}
}
/// <summary>A2: 心跳。认证方式: NodeToken。每15秒调用一次。</summary>
[HttpPost]
[Route("/api/gateway/heartbeat")]
[AllowAnonymous]
public async Task<IActionResult> GatewayHeartbeat([FromBody] GatewayHeartbeatRequest req)
{
if (string.IsNullOrEmpty(req.NodeCode) || string.IsNullOrEmpty(req.Token))
return BadRequest(new { message = "NodeCode 和 Token 为必填项" });
try
{
await _service.UpdateHeartbeatAsync(req.NodeCode, req.Token);
return Ok(new { status = "ok", serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") });
}
catch (UnauthorizedAccessException)
{
return StatusCode(401, new { message = "认证失败" });
}
}
/// <summary>A3: 设备数据同步(字段分治 + parentSourceId 映射)。认证方式: NodeToken</summary>
[HttpPost]
[Route("/api/gateway/sync/devices")]
[AllowAnonymous]
public async Task<IActionResult> SyncDevices([FromBody] SyncDevicesRequest req)
{
if (string.IsNullOrEmpty(req.NodeCode) || string.IsNullOrEmpty(req.Token))
return BadRequest(new { message = "NodeCode 和 Token 为必填项" });
try
{
// 认证
var node = await _service.FindAsIQueryable(x => x.NodeCode == req.NodeCode && x.NodeToken == req.Token)
.FirstOrDefaultAsync();
if (node == null) return StatusCode(401, new { message = "认证失败" });
var items = req.Devices.Select(d => new SyncDeviceItem
{
AdapterCode = d.AdapterCode,
SourceId = d.SourceId,
Name = d.Name,
Category = d.Category,
Group = d.Group,
IsParent = d.IsParent,
ParentSourceId = d.ParentSourceId,
IsOnline = d.IsOnline,
IpAddress = d.IpAddress,
Port = d.Port,
ExtraDataJson = d.ExtraDataJson
}).ToList();
var (added, updated) = await _service.SyncDevicesAsync(node.NodeId, items);
return Ok(new { added, updated, removed = 0 });
}
catch (UnauthorizedAccessException)
{
return StatusCode(401, new { message = "认证失败" });
}
}
/// <summary>A4: 告警同步DeviceSourceId→DeviceId 映射 + SourceAlarmId 去重)。认证方式: NodeToken</summary>
[HttpPost]
[Route("/api/gateway/sync/alarms")]
[AllowAnonymous]
public async Task<IActionResult> SyncAlarms([FromBody] SyncAlarmsRequest req)
{
if (string.IsNullOrEmpty(req.NodeCode) || string.IsNullOrEmpty(req.Token))
return BadRequest(new { message = "NodeCode 和 Token 为必填项" });
try
{
var node = await _service.FindAsIQueryable(x => x.NodeCode == req.NodeCode && x.NodeToken == req.Token)
.FirstOrDefaultAsync();
if (node == null) return StatusCode(401, new { message = "认证失败" });
// 获取告警服务
var alarmSvc = _iot_alarmService;
// 批量查询 DeviceSourceId → DeviceId 映射
var deviceSvc = _deviceService;
int added = 0;
foreach (var a in req.Alarms)
{
int? deviceId = null;
if (deviceSvc != null)
{
var dev = await deviceSvc.FindAsIQueryable(
x => x.AdapterCode == a.AdapterCode && x.SourceId == a.DeviceSourceId)
.Select(x => new { x.DeviceId })
.FirstOrDefaultAsync();
deviceId = dev?.DeviceId;
}
if (alarmSvc != null)
{
var alarmItem = new SyncAlarmItem
{
SourceAlarmId = a.SourceAlarmId,
DeviceSourceId = a.DeviceSourceId,
AdapterCode = a.AdapterCode,
Level = a.Level,
Desc = a.Desc,
Value = a.Value,
StartTime = a.StartTime
};
await alarmSvc.UpsertAlarmAsync(alarmItem, deviceId);
added++;
}
}
return Ok(new { added });
}
catch (UnauthorizedAccessException)
{
return StatusCode(401, new { message = "认证失败" });
}
}
}
// ── A 组请求 DTO ──
public class GatewayRegisterRequest
{
public string NodeCode { get; set; } = "";
public string Token { get; set; } = "";
public string AdapterTypes { get; set; } = "";
public string BaseUrl { get; set; } = "";
}
public class GatewayHeartbeatRequest
{
public string NodeCode { get; set; } = "";
public string Token { get; set; } = "";
}
public class SyncDevicesRequest
{
public string NodeCode { get; set; } = "";
public string Token { get; set; } = "";
public List<SyncDeviceItemDto> Devices { get; set; } = new();
}
public class SyncDeviceItemDto
{
public string AdapterCode { get; set; } = "";
public string SourceId { get; set; } = "";
public string? Name { get; set; }
public string? Category { get; set; }
public string? Group { get; set; }
public bool IsParent { get; set; }
public string? ParentSourceId { get; set; }
public bool IsOnline { get; set; }
public string? IpAddress { get; set; }
public int? Port { get; set; }
public string? ExtraDataJson { get; set; }
}
public class SyncAlarmsRequest
{
public string NodeCode { get; set; } = "";
public string Token { get; set; } = "";
public List<SyncAlarmItemDto> Alarms { get; set; } = new();
}
public class SyncAlarmItemDto
{
public string SourceAlarmId { get; set; } = "";
public string DeviceSourceId { get; set; } = "";
public string AdapterCode { get; set; } = "";
public string Level { get; set; } = "";
public string Desc { get; set; } = "";
public double? Value { get; set; }
public string StartTime { get; set; } = "";
}
}

View File

@@ -0,0 +1,33 @@
/*
*接口编写处...
*如果接口需要做Action的权限验证请在Action上使用属性
*如: [ApiActionPermission("iot_alarm",Enums.ActionPermissionOptions.Search)]
*/
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using VolPro.Entity.DomainModels;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
public partial class iot_alarmController
{
private readonly Iiot_alarmService _service;//访问业务代码
private readonly IHttpContextAccessor _httpContextAccessor;
[ActivatorUtilitiesConstructor]
public iot_alarmController(
Iiot_alarmService service,
IHttpContextAccessor httpContextAccessor
)
: base(service)
{
_service = service;
_httpContextAccessor = httpContextAccessor;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
*接口编写处...
*如果接口需要做Action的权限验证请在Action上使用属性
*如: [ApiActionPermission("iot_devicedata",Enums.ActionPermissionOptions.Search)]
*/
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using VolPro.Entity.DomainModels;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
public partial class iot_devicedataController
{
private readonly Iiot_devicedataService _service;//访问业务代码
private readonly IHttpContextAccessor _httpContextAccessor;
[ActivatorUtilitiesConstructor]
public iot_devicedataController(
Iiot_devicedataService service,
IHttpContextAccessor httpContextAccessor
)
: base(service)
{
_service = service;
_httpContextAccessor = httpContextAccessor;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
*接口编写处...
*如果接口需要做Action的权限验证请在Action上使用属性
*如: [ApiActionPermission("video_channel",Enums.ActionPermissionOptions.Search)]
*/
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using VolPro.Entity.DomainModels;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
public partial class video_channelController
{
private readonly Ivideo_channelService _service;//访问业务代码
private readonly IHttpContextAccessor _httpContextAccessor;
[ActivatorUtilitiesConstructor]
public video_channelController(
Ivideo_channelService service,
IHttpContextAccessor httpContextAccessor
)
: base(service)
{
_service = service;
_httpContextAccessor = httpContextAccessor;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
*接口编写处...
*如果接口需要做Action的权限验证请在Action上使用属性
*如: [ApiActionPermission("video_record",Enums.ActionPermissionOptions.Search)]
*/
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using VolPro.Entity.DomainModels;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
public partial class video_recordController
{
private readonly Ivideo_recordService _service;//访问业务代码
private readonly IHttpContextAccessor _httpContextAccessor;
[ActivatorUtilitiesConstructor]
public video_recordController(
Ivideo_recordService service,
IHttpContextAccessor httpContextAccessor
)
: base(service)
{
_service = service;
_httpContextAccessor = httpContextAccessor;
}
}
}

View File

@@ -0,0 +1,21 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下Partial文件夹base_deviceController编写
*/
using Microsoft.AspNetCore.Mvc;
using VolPro.Core.Controllers.Basic;
using VolPro.Entity.AttributeManager;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
[Route("api/base_device")]
[PermissionTable(Name = "base_device")]
public partial class base_deviceController : ApiBaseController<Ibase_deviceService>
{
public base_deviceController(Ibase_deviceService service)
: base(service)
{
}
}
}

View File

@@ -0,0 +1,21 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下Partial文件夹gateway_nodesController编写
*/
using Microsoft.AspNetCore.Mvc;
using VolPro.Core.Controllers.Basic;
using VolPro.Entity.AttributeManager;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
[Route("api/gateway_nodes")]
[PermissionTable(Name = "gateway_nodes")]
public partial class gateway_nodesController : ApiBaseController<Igateway_nodesService>
{
public gateway_nodesController(Igateway_nodesService service)
: base(service)
{
}
}
}

View File

@@ -0,0 +1,21 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下Partial文件夹iot_alarmController编写
*/
using Microsoft.AspNetCore.Mvc;
using VolPro.Core.Controllers.Basic;
using VolPro.Entity.AttributeManager;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
[Route("api/iot_alarm")]
[PermissionTable(Name = "iot_alarm")]
public partial class iot_alarmController : ApiBaseController<Iiot_alarmService>
{
public iot_alarmController(Iiot_alarmService service)
: base(service)
{
}
}
}

View File

@@ -0,0 +1,21 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下Partial文件夹iot_devicedataController编写
*/
using Microsoft.AspNetCore.Mvc;
using VolPro.Core.Controllers.Basic;
using VolPro.Entity.AttributeManager;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
[Route("api/iot_devicedata")]
[PermissionTable(Name = "iot_devicedata")]
public partial class iot_devicedataController : ApiBaseController<Iiot_devicedataService>
{
public iot_devicedataController(Iiot_devicedataService service)
: base(service)
{
}
}
}

View File

@@ -0,0 +1,21 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下Partial文件夹video_channelController编写
*/
using Microsoft.AspNetCore.Mvc;
using VolPro.Core.Controllers.Basic;
using VolPro.Entity.AttributeManager;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
[Route("api/video_channel")]
[PermissionTable(Name = "video_channel")]
public partial class video_channelController : ApiBaseController<Ivideo_channelService>
{
public video_channelController(Ivideo_channelService service)
: base(service)
{
}
}
}

View File

@@ -0,0 +1,21 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下Partial文件夹video_recordController编写
*/
using Microsoft.AspNetCore.Mvc;
using VolPro.Core.Controllers.Basic;
using VolPro.Entity.AttributeManager;
using Warehouse.IServices;
namespace Warehouse.Controllers
{
[Route("api/video_record")]
[PermissionTable(Name = "video_record")]
public partial class video_recordController : ApiBaseController<Ivideo_recordService>
{
public video_recordController(Ivideo_recordService service)
: base(service)
{
}
}
}

View File

@@ -0,0 +1,7 @@
中文提示 : 检测到你没有开启文件AllowLoadLocalInfile=true加到自符串上已自动执行 SET GLOBAL local_infile=1 在试一次
English Message : Loading local data is disabled; this must be enabled on both the client and server sides at SqlSugar.Check.ExceptionEasy(String enMessage, String cnMessage)
at SqlSugar.MySqlFastBuilder.ExecuteBulkCopyAsync(DataTable dt)
at SqlSugar.FastestProvider`1._BulkCopy(List`1 datas)
at SqlSugar.FastestProvider`1.BulkCopyAsync(List`1 datas)
at SqlSugar.FastestProvider`1.BulkCopy(List`1 datas)
at VolPro.Core.Services.Logger.Start() in D:\Code\SecMPS\api_sqlsugar\VolPro.Core\Services\Logger.cs:line 194SqlSugar

View File

@@ -4,13 +4,18 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
--> -->
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<DeleteExistingFiles>False</DeleteExistingFiles> <DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>False</ExcludeApp_Data> <ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish> <LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration> <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform> <LastUsedPlatform>Any CPU</LastUsedPlatform>
<PublishProvider>FileSystem</PublishProvider> <PublishProvider>FileSystem</PublishProvider>
<PublishUrl>bin\Release\netcoreapp3.1\net6.0\publish\</PublishUrl> <PublishUrl>bin\Release\netcoreapp3.1\net6.0\publish\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod> <WebPublishMethod>FileSystem</WebPublishMethod>
<SiteUrlToLaunchAfterPublish />
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
<ProjectGuid>4db3c91b-93fe-4937-8b58-ddd3f57d4607</ProjectGuid>
<SelfContained>true</SelfContained>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -69,6 +69,14 @@ namespace VolPro.WebApi
services.AddSession(); services.AddSession();
services.AddMemoryCache(); services.AddMemoryCache();
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
// ── 网关集成: HttpClient + GatewayClient 注册 ──
services.AddHttpClient("VolPro", c =>
{
c.Timeout = TimeSpan.FromSeconds(30);
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
services.AddSingleton<VolPro.Warehouse.Services.GatewayClient>();
services.AddMvc(options => services.AddMvc(options =>
{ {
options.Filters.Add(typeof(ApiAuthorizeFilter)); options.Filters.Add(typeof(ApiAuthorizeFilter));

View File

@@ -59,5 +59,11 @@
<None Include="Startup copy.cs" /> <None Include="Startup copy.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="fonts\DejaVuSans.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

Binary file not shown.

View File

@@ -0,0 +1,18 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹Ibase_deviceRepository编写接口
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Extensions.AutofacManager;
namespace Warehouse.IRepositories
{
public partial interface Ibase_deviceRepository : IDependency,IRepository<base_device>
{
}
}

View File

@@ -0,0 +1,18 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹Igateway_nodesRepository编写接口
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Extensions.AutofacManager;
namespace Warehouse.IRepositories
{
public partial interface Igateway_nodesRepository : IDependency,IRepository<gateway_nodes>
{
}
}

View File

@@ -0,0 +1,18 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹Iiot_alarmRepository编写接口
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Extensions.AutofacManager;
namespace Warehouse.IRepositories
{
public partial interface Iiot_alarmRepository : IDependency,IRepository<iot_alarm>
{
}
}

View File

@@ -0,0 +1,18 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹Iiot_devicedataRepository编写接口
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Extensions.AutofacManager;
namespace Warehouse.IRepositories
{
public partial interface Iiot_devicedataRepository : IDependency,IRepository<iot_devicedata>
{
}
}

View File

@@ -0,0 +1,18 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹Ivideo_channelRepository编写接口
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Extensions.AutofacManager;
namespace Warehouse.IRepositories
{
public partial interface Ivideo_channelRepository : IDependency,IRepository<video_channel>
{
}
}

View File

@@ -0,0 +1,18 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹Ivideo_recordRepository编写接口
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Extensions.AutofacManager;
namespace Warehouse.IRepositories
{
public partial interface Ivideo_recordRepository : IDependency,IRepository<video_record>
{
}
}

View File

@@ -0,0 +1,12 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
namespace Warehouse.IServices
{
public partial interface Ibase_deviceService : IService<base_device>
{
}
}

View File

@@ -0,0 +1,12 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
namespace Warehouse.IServices
{
public partial interface Igateway_nodesService : IService<gateway_nodes>
{
}
}

View File

@@ -0,0 +1,12 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
namespace Warehouse.IServices
{
public partial interface Iiot_alarmService : IService<iot_alarm>
{
}
}

View File

@@ -0,0 +1,12 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
namespace Warehouse.IServices
{
public partial interface Iiot_devicedataService : IService<iot_devicedata>
{
}
}

View File

@@ -0,0 +1,12 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
namespace Warehouse.IServices
{
public partial interface Ivideo_channelService : IService<video_channel>
{
}
}

View File

@@ -0,0 +1,12 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
namespace Warehouse.IServices
{
public partial interface Ivideo_recordService : IService<video_record>
{
}
}

View File

@@ -0,0 +1,19 @@
/*
*所有关于base_device类的业务代码接口应在此处编写
*/
using System.Linq.Expressions;
using VolPro.Core.BaseProvider;
using VolPro.Core.Utilities;
using VolPro.Entity.DomainModels;
using Warehouse.Services;
namespace Warehouse.IServices
{
public partial interface Ibase_deviceService
{
Task<List<base_device>> GetDevicesByGatewayNodeAsync(int gatewayNodeId);
Task UpsertDeviceAsync(SyncDeviceItem d, int gatewayNodeId, Dictionary<(string, string), int> existingIds);
}
}

View File

@@ -0,0 +1,19 @@
/*
*所有关于gateway_nodes类的业务代码接口应在此处编写
*/
using System.Linq.Expressions;
using VolPro.Core.BaseProvider;
using VolPro.Core.Utilities;
using VolPro.Entity.DomainModels;
using Warehouse.Services;
namespace Warehouse.IServices
{
public partial interface Igateway_nodesService
{
Task<gateway_nodes> RegisterNodeAsync(string nodeCode, string token, string adapterTypes, string baseUrl);
Task UpdateHeartbeatAsync(string nodeCode, string token);
Task<(int added, int updated)> SyncDevicesAsync(int gatewayNodeId, List<SyncDeviceItem> devices);
}
}

View File

@@ -0,0 +1,17 @@
/*
*所有关于iot_alarm类的业务代码接口应在此处编写
*/
using System.Linq.Expressions;
using VolPro.Core.BaseProvider;
using VolPro.Core.Utilities;
using VolPro.Entity.DomainModels;
using Warehouse.Services;
namespace Warehouse.IServices
{
public partial interface Iiot_alarmService
{
Task UpsertAlarmAsync(SyncAlarmItem a, int? deviceId);
}
}

View File

@@ -0,0 +1,13 @@
/*
*所有关于iot_devicedata类的业务代码接口应在此处编写
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
namespace Warehouse.IServices
{
public partial interface Iiot_devicedataService
{
}
}

View File

@@ -0,0 +1,13 @@
/*
*所有关于video_channel类的业务代码接口应在此处编写
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
namespace Warehouse.IServices
{
public partial interface Ivideo_channelService
{
}
}

View File

@@ -0,0 +1,13 @@
/*
*所有关于video_record类的业务代码接口应在此处编写
*/
using VolPro.Core.BaseProvider;
using VolPro.Entity.DomainModels;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
namespace Warehouse.IServices
{
public partial interface Ivideo_recordService
{
}
}

View File

@@ -0,0 +1,24 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹base_deviceRepository编写代码
*/
using Warehouse.IRepositories;
using VolPro.Core.BaseProvider;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Repositories
{
public partial class base_deviceRepository : RepositoryBase<base_device> , Ibase_deviceRepository
{
public base_deviceRepository(ServiceDbContext dbContext)
: base(dbContext)
{
}
public static Ibase_deviceRepository Instance
{
get { return AutofacContainerModule.GetService<Ibase_deviceRepository>(); } }
}
}

View File

@@ -0,0 +1,24 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹gateway_nodesRepository编写代码
*/
using Warehouse.IRepositories;
using VolPro.Core.BaseProvider;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Repositories
{
public partial class gateway_nodesRepository : RepositoryBase<gateway_nodes> , Igateway_nodesRepository
{
public gateway_nodesRepository(ServiceDbContext dbContext)
: base(dbContext)
{
}
public static Igateway_nodesRepository Instance
{
get { return AutofacContainerModule.GetService<Igateway_nodesRepository>(); } }
}
}

View File

@@ -0,0 +1,24 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹iot_alarmRepository编写代码
*/
using Warehouse.IRepositories;
using VolPro.Core.BaseProvider;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Repositories
{
public partial class iot_alarmRepository : RepositoryBase<iot_alarm> , Iiot_alarmRepository
{
public iot_alarmRepository(ServiceDbContext dbContext)
: base(dbContext)
{
}
public static Iiot_alarmRepository Instance
{
get { return AutofacContainerModule.GetService<Iiot_alarmRepository>(); } }
}
}

View File

@@ -0,0 +1,24 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹iot_devicedataRepository编写代码
*/
using Warehouse.IRepositories;
using VolPro.Core.BaseProvider;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Repositories
{
public partial class iot_devicedataRepository : RepositoryBase<iot_devicedata> , Iiot_devicedataRepository
{
public iot_devicedataRepository(ServiceDbContext dbContext)
: base(dbContext)
{
}
public static Iiot_devicedataRepository Instance
{
get { return AutofacContainerModule.GetService<Iiot_devicedataRepository>(); } }
}
}

View File

@@ -0,0 +1,24 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹video_channelRepository编写代码
*/
using Warehouse.IRepositories;
using VolPro.Core.BaseProvider;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Repositories
{
public partial class video_channelRepository : RepositoryBase<video_channel> , Ivideo_channelRepository
{
public video_channelRepository(ServiceDbContext dbContext)
: base(dbContext)
{
}
public static Ivideo_channelRepository Instance
{
get { return AutofacContainerModule.GetService<Ivideo_channelRepository>(); } }
}
}

View File

@@ -0,0 +1,24 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*Repository提供数据库操作如果要增加数据库操作请在当前目录下Partial文件夹video_recordRepository编写代码
*/
using Warehouse.IRepositories;
using VolPro.Core.BaseProvider;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Repositories
{
public partial class video_recordRepository : RepositoryBase<video_record> , Ivideo_recordRepository
{
public video_recordRepository(ServiceDbContext dbContext)
: base(dbContext)
{
}
public static Ivideo_recordRepository Instance
{
get { return AutofacContainerModule.GetService<Ivideo_recordRepository>(); } }
}
}

View File

@@ -0,0 +1,73 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace VolPro.Warehouse.Services;
/// <summary>
/// 网关 HTTP 客户端。封装 Vol.Pro 调用 IntegrationGateway B 组接口的逻辑。
/// 所有对网关的请求统一经此类发出,便于连接池管理和错误处理。
/// </summary>
public class GatewayClient
{
private readonly IHttpClientFactory _httpFactory;
private readonly IConfiguration _config;
public GatewayClient(IHttpClientFactory httpFactory, IConfiguration config)
{
_httpFactory = httpFactory;
_config = config;
}
/// <summary>创建带超时和默认头的 HttpClient</summary>
private HttpClient CreateClient()
{
var client = _httpFactory.CreateClient("VolPro");
client.Timeout = TimeSpan.FromSeconds(30);
return client;
}
/// <summary>
/// B3: 手动触发网关全量设备同步。
/// POST {baseUrl}/api/gateway/devices/sync?adapter={adapterTypes}
/// </summary>
public async Task<JsonDocument?> TriggerFullSyncAsync(string baseUrl, string adapterTypes)
{
var http = CreateClient();
var resp = await http.PostAsync(
$"{baseUrl.TrimEnd('/')}/api/gateway/devices/sync?adapter={Uri.EscapeDataString(adapterTypes)}",
null);
if (!resp.IsSuccessStatusCode) return null;
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
}
/// <summary>
/// B4: 获取设备实时点位值。
/// GET {baseUrl}/api/gateway/realtime/{adapter}/{deviceId}
/// </summary>
public async Task<JsonDocument?> GetRealtimeAsync(string baseUrl, string adapter, string deviceId)
{
var http = CreateClient();
var resp = await http.GetAsync(
$"{baseUrl.TrimEnd('/')}/api/gateway/realtime/{Uri.EscapeDataString(adapter)}/{Uri.EscapeDataString(deviceId)}");
if (!resp.IsSuccessStatusCode) return null;
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
}
/// <summary>
/// B5: 设备反向控制。
/// POST {baseUrl}/api/gateway/realtime/{adapter}/control
/// </summary>
public async Task<bool> ControlDeviceAsync(string baseUrl, string adapter, string deviceId, int pointIndex, double value)
{
var http = CreateClient();
var resp = await http.PostAsJsonAsync(
$"{baseUrl.TrimEnd('/')}/api/gateway/realtime/{adapter}/control",
new { deviceId, pointIndex, value });
return resp.IsSuccessStatusCode;
}
}

View File

@@ -0,0 +1,74 @@
using Quartz;
using Microsoft.Extensions.DependencyInjection;
using Warehouse.IServices;
using VolPro.Entity.DomainModels;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Warehouse.IRepositories;
namespace VolPro.Warehouse.Services;
/// <summary>
/// 心跳超时检测任务。扫描心跳超时 30 秒的网关节点,标记为离线,
/// 并级联标记该节点下所有设备为离线。
/// Cron 建议: 每 15 秒 ("0/15 * * * * ?")
///
/// 设备与网关的关联通过 AdapterCode 前缀匹配(如设备 AdapterCode="MC4:31ku" 匹配网关 AdapterTypes="MC4:31ku")。
/// </summary>
public class HeartbeatMonitorJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
if (sp == null) return;
var gwSvc = sp.GetService<Igateway_nodesService>();
var gwRepo = sp.GetService<Igateway_nodesRepository>();
var devRepo = sp.GetService<Ibase_deviceRepository>();
if (gwSvc == null || gwRepo == null || devRepo == null) return;
var timeout = DateTime.Now.AddSeconds(-30);
// 扫描心跳超时的网关(当前在线但心跳超时)
var offlineNodes = await gwSvc.FindAsIQueryable(
x => x.IsOnline == "在线" && x.LastHeartbeat < timeout).ToListAsync();
foreach (var node in offlineNodes)
{
// 标记网关离线
node.IsOnline = "离线";
try { gwRepo.Update(node); } catch { }
Console.WriteLine($"[HeartbeatMonitorJob] 网关 {node.NodeCode} 心跳超时,标记离线");
// 级联标记该网关下所有设备离线(批量 SQL
try
{
var adapterPrefixes = (node.AdapterTypes ?? "")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.Trim()).ToList();
if (adapterPrefixes.Any())
{
var allDevices = await devRepo.FindAsIQueryable(
d => d.IsOnline == "在线").ToListAsync();
var matched = allDevices
.Where(d => adapterPrefixes.Any(p => (d.AdapterCode ?? "").StartsWith(p)))
.ToList();
if (matched.Any())
{
foreach (var dev in matched) dev.IsOnline = "离线";
devRepo.UpdateRange(matched);
Console.WriteLine($"[HeartbeatMonitorJob] 级联 {matched.Count} 台设备离线");
}
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[HeartbeatMonitorJob] 级联离线失败: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,87 @@
using Quartz;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
using VolPro.Entity.DomainModels;
using Warehouse.IRepositories;
using Warehouse.IServices;
namespace VolPro.Warehouse.Services;
/// <summary>
/// 实时数据轮询任务。
/// 定时轮询在线 MC4 IoT 设备的实时值 → 写入 iot_devicedata 表。
/// Cron 建议: 每 10 秒 ("0/10 * * * * ?")
///
/// 设备与网关的关联通过 AdapterCode 前缀匹配(如设备 AdapterCode="MC4:31ku" 匹配网关 AdapterTypes="MC4:31ku")。
/// </summary>
public class RealtimePollJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
if (sp == null) return;
var gwSvc = sp.GetService<Igateway_nodesService>();
var devRepo = sp.GetService<Ibase_deviceRepository>();
var dataRepo = sp.GetService<Iiot_devicedataRepository>();
var gatewayClient = sp.GetService<GatewayClient>();
if (gwSvc == null || devRepo == null || dataRepo == null || gatewayClient == null) return;
// 1. 查在线 MC4 网关
var onlineNodes = await gwSvc.FindAsIQueryable(x =>
x.IsOnline == "在线" && x.AdapterTypes != null && x.AdapterTypes.Contains("MC4")).ToArrayAsync();
foreach (var node in onlineNodes)
{
try
{
var baseUrl = node.BaseUrl;
if (string.IsNullOrEmpty(baseUrl)) continue;
// 2. 解析网关管理的适配器前缀列表
var adapterPrefixes = (node.AdapterTypes ?? "")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.Trim());
// 3. 查该网关下在线的 IoT 设备AdapterCode 前缀匹配)
var devices = await devRepo.FindAsIQueryable(d =>
d.DeviceGroup == "IoT设备" && d.IsOnline == "在线").ToListAsync();
var matchedDevices = devices.Where(d =>
adapterPrefixes.Any(p => (d.AdapterCode ?? "").StartsWith(p))).ToList();
if (!matchedDevices.Any()) continue;
// 4. 逐设备调网关 B4 获取实时值
foreach (var dev in matchedDevices)
{
try
{
var result = await gatewayClient.GetRealtimeAsync(baseUrl, dev.AdapterCode, dev.SourceId);
if (result == null) continue;
var root = result.RootElement;
var points = root.TryGetProperty("pointValues", out var pv) ? pv
: root.TryGetProperty("rows", out var r) ? r
: root;
// 结果可能是 PointValue[] 数组,取第一个点位写入
if (points.ValueKind == System.Text.Json.JsonValueKind.Array && points.GetArrayLength() > 0)
{
var first = points[0];
var entry = new iot_devicedata
{
DeviceId = dev.DeviceId,
PointValue = first.TryGetProperty("value", out var v) ? v.GetDecimal() : (decimal?)null,
UpdateTime = DateTime.Now,
Interval = first.TryGetProperty("interval", out var iv) ? iv.GetInt32() : 10
};
dataRepo.Add(entry);
}
}
catch { /* 单设备失败不阻塞其他设备 */ }
}
}
catch { /* 单网关失败不阻塞其他网关 */ }
}
}
}

View File

@@ -0,0 +1,43 @@
using Quartz;
using Microsoft.Extensions.DependencyInjection;
using Warehouse.IServices;
using VolPro.Entity.DomainModels;
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace VolPro.Warehouse.Services;
/// <summary>
/// 定时设备同步任务。遍历所有在线且启用的网关节点,触发全量设备同步。
/// Cron 建议: 每 5 分钟 ("0 */5 * * * ?")
/// </summary>
public class SyncDevicesJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
var gwSvc = sp.GetService<Igateway_nodesService>();
var client = sp.GetService<GatewayClient>();
if (gwSvc == null || client == null) return;
// 遍历所有在线且启用的网关
var onlineNodes = await gwSvc.FindAsIQueryable(
x => x.IsOnline == "在线" && x.Enable == "启用" && x.BaseUrl != null)
.ToListAsync();
foreach (var node in onlineNodes)
{
try
{
// 触发网关全量同步
await client.TriggerFullSyncAsync(node.BaseUrl!, node.AdapterTypes ?? "");
Console.WriteLine($"[SyncDevicesJob] 网关 {node.NodeCode} 同步触发成功");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[SyncDevicesJob] 网关 {node.NodeCode} 同步失败: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,116 @@
/*
*所有关于base_device类的业务代码应在此处编写
*可使用repository.调用常用方法获取EF/Dapper等信息
*如果需要事务请使用repository.DbContextBeginTransaction
*也可使用DBServerProvider.手动获取数据库相关信息
*用户信息、权限、角色等使用UserContext.Current操作
*base_deviceService对增、删、改查、导入、导出、审核业务代码扩展参照ServiceFunFilter
*/
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
using System.Linq;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
using VolPro.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Warehouse.IRepositories;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Warehouse.Services
{
public partial class base_deviceService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Ibase_deviceRepository _repository;//访问数据库
[ActivatorUtilitiesConstructor]
public base_deviceService(
Ibase_deviceRepository dbRepository,
IHttpContextAccessor httpContextAccessor
)
: base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
}
/// <summary>
/// 获取指定网关节点下的顶层设备列表(用于网关注册时返回所管设备)。
/// 返回 ParentDeviceId 为 null 的设备。
/// </summary>
public async Task<List<base_device>> GetDevicesByGatewayNodeAsync(int gatewayNodeId)
{
return await _repository.DbContext.Queryable<base_device>()
.Where(x => x.NodeId == gatewayNodeId && x.ParentDeviceId == null)
.ToListAsync();
}
/// <summary>
/// 按字段分治原则 Upsert 单个设备。
/// 首次入库时写全量管理员字段已有记录时仅更新网关字段IsOnline/ExtraData等
/// </summary>
/// <param name="d">同步设备条目</param>
/// <param name="gatewayNodeId">网关节点ID</param>
/// <param name="existingIds">已有设备映射表 (AdapterCode, SourceId) → DeviceId</param>
public async Task UpsertDeviceAsync(SyncDeviceItem d, int gatewayNodeId, Dictionary<(string, string), int> existingIds)
{
var db = _repository.DbContext;
var key = (d.AdapterCode, d.SourceId);
existingIds.TryGetValue(key, out var existingId);
bool isNew = existingId == 0;
// 解析父设备
int? parentDeviceId = null;
if (!string.IsNullOrEmpty(d.ParentSourceId))
{
existingIds.TryGetValue((d.AdapterCode, d.ParentSourceId), out var pid);
if (pid > 0) parentDeviceId = pid;
}
if (isNew)
{
var entity = new base_device
{
DeviceName = d.Name ?? $"DEV_{d.SourceId}",
AdapterCode = d.AdapterCode,
SourceId = d.SourceId,
DeviceCategory = d.Category,
DeviceGroup = d.Group,
NodeId = gatewayNodeId,
IsParent = d.IsParent ? "是" : "否",
ParentDeviceId = parentDeviceId,
IsOnline = d.IsOnline ? "在线" : "离线",
IpAddress = d.IpAddress,
Port = d.Port,
ExtraData = d.ExtraDataJson,
Enable = "启用",
LastSyncTime = DateTime.Now,
CreateDate = DateTime.Now
};
db.Insertable(entity).ExecuteCommand();
}
else
{
var entity = db.Queryable<base_device>().InSingle(existingId);
if (entity != null)
{
entity.IsOnline = d.IsOnline ? "在线" : "离线";
entity.IsParent = d.IsParent ? "是" : "否";
entity.ParentDeviceId = parentDeviceId ?? entity.ParentDeviceId;
entity.IpAddress = d.IpAddress;
entity.Port = d.Port;
entity.ExtraData = d.ExtraDataJson ?? entity.ExtraData;
entity.LastSyncTime = DateTime.Now;
db.Updateable(entity).ExecuteCommand();
}
}
}
}
}

View File

@@ -0,0 +1,196 @@
/*
*所有关于gateway_nodes类的业务代码应在此处编写
*可使用repository.调用常用方法获取EF/Dapper等信息
*如果需要事务请使用repository.DbContextBeginTransaction
*也可使用DBServerProvider.手动获取数据库相关信息
*用户信息、权限、角色等使用UserContext.Current操作
*gateway_nodesService对增、删、改查、导入、导出、审核业务代码扩展参照ServiceFunFilter
*/
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
using System.Linq;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
using VolPro.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Warehouse.IRepositories;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
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;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
}
/// <summary>
/// 网关注册Upsert
/// NodeCode 匹配则更新适配器类型/地址/在线状态并返回已有 NodeId
/// NodeCode 不匹配且 Token 验证通过则插入新记录。
/// </summary>
public async Task<gateway_nodes> RegisterNodeAsync(string nodeCode, string token, string adapterTypes, string baseUrl)
{
var existing = _repository.DbContext.Queryable<gateway_nodes>()
.First(x => x.NodeCode == nodeCode);
gateway_nodes entity;
if (existing != null)
{
// 已存在验证Token更新网关上报字段
if (existing.NodeToken != token)
throw new UnauthorizedAccessException("NodeToken 不匹配");
existing.AdapterTypes = adapterTypes;
existing.BaseUrl = baseUrl;
existing.IsOnline = "在线";
existing.LastHeartbeat = DateTime.Now;
_repository.DbContext.Updateable(existing).ExecuteCommand();
entity = existing;
}
else
{
// 新节点:直接插入
entity = new gateway_nodes
{
NodeCode = nodeCode,
NodeName = nodeCode,
NodeToken = token,
AdapterTypes = adapterTypes,
BaseUrl = baseUrl,
IsOnline = "在线",
Enable = "启用",
LastHeartbeat = DateTime.Now,
CreateDate = DateTime.Now
};
_repository.DbContext.Insertable(entity).ExecuteCommand();
}
return entity;
}
/// <summary>
/// 心跳更新。更新 LastHeartbeat 并标记在线。
/// </summary>
public async Task UpdateHeartbeatAsync(string nodeCode, string token)
{
var entity = _repository.DbContext.Queryable<gateway_nodes>()
.First(x => x.NodeCode == nodeCode && x.NodeToken == token);
if (entity == null)
throw new UnauthorizedAccessException("认证失败NodeCode 或 Token 无效");
entity.IsOnline = "在线";
entity.LastHeartbeat = DateTime.Now;
_repository.DbContext.Updateable(entity).ExecuteCommand();
}
/// <summary>
/// 设备数据同步。按照字段分治原则写入 base_device
/// 首次入库写全量后续仅更新网关字段IsOnline/ExtraData/ParentDeviceId等
/// parentSourceId 解析为 ParentDeviceId。
/// </summary>
public async Task<(int added, int updated)> SyncDevicesAsync(int gatewayNodeId, List<SyncDeviceItem> devices)
{
var db = _repository.DbContext;
// 批量查询已有设备映射表(用于 parentSourceId → ParentDeviceId 解析)
var adapterCodes = devices.Select(d => d.AdapterCode).Distinct().ToList();
var existingIds = db.Queryable<base_device>()
.Where(x => x.NodeId == gatewayNodeId && adapterCodes.Contains(x.AdapterCode))
.ToList()
.ToDictionary(x => (x.AdapterCode, x.SourceId), x => x.DeviceId);
int added = 0, updated = 0;
foreach (var d in devices)
{
var key = (d.AdapterCode, d.SourceId);
existingIds.TryGetValue(key, out var existingId);
bool isNew = existingId == 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)
{
// 首次入库写全量
var entity = new base_device
{
DeviceName = d.Name ?? $"DEV_{d.SourceId}",
AdapterCode = d.AdapterCode,
SourceId = d.SourceId,
DeviceCategory = d.Category,
DeviceGroup = d.Group,
NodeId = gatewayNodeId,
IsParent = d.IsParent ? "是" : "否",
ParentDeviceId = parentDeviceId,
IsOnline = d.IsOnline ? "在线" : "离线",
IpAddress = d.IpAddress,
Port = d.Port,
ExtraData = d.ExtraDataJson,
Enable = "启用",
LastSyncTime = DateTime.Now,
CreateDate = DateTime.Now
};
db.Insertable(entity).ExecuteCommand();
added++;
}
else
{
// 已有记录:仅更新网关字段
var entity = db.Queryable<base_device>().InSingle(existingId);
if (entity != null)
{
entity.IsOnline = d.IsOnline ? "在线" : "离线";
entity.IsParent = d.IsParent ? "是" : "否";
entity.ParentDeviceId = parentDeviceId ?? entity.ParentDeviceId;
entity.IpAddress = d.IpAddress;
entity.Port = d.Port;
entity.ExtraData = d.ExtraDataJson ?? entity.ExtraData;
entity.LastSyncTime = DateTime.Now;
db.Updateable(entity).ExecuteCommand();
updated++;
}
}
}
return (added, updated);
}
}
/// <summary>网关同步设备条目A3 接口接收的数据模型)</summary>
public class SyncDeviceItem
{
public string AdapterCode { get; set; } = "";
public string SourceId { get; set; } = "";
public string? Name { get; set; }
public string? Category { get; set; }
public string? Group { get; set; }
public bool IsParent { get; set; }
public string? ParentSourceId { get; set; }
public bool IsOnline { get; set; }
public string? IpAddress { get; set; }
public int? Port { get; set; }
public string? ExtraDataJson { get; set; }
}
}

View File

@@ -0,0 +1,82 @@
/*
*所有关于iot_alarm类的业务代码应在此处编写
*可使用repository.调用常用方法获取EF/Dapper等信息
*如果需要事务请使用repository.DbContextBeginTransaction
*也可使用DBServerProvider.手动获取数据库相关信息
*用户信息、权限、角色等使用UserContext.Current操作
*iot_alarmService对增、删、改查、导入、导出、审核业务代码扩展参照ServiceFunFilter
*/
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
using System.Linq;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
using VolPro.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Warehouse.IRepositories;
using System;
using System.Threading.Tasks;
namespace Warehouse.Services
{
public partial class iot_alarmService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Iiot_alarmRepository _repository;//访问数据库
[ActivatorUtilitiesConstructor]
public iot_alarmService(
Iiot_alarmRepository dbRepository,
IHttpContextAccessor httpContextAccessor
)
: base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
}
/// <summary>
/// Upsert 单条告警。按 SourceAlarmId 去重,已存在则跳过。
/// </summary>
public async Task UpsertAlarmAsync(SyncAlarmItem a, int? deviceId)
{
var db = _repository.DbContext;
// SourceAlarmId 去重
var exists = db.Queryable<iot_alarm>()
.Any(x => x.SourceAlarmId == a.SourceAlarmId);
if (exists) return;
var alarm = new iot_alarm
{
SourceAlarmId = a.SourceAlarmId,
DeviceId = (int)deviceId,
AdapterCode = a.AdapterCode,
AlarmLevel = a.Level,
AlarmDesc = a.Desc,
AlarmValue = (decimal?)a.Value,
StartTime = DateTime.TryParse(a.StartTime, out var st) ? st : DateTime.Now,
State = "未确认",
CreateDate = DateTime.Now
};
db.Insertable(alarm).ExecuteCommand();
}
}
/// <summary>告警同步条目A4 接口接收的数据模型)</summary>
public class SyncAlarmItem
{
public string SourceAlarmId { get; set; } = "";
public string DeviceSourceId { get; set; } = "";
public string AdapterCode { get; set; } = "";
public string Level { get; set; } = "";
public string Desc { get; set; } = "";
public double? Value { get; set; }
public string StartTime { get; set; } = "";
}
}

View File

@@ -0,0 +1,41 @@
/*
*所有关于iot_devicedata类的业务代码应在此处编写
*可使用repository.调用常用方法获取EF/Dapper等信息
*如果需要事务请使用repository.DbContextBeginTransaction
*也可使用DBServerProvider.手动获取数据库相关信息
*用户信息、权限、角色等使用UserContext.Current操作
*iot_devicedataService对增、删、改查、导入、导出、审核业务代码扩展参照ServiceFunFilter
*/
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
using System.Linq;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
using VolPro.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Warehouse.IRepositories;
namespace Warehouse.Services
{
public partial class iot_devicedataService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Iiot_devicedataRepository _repository;//访问数据库
[ActivatorUtilitiesConstructor]
public iot_devicedataService(
Iiot_devicedataRepository dbRepository,
IHttpContextAccessor httpContextAccessor
)
: base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
}
}
}

View File

@@ -0,0 +1,41 @@
/*
*所有关于video_channel类的业务代码应在此处编写
*可使用repository.调用常用方法获取EF/Dapper等信息
*如果需要事务请使用repository.DbContextBeginTransaction
*也可使用DBServerProvider.手动获取数据库相关信息
*用户信息、权限、角色等使用UserContext.Current操作
*video_channelService对增、删、改查、导入、导出、审核业务代码扩展参照ServiceFunFilter
*/
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
using System.Linq;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
using VolPro.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Warehouse.IRepositories;
namespace Warehouse.Services
{
public partial class video_channelService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Ivideo_channelRepository _repository;//访问数据库
[ActivatorUtilitiesConstructor]
public video_channelService(
Ivideo_channelRepository dbRepository,
IHttpContextAccessor httpContextAccessor
)
: base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
}
}
}

View File

@@ -0,0 +1,41 @@
/*
*所有关于video_record类的业务代码应在此处编写
*可使用repository.调用常用方法获取EF/Dapper等信息
*如果需要事务请使用repository.DbContextBeginTransaction
*也可使用DBServerProvider.手动获取数据库相关信息
*用户信息、权限、角色等使用UserContext.Current操作
*video_recordService对增、删、改查、导入、导出、审核业务代码扩展参照ServiceFunFilter
*/
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
using System.Linq;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
using VolPro.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Warehouse.IRepositories;
namespace Warehouse.Services
{
public partial class video_recordService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Ivideo_recordRepository _repository;//访问数据库
[ActivatorUtilitiesConstructor]
public video_recordService(
Ivideo_recordRepository dbRepository,
IHttpContextAccessor httpContextAccessor
)
: base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
//多租户会用到这init代码其他情况可以不用
//base.Init(dbRepository);
}
}
}

View File

@@ -0,0 +1,22 @@
/*
*Authorjxx
*Contact283591387@qq.com
*代码由框架生成,此处任何更改都可能导致被代码生成器覆盖
*所有业务编写全部应在Partial文件夹下base_deviceService与Ibase_deviceService中编写
*/
using Warehouse.IRepositories;
using Warehouse.IServices;
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Services
{
public partial class base_deviceService : ServiceBase<base_device, Ibase_deviceRepository>
, Ibase_deviceService, IDependency
{
public static Ibase_deviceService Instance
{
get { return AutofacContainerModule.GetService<Ibase_deviceService>(); } }
}
}

View File

@@ -0,0 +1,22 @@
/*
*Authorjxx
*Contact283591387@qq.com
*代码由框架生成,此处任何更改都可能导致被代码生成器覆盖
*所有业务编写全部应在Partial文件夹下gateway_nodesService与Igateway_nodesService中编写
*/
using Warehouse.IRepositories;
using Warehouse.IServices;
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Services
{
public partial class gateway_nodesService : ServiceBase<gateway_nodes, Igateway_nodesRepository>
, Igateway_nodesService, IDependency
{
public static Igateway_nodesService Instance
{
get { return AutofacContainerModule.GetService<Igateway_nodesService>(); } }
}
}

View File

@@ -0,0 +1,22 @@
/*
*Authorjxx
*Contact283591387@qq.com
*代码由框架生成,此处任何更改都可能导致被代码生成器覆盖
*所有业务编写全部应在Partial文件夹下iot_alarmService与Iiot_alarmService中编写
*/
using Warehouse.IRepositories;
using Warehouse.IServices;
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Services
{
public partial class iot_alarmService : ServiceBase<iot_alarm, Iiot_alarmRepository>
, Iiot_alarmService, IDependency
{
public static Iiot_alarmService Instance
{
get { return AutofacContainerModule.GetService<Iiot_alarmService>(); } }
}
}

View File

@@ -0,0 +1,22 @@
/*
*Authorjxx
*Contact283591387@qq.com
*代码由框架生成,此处任何更改都可能导致被代码生成器覆盖
*所有业务编写全部应在Partial文件夹下iot_devicedataService与Iiot_devicedataService中编写
*/
using Warehouse.IRepositories;
using Warehouse.IServices;
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Services
{
public partial class iot_devicedataService : ServiceBase<iot_devicedata, Iiot_devicedataRepository>
, Iiot_devicedataService, IDependency
{
public static Iiot_devicedataService Instance
{
get { return AutofacContainerModule.GetService<Iiot_devicedataService>(); } }
}
}

View File

@@ -0,0 +1,22 @@
/*
*Authorjxx
*Contact283591387@qq.com
*代码由框架生成,此处任何更改都可能导致被代码生成器覆盖
*所有业务编写全部应在Partial文件夹下video_channelService与Ivideo_channelService中编写
*/
using Warehouse.IRepositories;
using Warehouse.IServices;
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Services
{
public partial class video_channelService : ServiceBase<video_channel, Ivideo_channelRepository>
, Ivideo_channelService, IDependency
{
public static Ivideo_channelService Instance
{
get { return AutofacContainerModule.GetService<Ivideo_channelService>(); } }
}
}

View File

@@ -0,0 +1,22 @@
/*
*Authorjxx
*Contact283591387@qq.com
*代码由框架生成,此处任何更改都可能导致被代码生成器覆盖
*所有业务编写全部应在Partial文件夹下video_recordService与Ivideo_recordService中编写
*/
using Warehouse.IRepositories;
using Warehouse.IServices;
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace Warehouse.Services
{
public partial class video_recordService : ServiceBase<video_record, Ivideo_recordRepository>
, Ivideo_recordService, IDependency
{
public static Ivideo_recordService Instance
{
get { return AutofacContainerModule.GetService<Ivideo_recordService>(); } }
}
}

View File

@@ -1,168 +1,193 @@
-- ============================================ -- ============================================
-- SecMPS v2.0 数据库建表脚本 -- SecMPS v3.0 数据库建表脚本6张表
-- 数据库: gljs_main -- 数据库: gljs_main
-- 扩展表已合并到 Base_Device.ExtraData(JSON)
-- ============================================ -- ============================================
USE gljs_main; -- ============================================
-- 1. 统一设备主表 -- 1. 统一设备主表
CREATE TABLE IF NOT EXISTS Base_Device ( -- ExtraData(JSON) 承载所有适配器特有字段
DeviceId CHAR(36) NOT NULL PRIMARY KEY, -- DeviceGroup 路由到正确的网关Adapter和前端按钮组
DeviceName NVARCHAR(100) NOT NULL, -- ============================================
AdapterCode NVARCHAR(50) NOT NULL, DROP TABLE IF EXISTS base_device;
SourceId NVARCHAR(100) NOT NULL, CREATE TABLE base_device (
DeviceCategory INT NOT NULL DEFAULT 1, DeviceId INT AUTO_INCREMENT COMMENT '设备ID',
DeviceType NVARCHAR(50), DeviceName NVARCHAR(100) NOT NULL COMMENT '设备名称',
RegionId INT NULL, AdapterCode NVARCHAR(50) COMMENT '来源适配器(类型:实例)',
IsParent TINYINT NOT NULL DEFAULT 0, SourceId NVARCHAR(100) COMMENT '源系统设备ID',
ParentDeviceId CHAR(36) NULL, DeviceCategory NVARCHAR(50) NOT NULL COMMENT '设备种类(数据字典:门磁/空调/智能断路器/人行道闸/车辆道闸/485钥匙柜/网络钥匙柜/紧急报警按钮/红外报警器/门禁一体机/除湿_恒湿机/空调控制器/烟雾报警器/气体报警器/温湿度变送器/摄像机/硬盘录像机/动环采集器)',
IsOnline TINYINT NOT NULL DEFAULT 0, DeviceGroup NVARCHAR(20) NOT NULL COMMENT '设备分组(数据字典:视频设备/IoT设备/门禁设备/道闸设备/报警设备)',
IpAddress NVARCHAR(50), PointId INT NULL COMMENT '所属点位ID',
Port INT, NodeId INT NULL COMMENT '所属网关节点ID',
Location NVARCHAR(200), IsParent NVARCHAR(20) NOT NULL DEFAULT '' COMMENT '是否父设备(数据字典:是/否)',
Lat DOUBLE, ParentDeviceId INT NULL COMMENT '父设备ID(自引用,子设备挂父设备下)',
Lng DOUBLE, IsOnline NVARCHAR(20) DEFAULT '离线' COMMENT '在线状态(数据字典:在线/离线)',
MapModelId NVARCHAR(100), IpAddress NVARCHAR(50) COMMENT 'IP地址',
MapModelScale FLOAT DEFAULT 1.0, Port INT COMMENT '端口',
MapModelRotation NVARCHAR(100), Location NVARCHAR(200) COMMENT '安装位置',
ExtraData TEXT, Lat DOUBLE COMMENT '纬度',
LocalOverrides TEXT, Lng DOUBLE COMMENT '经度',
SyncVersion BIGINT DEFAULT 0, MapModelId NVARCHAR(100) COMMENT '三维地图模型ID',
LastSyncTime DATETIME, MapModelScale FLOAT DEFAULT 1.0 COMMENT '模型缩放比例',
Enable TINYINT DEFAULT 1, MapModelRotation NVARCHAR(100) COMMENT '模型旋转角度(JSON)',
Remark NVARCHAR(500), ExtraData TEXT COMMENT '适配器扩展数据JSON(Owl/MC4/门禁字段均存于此)',
CreateID INT, LastSyncTime DATETIME COMMENT '上次同步时间',
Creator NVARCHAR(50), Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP, Remark NVARCHAR(500) COMMENT '备注',
ModifyID INT, CreateID INT COMMENT '创建人ID',
Modifier NVARCHAR(50), Creator NVARCHAR(50) COMMENT '创建人',
ModifyDate DATETIME, CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE INDEX IX_Base_Device_Adapter_Source (AdapterCode, SourceId), ModifyID INT COMMENT '修改人ID',
INDEX IX_Base_Device_RegionId (RegionId), Modifier NVARCHAR(50) COMMENT '修改人',
INDEX IX_Base_Device_ParentId (ParentDeviceId) ModifyDate DATETIME COMMENT '修改时间',
PRIMARY KEY (DeviceId),
INDEX IX_Sync (AdapterCode, SourceId),
INDEX IX_Point (PointId),
INDEX IX_Parent (ParentDeviceId),
INDEX IX_Gateway (GatewayNodeId),
INDEX IX_Group (DeviceGroup)
) COMMENT '统一设备主表';
-- ============================================
-- 2. 视频通道表
-- DeviceId(INT) → base_device.DeviceId
-- ============================================
DROP TABLE IF EXISTS video_channel;
CREATE TABLE video_channel (
ChannelId INT AUTO_INCREMENT COMMENT '通道记录ID',
OwlChannelId NVARCHAR(64) NOT NULL COMMENT 'Owl系统通道ID',
DeviceId INT NOT NULL COMMENT '关联Base_Device设备ID',
OwlStreamApp NVARCHAR(50) COMMENT 'Owl流应用名',
OwlStreamName NVARCHAR(100) COMMENT 'Owl流名称',
HasPtz TINYINT DEFAULT 0 COMMENT '是否支持云台',
HasRecording TINYINT DEFAULT 0 COMMENT '是否支持录像',
RecordMode INT DEFAULT 0 COMMENT '录像模式',
SnapshotUrl NVARCHAR(500) COMMENT '快照地址',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (ChannelId),
INDEX IX_Device (DeviceId),
INDEX IX_Owl (OwlChannelId)
) COMMENT '视频通道表';
-- ============================================
-- 3. 录像记录表
-- ChannelId(INT) → video_channel.ChannelId
-- ============================================
DROP TABLE IF EXISTS video_record;
CREATE TABLE video_record (
RecordId INT AUTO_INCREMENT COMMENT '录像记录ID',
ChannelId INT NOT NULL COMMENT '关联通道ID',
OwlRecordId INT NOT NULL COMMENT 'Owl录像记录ID',
App NVARCHAR(50) COMMENT '应用名',
Stream NVARCHAR(100) COMMENT '流ID',
StartedAt DATETIME NOT NULL COMMENT '录像开始时间',
EndedAt DATETIME COMMENT '录像结束时间',
Duration DOUBLE DEFAULT 0 COMMENT '录像时长(秒)',
FilePath NVARCHAR(500) COMMENT '文件路径',
FileSize BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (RecordId),
INDEX IX_Channel (ChannelId),
INDEX IX_Time (StartedAt)
) COMMENT '录像记录表';
-- ============================================
-- 4. 设备数据归档表
-- DeviceId(INT) → base_device.DeviceId
-- ============================================
DROP TABLE IF EXISTS iot_devicedata;
CREATE TABLE iot_devicedata (
DataId INT AUTO_INCREMENT COMMENT '数据记录ID',
DeviceId INT NOT NULL COMMENT '关联设备ID(子设备/点位)',
PointValue DOUBLE COMMENT '点位数值',
UpdateTime DATETIME NOT NULL COMMENT '数据更新时间',
`Interval` INT DEFAULT 0 COMMENT '采集间隔(毫秒)',
ArchiveType INT DEFAULT 1 COMMENT '归档类型(1小时/2日)',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (DataId),
INDEX IX_Device (DeviceId),
INDEX IX_Time (CreateDate)
) COMMENT '设备数据归档表';
-- ============================================
-- 5. 告警记录表(通用)
-- DeviceId(INT) → base_device.DeviceId
-- ============================================
DROP TABLE IF EXISTS iot_alarm;
CREATE TABLE iot_alarm (
AlarmId INT AUTO_INCREMENT COMMENT '告警ID',
SourceAlarmId NVARCHAR(100) NOT NULL COMMENT '源系统告警ID',
DeviceId INT NOT NULL COMMENT '关联设备ID',
AlarmType INT DEFAULT 0 COMMENT '告警类型',
AlarmLevel NVARCHAR(20) DEFAULT '提示' COMMENT '告警等级(数据字典:提示/普通/重要/紧急)',
AlarmDesc NVARCHAR(500) COMMENT '告警描述',
AlarmValue DOUBLE COMMENT '触发值',
StartTime DATETIME NOT NULL COMMENT '告警开始时间',
EndTime DATETIME COMMENT '告警结束时间',
ConfirmTime DATETIME COMMENT '确认时间',
ConfirmUser NVARCHAR(50) COMMENT '确认人',
State NVARCHAR(20) DEFAULT '未确认' COMMENT '状态(数据字典:未确认/已确认/已结束)',
AdapterCode NVARCHAR(50) COMMENT '来源适配器',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (AlarmId),
INDEX IX_Device (DeviceId),
INDEX IX_Source (SourceAlarmId),
INDEX IX_Time (StartTime),
INDEX IX_Level (AlarmLevel)
) COMMENT '告警记录表';
-- ============================================
-- 6. 网关节点注册表
-- ============================================
DROP TABLE IF EXISTS gateway_nodes;
CREATE TABLE gateway_nodes (
NodeId INT AUTO_INCREMENT COMMENT '网关节点ID',
NodeCode NVARCHAR(50) NOT NULL COMMENT '网关唯一编码',
NodeName NVARCHAR(100) NOT NULL COMMENT '网关名称',
NodeToken NVARCHAR(100) NOT NULL COMMENT '认证令牌',
AdapterTypes NVARCHAR(200) COMMENT '支持的适配器类型(网关上报)',
BaseUrl NVARCHAR(200) COMMENT '网关自身地址(网关上报)',
LastHeartbeat DATETIME COMMENT '上次心跳时间',
IsOnline NVARCHAR(20) DEFAULT '离线' COMMENT '在线状态(数据字典:在线/离线)',
Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)',
Remark NVARCHAR(500) COMMENT '备注',
CreateID INT COMMENT '创建人ID',
Creator NVARCHAR(50) COMMENT '创建人',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
ModifyID INT COMMENT '修改人ID',
Modifier NVARCHAR(50) COMMENT '修改人',
ModifyDate DATETIME COMMENT '修改时间',
PRIMARY KEY (NodeId),
UNIQUE INDEX IX_Code (NodeCode),
INDEX IX_Online (IsOnline)
) COMMENT '网关节点注册表';
-- ═══════════════════════════════════════════════
-- SecMPS 规则引擎: warehouse_variable 变量定义表 (P1-6)
-- =================================================
-- 规则条件/动作的 ValueId 绑定到此表的 VariableId
-- DeviceId 关联 base_device.DeviceId
-- =================================================
CREATE TABLE warehouse_variable (
VariableId INT IDENTITY(1,1) PRIMARY KEY,
DeviceId INT NOT NULL,
VariableName NVARCHAR(255) NOT NULL, -- 温度/湿度/人数
PointIndex INT DEFAULT 0, -- MC4 pointIndex / Owl 统计量编码
Unit NVARCHAR(50) NULL, -- ℃/%/人
SortOrder INT DEFAULT 0
); );
-- 2. 视频设备扩展表 CREATE INDEX IX_warehouse_variable_DeviceId ON warehouse_variable (DeviceId);
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)
);
-- 3. 视频通道表
CREATE TABLE IF NOT EXISTS Video_Channel (
ChannelId CHAR(36) NOT NULL PRIMARY KEY,
OwlChannelId NVARCHAR(64) NOT NULL,
DeviceId CHAR(36) NOT NULL,
ChannelName NVARCHAR(100) NOT NULL,
ChannelNo INT DEFAULT 0,
OwlStreamApp NVARCHAR(50),
OwlStreamName NVARCHAR(100),
HasPtz TINYINT DEFAULT 0,
HasRecording TINYINT DEFAULT 0,
RecordMode INT DEFAULT 0,
IsOnline TINYINT DEFAULT 0,
SnapshotUrl NVARCHAR(500),
Location NVARCHAR(200),
Lat DOUBLE,
Lng DOUBLE,
Enable TINYINT DEFAULT 1,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_Channel_Owl (OwlChannelId),
INDEX IX_Channel_Device (DeviceId)
);
-- 4. 录像记录表 -- F3.2 规则引擎滞后窗 (hysteresis)
CREATE TABLE IF NOT EXISTS Video_Record ( ALTER TABLE warehouse_rulecondition ADD
RecordId CHAR(36) NOT NULL PRIMARY KEY, RecoveryThreshold_Numeric DECIMAL(18,2) NULL,
ChannelId CHAR(36) NOT NULL, RecoveryThreshold_Switch NVARCHAR(50) 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)
);
-- 5. IoT设备扩展表 -- F3.3 条件级冷却
CREATE TABLE IF NOT EXISTS Device_IoT_Ext ( ALTER TABLE warehouse_rulecondition ADD
ExtId CHAR(36) NOT NULL PRIMARY KEY, LastTriggered DATETIME NULL,
DeviceId CHAR(36) NOT NULL, LastTriggerValue DECIMAL(18,2) 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)
);
-- 6. 设备点位表
CREATE TABLE IF NOT EXISTS IoT_DevicePoint (
PointId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
Mc4DeviceId INT NOT NULL,
PointIndex INT NOT NULL,
PointType INT,
PointTag NVARCHAR(100),
PointName NVARCHAR(100) NOT NULL,
PointDesc NVARCHAR(200),
Unit NVARCHAR(50),
IsControlPoint TINYINT DEFAULT 0,
Mc4Option NVARCHAR(500),
Enable TINYINT DEFAULT 1,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_Point_Mc4 (Mc4DeviceId, PointIndex),
INDEX IX_Point_Device (DeviceId)
);
-- 7. 设备数据归档表(仅存快照,实时不入库)
CREATE TABLE IF NOT EXISTS IoT_DeviceData (
DataId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
PointId CHAR(36) NOT NULL,
PointValue DOUBLE,
UpdateTime DATETIME NOT NULL,
`Interval` INT DEFAULT 0,
ArchiveType INT DEFAULT 1,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX IX_Data_Device (DeviceId),
INDEX IX_Data_Time (CreateDate)
);
-- 8. 告警记录表
CREATE TABLE IF NOT EXISTS IoT_Alarm (
AlarmId CHAR(36) NOT NULL PRIMARY KEY,
Mc4AlarmId NVARCHAR(64) NOT NULL,
DeviceId CHAR(36),
PointId CHAR(36),
AlarmType INT DEFAULT 0,
AlarmLevel INT DEFAULT 1,
AlarmDesc NVARCHAR(500),
AlarmValue DOUBLE,
StartTime DATETIME NOT NULL,
EndTime DATETIME,
ConfirmTime DATETIME,
ConfirmUser NVARCHAR(50),
State INT DEFAULT 1,
AdapterCode NVARCHAR(50),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_Alarm_Mc4 (Mc4AlarmId),
INDEX IX_Alarm_Device (DeviceId),
INDEX IX_Alarm_Time (StartTime)
);

View File

@@ -0,0 +1,3 @@
严重性 代码 说明 项目 文件 行 抑制状态 详细信息
错误(活动) CS1061 “ControlRequest”未包含“Command”的定义并且找不到可接受第一个“ControlRequest”类型参数的可访问扩展方法“Command”(是否缺少 using 指令或程序集引用?) IntegrationGateway.Host D:\Code\SecMPS\gateway\src\IntegrationGateway.Host\Program.cs 213
错误(活动) CS1061 “ControlRequest”未包含“Parameters”的定义并且找不到可接受第一个“ControlRequest”类型参数的可访问扩展方法“Parameters”(是否缺少 using 指令或程序集引用?) IntegrationGateway.Host D:\Code\SecMPS\gateway\src\IntegrationGateway.Host\Program.cs 213

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,256 @@
# 钥匙柜KMS整合方案 v1.0
> **版本**: 1.0
> **日期**: 2025-05-19
> **数据源**: doc/对接文档/钥匙管理系统软件接口.docx
> **架构**: IntegrationGateway 适配器模式 + Vol.Pro 管理端
---
## 1. 现状分析
### 1.1 KMS 系统概览
钥匙管理系统是一个独立的 REST API 服务,管理智能钥匙柜的硬件设备。
| 项目 | 说明 |
|------|------|
| 技术栈 | Java/Spring Boot (推测) |
| 认证方式 | clientId/clientSecret → Bearer Token (30分钟) |
| 数据模型 | 柜体(locker) → 锁孔(lockhole) → 钥匙(opener) |
| 用户体系 | 员工(staff) + 员工组(staff group) |
| 核心业务 | 借还记录、交接记录、远程授权、告警记录 |
### 1.2 KMS 第三方集成接口第2.18节)
KMS 预留了 8 个专用于第三方对接的接口:
| 接口 | 路径 | 用途 |
|------|------|------|
| 心跳 | `/prod-api/kms/thirdparty/heartbeat` | 检测 KMS 存活 |
| 批量删除员工 | `/prod-api/kms/staff/batchDelete` | 同步删除 |
| 批量同步员工 | `/prod-api/kms/staff/batchSync` | 同步员工信息 |
| 查询柜体钥匙 | `/prod-api/kms/thirdparty/locker/keys` | 获取所有柜体及其钥匙列表 |
| 授权记录 | `/prod-api/kms/thirdparty/auth/records` | 查询远程授权历史 |
| 借还记录 | `/prod-api/kms/thirdparty/borrow/records` | 查询借还日志 |
| 告警记录 | `/prod-api/kms/thirdparty/alarm/records` | 查询告警列表 |
| 事件记录 | `/prod-api/kms/thirdparty/event/records` | 查询系统事件 |
认证方式:所有接口需要在 `Authorization: Bearer <token>` 头中携带 Token。
### 1.3 Vol.Pro 端现存钥匙相关模块
| 表/模块 | 说明 | 来源 |
|----------|------|------|
| `warehouse_keys` | 钥匙管理(自主) | Vol.Pro 代码生成 |
| `warehouse_keyapply` | 钥匙领用申请 | Vol.Pro 代码生成 |
| `warehouse_keylog` | 钥匙使用日志 | Vol.Pro 代码生成 |
这些是 Vol.Pro 自建的钥匙管理流程(申请→审批→记录),与硬件 KMS 系统平行运行。
---
## 2. 整合策略
### 2.1 核心决策
**KMS 作为一个独立的物联网子系统接入 IntegrationGateway**,与 Owl、MC4.0 平级。
通过适配器模式KMS 的能力被抽象为统一的网关接口Vol.Pro 管理端通过网关调用 KMS无需直接对 KMS 编程。
### 2.2 KMS 能力评估
| 能力 | KMS 支持 | 统一接口 | 实现优先级 |
|------|:---:|------|:---:|
| 设备列表 | ✅ (钥匙柜+钥匙) | `IHasFlatDevices` | Phase 1 |
| 告警 | ✅ (告警记录) | `IHasAlarms` | Phase 1 |
| 实时状态 | ❌ | 无 | — |
| 远程控制 | ✅ (远程授权/开门) | `IAcceptsControl` (新增) | Phase 2 |
| 借还记录 | ✅ | 新增接口 | Phase 2 |
| 员工同步 | ✅ | 无 (单向推送) | Phase 2 |
### 2.3 设备模型映射
KMS 的物理拓扑是 **柜体 → 锁孔 → 钥匙**。映射到 `StandardDevice`
```
KMS 柜体 (locker) → StandardDevice { DeviceGroup="门禁设备", DeviceCategory="智能钥匙柜", IsParent=true }
KMS 锁孔 (lockhole) → StandardDevice { DeviceGroup="门禁设备", DeviceCategory="钥匙位", IsParent=false, ParentSourceId=柜体SourceId }
(钥匙本身是一个逻辑实体,不映射为设备)
```
**示例**
```
KMS "10位智能公共钥匙柜" (lockerId=25)
├── 钥孔1 "仓库大门钥匙" (lockholeSort=1)
├── 钥孔2 "机房钥匙" (lockholeSort=2)
├── ...
└── 钥孔10 "配电室钥匙" (lockholeSort=10)
```
每个锁孔又是一个 `StandardDevice`,其 `Extra` 字段承载钥匙状态(在位/离位/借出)。
---
## 3. 网关改造
### 3.1 新增 `IntegrationGateway.Adapters.Kms`
```
gateway/src/IntegrationGateway.Adapters.Kms/
├── KmsAdapter.cs # IHasFlatDevices + IHasAlarms
└── KmsAuthHelper.cs # clientId/clientSecret → Bearer Token
```
### 3.2 KmsAuthHelper
```csharp
/// 认证流程: POST /prod-api/kms/token?clientId=xxx&clientSecret=yyy → { code, token }
/// Token 有效期 30 分钟, 过期前 5 分钟自动刷新
public class KmsAuthHelper
{
public async Task<string> GetTokenAsync() { ... }
public async Task<HttpClient> GetAuthenticatedClientAsync() { ... }
}
```
### 3.3 KmsAdapter 能力接口
```csharp
public class KmsAdapter : IHasFlatDevices, IHasAlarms
{
public string AdapterCode { get; } // "KMS:main"
public AdapterCapabilities Capabilities => new()
{
HasFlatDevices = true, HasAlarms = true
};
// IHasFlatDevices: 查询柜体钥匙信息 → List<StandardDevice>
public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword)
{
// 调 2.18.4 GET /prod-api/kms/thirdparty/locker/keys
// 展开为: 每个柜体 → 1个父设备, 每个锁孔 → 1个子设备
}
// IHasAlarms: 查询告警记录
public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(...)
{
// 调 2.18.7 GET /prod-api/kms/thirdparty/alarm/records
// KMS 告警类型: 1=当前告警, 2=历史告警
}
public async Task ConfirmAlarmAsync(string alarmId) { ... }
public async Task EndAlarmAsync(string alarmId) { ... }
}
```
### 3.4 配置新增
```json
// appsettings.json
{
"KMS": {
"InstanceName": "main",
"BaseUrl": "http://192.168.1.50:8080",
"ClientId": "your_client_id",
"ClientSecret": "your_client_secret"
}
}
```
### 3.5 Program.cs 注册
```csharp
var kmsList = app.Configuration.GetSection("KMS").Get<List<KmsConfig>>() ?? new();
foreach (var k in kmsList)
registry.Register(new KmsAdapter($"KMS:{k.InstanceName ?? "default"}", http, k.BaseUrl, k.ClientId, k.ClientSecret));
```
---
## 4. Vol.Pro 管理端改动
### 4.1 数据流向
```
KMS 硬件柜 IntegrationGateway Vol.Pro
─────── ────────────── ──────
钥匙在位/离位 ────→ KmsAdapter.GetDevices ────→ base_device 表
(AdapterCode="KMS:main")
(DeviceCategory="钥匙位")
告警事件 ────→ KmsAdapter.GetAlarms ────→ iot_alarm 表
远程授权开门 ←──── B-interface (Phase2) ←──── 管理端操作
```
### 4.2 不需要改的内容
- `warehouse_keys` / `warehouse_keyapply` / `warehouse_keylog` — 保留现有体系,不与 KMS 冲突
- `base_device` 表 — 已支持 `DeviceCategory="钥匙位"``DeviceGroup="门禁设备"`
- 前端操作列 — 已预留 `AccessDeviceActions` / `AlarmDeviceActions` 骨架
- 管理端设备列表 — 自动显示 KMS 同步的设备
### 4.3 需要新增的内容
| 项 | 说明 |
|------|------|
| KMS 数据字典项 | `设备种类` 字典增加 "智能钥匙柜"、"钥匙位" |
| 前端 KMS 操作按钮 | `KeyDeviceActions.vue` — 显示钥匙状态 + 远程授权入口 (Phase 2) |
---
## 5. 数据映射表
### 5.1 KMS 柜体 → StandardDevice
| KMS 字段 | StandardDevice 字段 |
|------|------|
| lockerId | SourceId |
| lockerName | Name |
| "智能钥匙柜" | Category |
| "门禁设备" | Group |
| true | IsParent |
| lockerCode | Extra.lockerCode |
| online (健康检查) | IsOnline |
### 5.2 KMS 锁孔 → StandardDevice
| KMS 字段 | StandardDevice 字段 |
|------|------|
| lockerId.lockholeSort | SourceId (组合) |
| openerName (钥匙名) | Name |
| "钥匙位" | Category |
| "门禁设备" | Group |
| false | IsParent |
| lockerId | ParentSourceId |
| openerState (在位/离位) | Extra.openerState |
| openerType (永久授权/一次性授权/应急授权) | Extra.openerType |
### 5.3 KMS 告警 → StandardAlarm
| KMS 字段 | StandardAlarm 字段 |
|------|------|
| uuid | AlarmId (SourceAlarmId) |
| warningTime | OccurTime |
| alarmType 映射 | Level (提示/普通/重要) |
| lockerName + openerName | Title |
| remark | Content |
---
## 6. 实施计划
| 阶段 | 内容 | 预计工时 |
|------|------|:---:|
| K1 | 创建 `IntegrationGateway.Adapters.Kms` 项目骨架 | 0.5h |
| K2 | 实现 `KmsAuthHelper` (clientId/secret → Token) | 1h |
| K3 | 实现 `KmsAdapter``IHasFlatDevices` (设备同步) | 2h |
| K4 | 实现 `KmsAdapter``IHasAlarms` (告警同步) | 1h |
| K5 | 网关配置 + Program.cs 注册 | 0.5h |
| K6 | 字典补充(智能钥匙柜/钥匙位) | 0.5h |
| K7 | 联调验证 (需 KMS 环境) | 2h |
| K8 | Phase 2: 远程授权/开门 + 前端按钮 | 3h |
---
> **风险**: KMS 的实际 API 路径和响应格式需在真实环境验证,文档中的路径格式可能与实际部署有差异(如 `/prod-api/kms/` 前缀可能变化)。

View File

@@ -0,0 +1,256 @@
# 钥匙柜KMS整合方案 v2.0
> **版本**: 2.0
> **日期**: 2025-05-19
> **数据源**: `doc/对接文档/钥匙管理系统软件接口.docx`KMS API v1.0.4
> **技术栈**: 全链路 .NET 8 / C#(网关 ASP.NET Core + Vol.Pro ASP.NET Core
> **架构**: IntegrationGateway 适配器模式KMS 作为第三个子系统适配器加入
---
## 1. KMS 系统全接口分析
### 1.1 认证体系
| 项目 | 说明 |
|------|------|
| 认证接口 | `POST /prod-api/getToken` |
| 参数 | `clientId` + `clientSecret`(由 KMS 管理员分配) |
| 返回 | `{ code: 200, token: "xxx" }` |
| 有效期 | 30 分钟,过期重新获取 |
| 使用方式 | 后续请求 Header: `Authorization: Bearer <token>` |
### 1.2 第三方专用接口(第 2.18 节 —— 整合核心)
KMS 预留了 8 个专供第三方对接的接口,这些是网关适配器必须实现的:
| # | 方法 | 路径 | 用途 | 对应能力接口 |
|---|:---:|------|------|------|
| 2.18.1 | `POST` | `/prod-api/heartBeat` | 心跳检测 | `IGatewayAdapter.HealthCheck` |
| 2.18.2 | `POST` | `/prod-api/batchDeleteStaff` | 批量删除员工 | Phase 2 |
| 2.18.3 | `POST` | `/prod-api/batchSyncStaff` | 批量同步员工信息 | Phase 2 |
| 2.18.4 | `POST` | `/prod-api/getOpenerList` | 查询所有柜体及钥匙列表 | `IHasFlatDevices` |
| 2.18.5 | `POST` | `/prod-api/getPermissionList` | 查询远程授权记录 | Phase 2 |
| 2.18.6 | `POST` | `/prod-api/getRecordList` | 查询借还记录 | Phase 2 |
| 2.18.7 | `POST` | `/prod-api/getWarningList` | 查询告警记录 | `IHasAlarms` |
| 2.18.8 | `POST` | `/thirdPlatlogin` | 第三方登录/事件记录 | Phase 2 |
> 注意:第 2.18.X 节接口的路径不同于标准 KMS 业务接口(不以 `/prod-api/kms/` 开头)。这是 KMS 为第三方特意设计的**扁平化集成 API**。
### 1.3 标准 KMS 业务接口(第 2.3-2.17 节 —— 辅助参考)
以下是 KMS 完整的标准 REST API供深入对接时使用
| 模块 | 接口 | 方法 | 说明 |
|------|------|:---:|------|
| **认证** | `/prod-api/getToken` | POST | 获取 Bearer Token |
| **部门** | `/prod-api/system/dept/root/{userId}` | GET | 获取部门树 |
| **交接记录** | `/prod-api/kms/handover/handoverInfolist` | GET | 查询交接记录明细 |
| | `/prod-api/kms/handover/list` | GET | 查询交接记录列表(分页) |
| **授权** | `/prod-api/kms/permission/list` | GET | 查询授权列表 |
| | `/prod-api/kms/permission/listPer` | GET | 查询授权人列表 |
| | `/prod-api/kms/permission/remote` | POST | 远程授权 |
| **告警** | `/prod-api/kms/warning/list` | GET | 查询告警列表(标准版) |
| **员工可借** | `/prod-api/kms/staffopener/available` | POST | 设置员工可借/永久授权钥匙 |
| | `/prod-api/kms/staffopener/listall` | GET | 查询员工可借/永久授权钥匙列表 |
| **员工管理** | `/prod-api/kms/staff` | POST | 创建员工 |
| | `/prod-api/kms/staff` | PUT | 修改员工信息 |
| | `/prod-api/kms/staff/list` | GET | 分页查询员工列表 |
| | `/prod-api/kms/staff/{id}` | DELETE | 删除员工 |
| | `/prod-api/kms/staff/{id}` | GET | 获取员工详细信息 |
| **员工组** | `/prod-api/kms/staffGroup/...` | CRUD | 员工组管理6个接口 |
| **物品类别** | `/prod-api/kms/openerType/...` | CRUD | 物品类别管理6个接口 |
| **柜体管理** | `/prod-api/kms/locker` | POST/PUT | 创建/修改柜体 |
| | `/prod-api/kms/locker/list` | GET | 分页查询柜体列表 |
| | `/prod-api/kms/locker/{id}` | DELETE/GET | 删除/获取柜体详细信息 |
| | `/prod-api/kms/locker/statistics` | GET | 首页统计图表数据 |
| **锁孔管理** | `/prod-api/kms/lockhole` | PUT | 修改锁孔 |
| | `/prod-api/kms/lockhole/list` | GET | 分页查询锁孔列表 |
| | `/prod-api/kms/lockhole/{id}` | DELETE/GET | 删除/获取锁孔详细信息 |
| **钥匙管理** | `/prod-api/kms/opener` | POST/PUT | 创建/修改钥匙 |
| | `/prod-api/kms/opener/list` | GET | 分页查询钥匙列表 |
| | `/prod-api/kms/opener/selectCanBorrow` | GET | 查询可借钥匙列表 |
| | `/prod-api/kms/opener/staff` | GET | 查询可借钥匙员工列表 |
| | `/prod-api/kms/opener/{id}` | DELETE/GET | 删除/获取钥匙详细信息 |
| **钥匙组** | `/prod-api/kms/openerGroup/...` | CRUD | 钥匙组管理7个接口 |
> 共约 **50+ 个 REST 端点**,覆盖 KMS 完整业务。
---
## 2. 整合策略
### 核心原则
1. **KMS 作为独立子系统**,通过 IntegrationGateway 的 `KmsAdapter` 接入
2. **Phase 1** 实现第三方接口8 个端点),这是 KMS 专门为集成设计的
3. **Phase 2** 按需扩展标准业务接口(远程授权/开门/借还记录查询)
4. Vol.Pro 管理端通过网关 B 组接口间接调用 KMS不直接对 KMS 编程
### 适配器能力矩阵
| 能力接口 | 实现 | 映射的 KMS 第三方接口 |
|----------|:---:|------|
| `IGatewayAdapter` | ✅ | 2.18.1 心跳 |
| `IHasFlatDevices` | ✅ | 2.18.4 柜体钥匙列表 → StandardDevice |
| `IHasAlarms` | ✅ | 2.18.7 告警列表 → StandardAlarm |
| 远程控制 (新接口) | ⏭️ | 2.18.5 远程授权 + KMS 标准接口 remote |
---
## 3. 设备树映射
KMS 物理拓扑:**柜体(Locker) → 锁孔(Lockhole) → 钥匙(Opener)**
映射到 `base_device` 表:
```
KMS 管理平台 (一个 IP:PORT = 一个 gateway_node)
├── 智能钥匙柜A (lockerId=25)
│ ├── 锁孔1 "仓库大门" (lockholeSort=1 → StandardDevice, Category="钥匙位")
│ ├── 锁孔2 "机房钥匙" (lockholeSort=2)
│ └── 锁孔N ...
├── 智能钥匙柜B (lockerId=26)
│ ├── 锁孔1 "配电室"
│ └── 锁孔M ...
```
| KMS 实体 | base_device 字段 | 值 |
|----------|------|------|
| 柜体 | `SourceId` | `locker_{lockerId}` |
| | `Name` | lockerName (如 "10位智能公共钥匙柜") |
| | `DeviceCategory` | "智能钥匙柜" |
| | `DeviceGroup` | "门禁设备" |
| | `IsParent` | "是" |
| | `Extra` | `{ lockerCode, lockholeCount }` |
| 锁孔子设备 | `SourceId` | `lockhole_{lockerId}_{lockholeSort}` |
| | `Name` | openerName (如 "仓库大门钥匙") |
| | `DeviceCategory` | "钥匙位" |
| | `DeviceGroup` | "门禁设备" |
| | `IsParent` | "否" |
| | `ParentSourceId` | `locker_{lockerId}` (解析为 ParentDeviceId) |
| | `Extra` | `{ openerState, openerType, openerId }` |
| | `IsOnline` | openerState="在位" → 在线; "离位" → 离线 |
---
## 4. 告警映射
| KMS 告警字段(2.18.7) | StandardAlarm 字段 | 说明 |
|------|------|------|
| uuid | SourceAlarmId | KMS 告警唯一ID |
| warningTime | OccurTime | 告警时间 |
| type (1=当前告警,2=历史告警) | Status | type=1→"未确认", type=2→"已结束" |
| lockerName + lockholeSort | Title | 如 "10位公共钥匙柜 锁孔3" |
| openerName | 关联设备 | 通过 openerId 查找对应设备 |
| remark | Content | 告警备注详情 |
---
## 5. 网关改造清单
### 5.1 新增项目
```
gateway/src/IntegrationGateway.Adapters.Kms/
├── IntegrationGateway.Adapters.Kms.csproj
├── KmsAdapter.cs # IHasFlatDevices + IHasAlarms
├── KmsAuthHelper.cs # clientId/clientSecret → Bearer Token
└── KmsModels.cs # KMS 响应 DTO
```
### 5.2 KmsAuthHelper
```csharp
/// .NET 8 ASP.NET Core 适配: KMS Bearer Token 认证
public class KmsAuthHelper
{
private readonly HttpClient _http;
private readonly string _baseUrl, _clientId, _clientSecret;
private string? _token;
private DateTime _expiry = DateTime.MinValue; // 30min TTL
public async Task<string> GetTokenAsync() { ... }
public async Task<HttpClient> GetAuthenticatedClientAsync() { ... }
}
```
### 5.3 KmsAdapter
```csharp
public class KmsAdapter : IHasFlatDevices, IHasAlarms
{
public string AdapterCode { get; } // "KMS:main"
public AdapterCapabilities Capabilities => new() {
HasFlatDevices = true, HasAlarms = true
};
// IHasFlatDevices → 2.18.4 POST /prod-api/getOpenerList
public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword);
// IHasAlarms → 2.18.7 POST /prod-api/getWarningList
public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to, string? level, string? state);
public async Task ConfirmAlarmAsync(string alarmId);
public async Task EndAlarmAsync(string alarmId);
// IGatewayAdapter
public async Task<bool> HealthCheckAsync(); // → 2.18.1 POST /prod-api/heartBeat
}
```
### 5.4 配置文件
```json
// appsettings.json 新增 KMS 段
{
"KMS": {
"InstanceName": "main",
"BaseUrl": "http://192.168.1.50:8080",
"ClientId": "your_client_id",
"ClientSecret": "your_client_secret"
}
}
```
### 5.5 Program.cs 注册
```csharp
var kmsList = app.Configuration.GetSection("KMS").Get<List<KmsConfig>>() ?? new();
foreach (var k in kmsList)
registry.Register(new KmsAdapter($"KMS:{k.InstanceName ?? "default"}",
httpClient, k.BaseUrl, k.ClientId, k.ClientSecret));
```
---
## 6. Vol.Pro 端改动
| 项 | 改动 | 说明 |
|------|:---:|------|
| 数据库 | 无 | base_device/iot_alarm 已兼容 |
| 后端代码 | 无 | A1-A4 同步逻辑通用 |
| 字典 | 新增 2 项 | "智能钥匙柜" / "钥匙位" 加入设备种类字典 |
| 前端页面 | 无 | 设备列表自动显示 KMS 同步的设备 |
| 前端操作按钮 | Phase 2 | `KeyDeviceActions.vue` — 显示钥匙状态 + 远程授权入口 |
---
## 7. 实施计划
| 阶段 | 内容 | 涉及文件 | 预计 |
|------|------|------|:---:|
| K0 | 创建 `Adapters.Kms` 项目 + 引用 Core | csproj + sln | 10min |
| K1 | `KmsModels.cs` — 所有 KMS 响应 DTO | 1 文件 | 30min |
| K2 | `KmsAuthHelper.cs` — Token 获取 + 刷新 | 1 文件 | 30min |
| K3 | `KmsAdapter.HealthCheck` + `GetDevicesAsync` | 1 文件 | 1h |
| K4 | `KmsAdapter.GetAlarmsAsync` + Confirm/End | 1 文件 | 1h |
| K5 | appsettings.json + Program.cs 注册 | 2 文件 | 15min |
| K6 | 字典补充 + 编译验证 | 管理端 | 15min |
| K7 | 联调验证 (需 KMS 环境) | — | 2h |
| K8 | Phase 2: 远程授权 + 前端按钮 | 3 文件 | 3h |
---
> **版本历史**:
> - v1.0 (2025-05-17) — 初版,未完整覆盖所有接口
> - v2.0 (2025-05-19) — 完整覆盖 50+ REST 接口,明确 Phase 1/2 分界,修正技术栈为 .NET 8

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) — 第五章修订:取消独立设备管理页面,改为框架主从表嵌入操作按钮

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,329 @@
# KMS 钥匙柜适配器 — 任务清单
> **基准文档**: `doc/设计文档/KMS钥匙柜适配器详细设计文档.md` v2.1
> **分支**: gateway-dev
> **原则**: 严格按照设计文档执行,严禁无中生有。网关/Vol.Pro 改动放倒数第二步,联调放最后。
---
## Phase K0: 项目骨架(预计 15min
### K0.1 创建适配器项目
- [ ]`gateway/src/` 下执行 `dotnet new classlib -n IntegrationGateway.Adapters.Kms -f net8.0`
- [ ] 删除自动生成的 `Class1.cs`
- [ ] 添加项目引用:`dotnet add reference ../IntegrationGateway.Core/IntegrationGateway.Core.csproj`
- [ ] 将项目加入解决方案:`dotnet sln add`
### K0.2 Host 引用适配器
- [ ] `dotnet add Host reference Adapters.Kms`
### K0.3 编译验证
- [ ] `dotnet build` → 0 错误
> **K0 提交点**: `PhaseK0_scaffold — Kms适配器项目骨架就绪`
---
## Phase K1: KmsModels — 数据模型(预计 1h
### K1.1 认证模型
- [ ] 创建 `KmsModels.cs`
- [ ] 添加 `KmsTokenResponse { Code, Token, Msg }`
### K1.2 第三方接口响应模型2.18.X
- [ ] `KmsOpenerListResponse { Code, Msg, Rows }`
- [ ] `KmsLocker { LockerId, LockerName, LockerCode, LockholeList }`
- [ ] `KmsLockhole { LockholeSort, OpenerId, OpenerName, OpenerType, OpenerState }`
- [ ] `KmsWarningListResponse { Code, Msg, Total, Rows }`
- [ ] `KmsWarning { Uuid, LockerName, LockholeSort, OpenerName, Type, WarningTime, Remark, StaffName }`
- [ ] `KmsRecordListResponse { Code, Msg, Total, Rows }`
- [ ] `KmsRecord { Uuid, LockerName, LockholeSort, OpenerName, StaffName, BorrowTime, ReturnTime, Type }`
### K1.3 标准接口响应模型2.3-2.17
- [ ] `KmsHandoverInfo` — 交接记录
- [ ] `KmsPermissionListResponse` + `KmsPermission` — 授权记录
- [ ] `KmsStaffListResponse` + `KmsStaff` — 员工
- [ ] `KmsLockerListResponse` + `KmsLockerInfo` — 柜体
- [ ] `KmsLockholeListResponse` + `KmsLockholeInfo` — 锁孔
- [ ] `KmsOpenerListResponse2` + `KmsOpenerInfo` — 钥匙
- [ ] `KmsStaffOpenerListResponse` + `KmsStaffOpener` — 员工可借
- [ ] `KmsRemotePermissionRequest` — 远程授权请求(联调时确认字段)
### K1.4 编译验证
- [ ] `dotnet build` → 0 错误DTO 引用 Core 的 `StandardDevice`/`StandardAlarm` 等确认无编译错误)
> **K1 提交点**: `PhaseK1_models — KmsModels.cs 完整定义全部 15 个 DTO`
---
## Phase K2: KmsAuthHelper — 认证(预计 30min
### K2.1 创建 KmsAuthHelper.cs
- [ ] 构造函数:接收 `HttpClient`, `baseUrl`, `clientId`, `clientSecret`
- [ ] 属性:`_token` (string?), `_tokenExpiry` (DateTime)
- [ ] 依赖:`System.Text.Json`, `System.Net.Http.Json`
### K2.2 GetTokenAsync
- [ ] POST `/prod-api/getToken?clientId=xx&clientSecret=yy`
- [ ] 检查 `resp.EnsureSuccessStatusCode()`
- [ ] 反序列化 `KmsTokenResponse`
- [ ] 校验 `Code == 200`
- [ ] 缓存 Token过期时间 = `UtcNow.AddMinutes(25)`30 分钟效期5 分钟余量)
### K2.3 GetAuthenticatedClientAsync
- [ ] 调用 `GetTokenAsync()`
- [ ] 创建新 `HttpClient``BaseAddress = _baseUrl`
- [ ] 设置 Header `Authorization: Bearer {token}`
- [ ] 返回 client
### K2.4 Invalidate
- [ ] `_token = null` 强制下次重新获取
### K2.5 编译验证
- [ ] `dotnet build` → 0 错误
> **K2 提交点**: `PhaseK2_auth — KmsAuthHelper Bearer Token 认证就绪`
---
## Phase K3: KmsAdapter 核心方法(预计 1.5h
### K3.1 类定义与构造函数
- [ ] `public class KmsAdapter : IHasFlatDevices, IHasAlarms`
- [ ] 字段:`_http`, `_auth` (KmsAuthHelper), `_limiter` (RateLimiter(5))
- [ ] 属性:`AdapterCode`, `DisplayName`, `Capabilities { HasFlatDevices=true, HasAlarms=true }`
- [ ] 构造函数:注入 `httpClient`, `baseUrl`, `clientId`, `clientSecret`
### K3.2 InitializeAsync
- [ ] `await _auth.GetTokenAsync()`
### K3.3 HealthCheckAsync2.18.1
- [ ] POST `/prod-api/heartBeat` (空 body `{}`)
- [ ] 返回 `resp.IsSuccessStatusCode`
- [ ] 异常捕获返回 false
### K3.4 GetDevicesAsync2.18.4 — 柜体+锁孔 → StandardDevice
- [ ] `await _limiter.WaitAsync()`
- [ ] POST `/prod-api/getOpenerList` (body `{}`)
- [ ] 反序列化 `KmsOpenerListResponse`
- [ ] 遍历 `Rows`
- 每个 `KmsLocker``MapLockerToDevice`父设备SourceId=`locker_{LockerId}`
- 每个 `KmsLockhole``MapLockholeToDevice`子设备ParentSourceId=`locker_{LockerId}`
- [ ] IsOnline 判断:`OpenerState == "在位"` → true
- [ ] Extra 字典:`{ openerId, openerType, openerState }` / `{ lockerCode, lockholeCount }`
- [ ] 返回 `PagedResult<StandardDevice>`
### K3.5 GetAlarmsAsync2.18.7 — 告警列表 → StandardAlarm
- [ ] `await _limiter.WaitAsync()`
- [ ] POST `/prod-api/getWarningList` (body `{}`)
- [ ] 反序列化 `KmsWarningListResponse`
- [ ] 映射:`AlarmId=uuid`, `Title="{lockerName} 锁孔{lockholeSort}: {openerName}"`, `Status=Type==1?"未确认":"已结束"`, `Level="普通"`
- [ ] 返回 `PagedResult<StandardAlarm>`
### K3.6 ConfirmAlarmAsync / EndAlarmAsync
- [ ] `ConfirmAlarmAsync`: POST `/prod-api/kms/warning/confirm/{alarmId}`
- [ ] `EndAlarmAsync`: 留空实现KMS 第三方接口不提供结束告警)
### K3.7 编译验证
- [ ] `dotnet build` → 0 错误
> **K3 提交点**: `PhaseK3_adapter_core — KmsAdapter 核心4方法就绪(HealthCheck/GetDevices/GetAlarms/Confirm)`
---
## Phase K4: KmsAdapter 扩展方法(预计 1h
### K4.1 GetBorrowRecordsAsync2.18.6
- [ ] POST `/prod-api/getRecordList`
- [ ] 参数:`from`, `to` DateTime?(联调时确认请求体格式)
- [ ] 返回 `PagedResult<KmsRecord>`
### K4.2 GetPermissionListAsync2.18.5
- [ ] POST `/prod-api/getPermissionList`
- [ ] 参数:`from`, `to` DateTime?
- [ ] 返回 `PagedResult<KmsPermission>`
### K4.3 BatchSyncStaffAsync2.18.3
- [ ] POST `/prod-api/batchSyncStaff`
- [ ] 请求体:`new { staff = staffList }`
- [ ] `resp.EnsureSuccessStatusCode()`
### K4.4 BatchDeleteStaffAsync2.18.2
- [ ] POST `/prod-api/batchDeleteStaff`
- [ ] 请求体:`List<string>` (staffUuid 数组)
- [ ] `resp.EnsureSuccessStatusCode()`
### K4.5 RemoteAuthorizeAsync2.4.3
- [ ] POST `/prod-api/kms/permission/remote`
- [ ] 请求体:`KmsRemotePermissionRequest`(联调确认字段)
### K4.6 ThirdPlatLoginAsync2.18.8
- [ ] POST `/thirdPlatlogin?username={username}`
- [ ] 处理 302 重定向:返回 `Location` header 或响应体
- [ ] 超时设置 15s
### K4.7 编译验证
- [ ] `dotnet build` → 0 错误
> **K4 提交点**: `PhaseK4_adapter_ext — 6个扩展方法全部就绪(记录/同步/授权/登录)`
---
## Phase K5: 配置与注册(预计 15min
### K5.1 KmsConfig POCO
- [ ]`Program.cs` 同级新增 `KmsConfig`
- [ ] 属性:`InstanceName?`, `BaseUrl`, `ClientId`, `ClientSecret`
### K5.2 appsettings.json
- [ ] 新增 `KMS` 数组配置段
- [ ] 配置项:`InstanceName`, `BaseUrl`, `ClientId`, `ClientSecret`
### K5.3 Program.cs 注册
- [ ] `var kmsList = app.Configuration.GetSection("KMS").Get<List<KmsConfig>>() ?? new();`
- [ ] foreach 注册 `KmsAdapter("KMS:{InstanceName}", http, baseUrl, clientId, clientSecret)`
- [ ] 适配器编码加入 `adapterTypes` 拼接
### K5.4 编译验证
- [ ] `dotnet build` → 0 错误
> **K5 提交点**: `PhaseK5_config — KMS多实例配置+Program.cs注册就绪`
---
## Phase K6: 编译与自测(预计 15min
### K6.1 全量编译
- [ ] `dotnet build` → 0 错误(确认 KMS 适配器不引入外部依赖)
### K6.2 启动测试
- [ ] `dotnet run` 启动网关
- [ ] 检查控制台输出:`[Gateway] N 个适配器已注册: Owl:main,MC4:31ku,KMS:main`
- [ ] 确认 KMS 初始化失败时打印错误但不阻塞
> **K6 提交点**: `PhaseK6_build — 网关全量编译通过 KMS适配器热加载不阻塞启动`
---
## Phase K7: 网关核心与 Host 扩展(预计 1.5h)⚠️ 倒数第二步
> **说明**: 此阶段按设计文档附录 B 新增 Core 能力接口 + B 组路由,遵循网关设计原则 §3.4。
### K7.1 新增 IAcceptsControl 接口
- [ ] 创建 `Core/Abstractions/IAcceptsControl.cs`
- [ ] 方法:`Task<ControlResult> SendControlAsync(sourceDeviceId, command, parameters)`
- [ ] 新增 `Core/Models/ControlResult.cs``{ Success, Message }`
### K7.2 新增 IHasBusinessLogs 接口
- [ ] 创建 `Core/Abstractions/IHasBusinessLogs.cs`
- [ ] 方法:`Task<PagedResult<BusinessLogEntry>> GetBusinessLogsAsync(logType, from, to, page, size, filters)`
- [ ] 新增 `Core/Models/BusinessLogEntry.cs``{ LogId, LogType, DeviceSourceId, StaffName, Description, CreatedAt, Extra }`
### K7.3 新增 IAcceptsDataSync 接口
- [ ] 创建 `Core/Abstractions/IAcceptsDataSync.cs`
- [ ] 方法:`Task<SyncResult> SyncDataAsync(dataType, items)`
- [ ] 方法:`Task<SyncResult> DeleteDataAsync(dataType, ids)`
- [ ] 新增 `Core/Models/SyncResult.cs``{ SuccessCount, FailCount, Message }`
### K7.4 KmsAdapter 实现新接口
- [ ] `KmsAdapter` 增加 `: IAcceptsControl, IHasBusinessLogs, IAcceptsDataSync`
- [ ] `SendControlAsync`:调 `RemoteAuthorizeAsync`command="open" 时调 `/kms/permission/remote`
- [ ] `GetBusinessLogsAsync`:按 logType 分发到 `GetBorrowRecordsAsync` / `GetPermissionListAsync` / 交接记录
- [ ] `SyncDataAsync`dataType="staff" 时调 `BatchSyncStaffAsync`
- [ ] `DeleteDataAsync`dataType="staff" 时调 `BatchDeleteStaffAsync`
### K7.5 Program.cs 新增 B 组路由
- [ ] `POST /api/gateway/control/{adapter}``IAcceptsControl.SendControlAsync`
- [ ] `GET /api/gateway/logs/{adapter}``IHasBusinessLogs.GetBusinessLogsAsync`
- [ ] `POST /api/gateway/sync/{adapter}``IAcceptsDataSync.SyncDataAsync`
- [ ] `DELETE /api/gateway/sync/{adapter}``IAcceptsDataSync.DeleteDataAsync`
### K7.6 编译验证
- [ ] `dotnet build` → 0 错误
> **K7 提交点**: `PhaseK7_gateway — 3个新Core接口+4条B路由+KmsAdapter多接口实现`
---
## Phase K8: Vol.Pro 管理端配套(预计 1h 倒数第二步
### K8.1 数据字典补充
- [ ] 管理端 → 字典管理 → 设备种类新增:"智能钥匙柜" / "钥匙位"
### K8.2 前端操作列扩展
- [ ] 编辑 `web.vite/src/views/warehouse/device_manager/base_device.vue`
- [ ] `onInited` 的 render 函数中增加 `DeviceGroup==='门禁设备'` 分支
- [ ] 显示 "开门" 按钮(调用网关 B8
- [ ] 显示 "权限" 下拉菜单(永久授权/临时授权/取消授权)
### K8.3 前端 API 调用
- [ ] `fetch()` 调网关 `http://localhost:5100/api/gateway/control/KMS:main`
- [ ] 请求体:`{ sourceDeviceId, command: "open", parameters: { openerId, staffId } }`
### K8.4 编译验证
- [ ] `npm run dev` → 无编译错误
> **K8 提交点**: `PhaseK8_volpro — 字典+前端操作按钮就绪`
---
## Phase K9: 联调验证(预计 3h 最后
> **前置条件**: KMS 服务端可访问,已分配 clientId/clientSecret
### K9.1 认证联调
- [ ] 网关启动 → KmsAdapter.InitializeAsync 成功获取 Token
- [ ] Token 过期自动刷新验证
- [ ] 错误 clientSecret → 网关控制台打印初始化失败日志
### K9.2 设备同步联调2.18.4
- [ ] `/api/gateway/health` 返回 KMS 适配器在线
- [ ] `/api/gateway/devices?adapter=KMS:main` 返回柜体+锁孔设备树
- [ ] 管理端 base_device 列表显示 KMS 设备AdapterCode=KMS:main
### K9.3 告警同步联调2.18.7
- [ ] `/api/gateway/alarms/KMS:main` 返回告警列表
- [ ] 管理端 iot_alarm 表有记录
### K9.4 远程控制联调2.4.3
- [ ] `/api/gateway/control/KMS:main` → 远程开门 → KMS 端锁孔门开
### K9.5 记录查询联调2.18.6
- [ ] `/api/gateway/logs/KMS:main?logType=borrow` 返回借还记录
### K9.6 员工同步联调2.18.3
- [ ] `/api/gateway/sync/KMS:main` → 批量同步员工成功
### K9.7 异常场景
- [ ] KMS 服务离线 → `/api/gateway/health` 中 KMS 返回 unhealthy
- [ ] KMS 恢复 → 下次心跳自动变 healthy
- [ ] 并发请求超过 5 QPS → 限流生效不崩溃
### K9.8 验收
- [ ] 网关 + Vol.Pro + KMS 三端数据一致
- [ ] 管理端可查看 KMS 设备树、告警
- [ ] 前端可远程开门
> **K9 提交点**: `PhaseK9_integration — 全链路联调通过`
---
## 任务总览
| Phase | 内容 | 文件数 | 预计 |
|:---:|------|:---:|:---:|
| K0 | 项目骨架 | 2 | 15min |
| K1 | KmsModels 全部 DTO | 1 | 1h |
| K2 | KmsAuthHelper | 1 | 30min |
| K3 | KmsAdapter 核心方法 | 1 | 1.5h |
| K4 | KmsAdapter 扩展方法 | 1 | 1h |
| K5 | 配置与注册 | 3 | 15min |
| K6 | 编译自测 | — | 15min |
| K7 | 网关 Core + Host 扩展 | 6 | 1.5h |
| K8 | Vol.Pro 管理端配套 | 2 | 1h |
| K9 | 联调验证 | — | 3h |
| **合计** | — | **17** | **~10h** |
---
> **版本**: 1.0 / 2025-05-19 / 严格按照 `KMS钥匙柜适配器详细设计文档.md` v2.1 制订

View File

@@ -0,0 +1,19 @@
Phase V3.6: Quartz Job 注册说明
═══════════════════════════════
以下 3 个 Job 需在 Vol.Pro 管理端 → Quartz 任务管理 中手动创建:
1. SyncDevicesJob
- 类名: VolPro.Warehouse.Services.SyncDevicesJob
- Cron: 0 */5 * * * ? (每5分钟)
- 说明: 遍历在线网关触发全量设备同步
2. HeartbeatMonitorJob
- 类名: VolPro.Warehouse.Services.HeartbeatMonitorJob
- Cron: 0/15 * * * * ? (每15秒)
- 说明: 检测心跳超时标记离线
3. RealtimePollJob (Phase 2 启用)
- 类名: VolPro.Warehouse.Services.RealtimePollJob
- Cron: 0/10 * * * * ? (每10秒)
- 说明: 轮询MC4.0实时值写iot_devicedata

View File

@@ -0,0 +1,56 @@
# SecMPS 统一问题清单 2026-06-03
> **版本**: 1.0
> **日期**: 2026-06-03
> **范围**: gateway / VolPro (api_sqlsugar) / web.vite / warehouse / owl_zlmediakit
> **来源**: 项目深度审计 + 规则引擎方案审查
---
## P0 — 阻塞性(影响功能完整性,必须修复)
| 编号 | 类别 | 问题 | 影响 | 方案 |
|:---:|:---:|------|------|------|
| P0-1 | 规则引擎 | RealtimePollJob 空壳 — IoT 实时值从未持久化,规则引擎无历史数据源 | 规则无法追溯历史趋势 | 在此 Job 实现轮询→写入 iot_devicedata或合并到 RuleEngineJob |
| P0-2 | 网关 | A1 自注册未调用 — `GatewayClientFactory.RegisterAsync` 已定义但 Program.cs 从未执行 | 网关启动后不向 Vol.Pro 注册 | `InitializeAllAsync()` 后遍历适配器调 A1 |
| P0-3 | 安全 | B 组路由零认证 — 14+ 条路由无任何认证 | 内网未授权客户端可操控设备、查视频流 | 生产环境绑定 `127.0.0.1`,或加 `X-Gateway-Key` 中间件 |
---
## P1 — 重要(影响性能、安全、可靠性)
| 编号 | 类别 | 问题 | 影响 | 方案 |
|:---:|:---:|------|------|------|
| P1-1 | 性能 | 逐设备 B4 调用 — 规则引擎按设备逐个调 B4 | 规则引擎 90% 时间耗在网络往返 | 新增 `POST /realtime/{adapter}/batch` 批量接口 |
| P1-2 | 性能 | 级联离线标记逐条 UPDATE — HeartbeatMonitorJob 对每台设备单独更新 | 设备多时慢且无事务 | 一条 SQL: `UPDATE base_device SET IsOnline='离线' WHERE GatewayNodeId=@id` |
| P1-3 | 安全 | Token/密码明文存储 — appsettings.json 明文且被复制到 bin/ | 源码泄露 = 凭据泄露 | 环境变量覆盖 + `.gitignore bin` |
| P1-4 | 可维护 | 前端硬编码网关地址 — `const GW = 'http://localhost:5100'` | 部署时需逐文件修改 | 统一用 `window.apiConfig.gatewayUrl` |
| P1-5 | 规则引擎 | DeviceId→(AdapterCode, SourceId) 解析缺失 | 规则引擎无法直接调网关 B4 | 批量查 base_device 建映射表 |
| P1-6 | 规则引擎 | ValueId 语义模糊 — 字典绑定但无对应实体表 | "变量"选的是什么不明确 | 新建 `warehouse_variable` 表 |
---
## P2 — 改善(影响排错效率、维护成本)
| 编号 | 类别 | 问题 | 影响 | 方案 |
|:---:|:---:|------|------|------|
| P2-1 | 代码质量 | 静默异常吞噬 — 适配器 `catch { return false; }` | 离线不知道原因 | `catch(Exception ex)` + STDERR 输出 |
| P2-2 | 规则引擎 | 阈值抖动 — 温度反复跳变时规则频繁触发→恢复 | 空调反复开关,告警洪水 | hysteresis 滞后窗 |
| P2-3 | 规则引擎 | 冷却期粒度 — Cooldown 在规则级OR 组合不该整体冷却 | 冷却期过宽 | 冷却期下沉到条件表或基于"上次触发值"去重 |
| P2-4 | 可维护 | warehouse 端 console.log 残留 — 30+ 处开发日志 | 生产环境噪声 | vite.config.ts 移除非生产日志 |
| P2-5 | 可维护 | 双端 gateway API 重复封装 | 维护两份 | 统一到 web.vite/src/api/gateway.js |
---
## P3 — 优化(影响开发体验、仓库整洁)
| 编号 | 类别 | 问题 | 影响 | 方案 |
|:---:|:---:|------|------|------|
| P3-1 | 规则引擎 | 动作执行阻塞 — `ExecuteActionsAsync` 串行等待 B5 响应 | 一条规则卡住全部阻塞 | `Task.WhenAll` + 5s 超时 |
| P3-2 | 文档 | bin 目录残留配置 | 仓库体积 + 凭据泄露 | `.gitignore``**/bin/` |
| P3-3 | 开发 | 网关无 Swagger | 调试需手动 curl | `AddEndpointsApiExplorer` + `MapSwagger` |
| P3-4 | 文档 | 设计文档与代码路由数不一致 | 架构文档过时 | 每次 Phase 同步更新 |
---
> **总计**: 18 项 — P0: 3 / P1: 6 / P2: 5 / P3: 4

View File

@@ -0,0 +1,280 @@
# SecMPS 统一问题清单 2026-06-03 修复方案
> **版本**: 1.0
> **日期**: 2026-06-03
> **基准**: `SecMPS统一问题清单20260603.md`
> **原则**: 按优先级逐项修复,每项修复后编译验证;涉及网关/Vol.Pro 改动的放一组批量提交
---
## 修复总览
| 阶段 | 优先级 | 涉及项目 | 文件数 | 预计 |
|:---:|:---:|------|:---:|:---:|
| F1 | P0-1 ~ P0-3 | gateway + Vol.Pro | 5 | 2h |
| F2 | P1-1 ~ P1-6 | gateway + Vol.Pro + 库表 + 前端 | 8 | 4h |
| F3 | P2-1 ~ P2-5 | gateway + warehouse | 8 | 2h |
| F4 | P3-1 ~ P3-4 | gateway + 文档 | 4 | 1h |
| **合计** | — | 4 项目 | **25** | **~9h** |
---
## 阶段 F1: P0 阻塞项修复(预计 2h
#### F1.1 [P0-1] RealtimePollJob 填充实现
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/RealtimePollJob.cs`
- [ ] 注入 `GatewayClient` + `Ibase_deviceRepository`
- [ ] `Execute()` 中:
1. 查询在线 MC4 网关 (`gateway_nodes WHERE IsOnline=在线 AND AdapterTypes LIKE '%MC4%'`)
2. 查对应设备列表 (`base_device WHERE DeviceGroup='IoT设备' AND IsOnline=在线`)
3. 对每个设备调 `GatewayClient.GetRealtimeAsync(gwBaseUrl, adapterCode, sourceId)`
4. 结果写入 `iot_devicedata`INSERT 新记录)
- [ ] `dotnet build` → 0 错误
#### F1.2 [P0-2] 网关 A1 自注册
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
- [ ]`registry.InitializeAllAsync()` 后加入:
```csharp
var nodeCode = gwCfg["NodeCode"] ?? "gw-default";
var nodeToken = gwCfg["NodeToken"] ?? "";
var adapterTypes = string.Join(",", registry.All.Select(a => a.AdapterCode));
await clientFactory.RegisterAsync(nodeCode, nodeToken, adapterTypes, volProUrl);
```
- [ ] `dotnet build` → 0 错误
#### F1.3 [P0-3] B 组路由认证
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
- [ ] 在 `app.UseCors()` 之后添加中间件:
```csharp
var gatewayKey = gwCfg["GatewayKey"];
if (!string.IsNullOrEmpty(gatewayKey))
{
app.Use(async (context, next) => {
var key = context.Request.Headers["X-Gateway-Key"].FirstOrDefault();
if (key == gatewayKey || context.Request.Path == "/") { await next(); }
else { context.Response.StatusCode = 401; }
});
}
```
- [ ] appsettings.json Gateway 段新增 `"GatewayKey": null`
- [ ] Vol.Pro 端 `GatewayClient` 所有 HTTP 请求头自动附加 `X-Gateway-Key`
- [ ] `dotnet build` → 0 错误
> **F1 提交点**: `Fix-P0: RealtimePollJob+A1自注册+B组认证`
---
## 阶段 F2: P1 重要项修复(预计 4h
#### F2.1 [P1-1] 网关新增批量实时值接口
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
- [ ] 新增 B4-batch 路由:
```csharp
app.MapPost("/api/gateway/realtime/{adapter}/batch", async (string adapter, BatchRealtimeRequest req) =>
{
var a = registry.FindByCode<IHasPoints>(adapter);
if (a == null) return Results.NotFound();
var results = new Dictionary<string, List<PointValue>>();
foreach (var deviceId in req.DeviceIds ?? new())
results[deviceId] = await a.GetRealtimeValuesAsync(deviceId);
return Results.Ok(results);
});
```
- [ ] 新增 `record BatchRealtimeRequest(List<string>? DeviceIds);`
- [ ] `dotnet build` → 0 错误
#### F2.2 [P1-2] 批量级联离线标记
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/HeartbeatMonitorJob.cs`
- [ ] 替换逐条 `UpdateAsync` 为:
```csharp
context.Repository.DbContext.Db.Ado.ExecuteCommand(
"UPDATE base_device SET IsOnline='离线' WHERE GatewayNodeId=@id AND IsOnline='在线'",
new { id = node.GatewayNodeId });
```
- [ ] `dotnet build` → 0 错误
#### F2.3 [P1-3] 凭据安全化
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/appsettings.json`
- `NodeToken` → `null`, 加注释 "生产环境由 SECMPS_GATEWAY_TOKEN 环境变量注入"
- Owl `Password` → `""`, 加注释
- KMS `ClientSecret` → `""`, 加注释
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
- `gwCfg["NodeToken"]` → `Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"]`
- [ ] 编辑 `.gitignore` → 加 `**/bin/`、`**/obj/`
- [ ] `git rm -r --cached gateway/src/IntegrationGateway.Host/bin/`
#### F2.4 [P1-4] 前端网关地址统一化
- [ ] 编辑 `web.vite/public/index.html` 的 `window.apiConfig` → 加 `gatewayUrl: 'http://localhost:5100'`
- [ ] 编辑 `web.vite/src/views/warehouse/device_manager/base_device.vue`
- `const GW = 'http://localhost:5100'` → `const GW = window.apiConfig.gatewayUrl || 'http://localhost:5100'`
- [ ] 编辑 `warehouse/src/api/gateway.ts`
- `const GW_BASE = 'http://localhost:5100'` → 读取 `window.apiConfig.gatewayUrl`
- [ ] 编辑 `warehouse/index.html` 的 `window.apiConfig` → 加 `gatewayUrl`
#### F2.5 [P1-5] 规则引擎增加 DeviceId 映射
- [ ] 在规则引擎实现方案中增加 `BuildDeviceMappingAsync` 方法:
```csharp
var deviceIds = rules.SelectMany(r => r.Conditions).Select(c => c.DeviceId).Distinct();
var devices = await _deviceRepo.FindAsync(d => deviceIds.Contains(d.DeviceId));
var map = devices.ToDictionary(d => d.DeviceId, d => (d.AdapterCode, d.SourceId));
```
- [ ] 后续调网关时用 `map[cond.DeviceId]` 拼装 URL
#### F2.6 [P1-6] 新建 warehouse_variable 表
- [ ] 执行 SQL:
```sql
CREATE TABLE warehouse_variable (
VariableId INT IDENTITY PRIMARY KEY,
DeviceId INT NOT NULL,
VariableName NVARCHAR(255), -- 温度/湿度/人数
PointIndex INT DEFAULT 0, -- MC4 pointIndex
Unit NVARCHAR(50), -- ℃/%/人
SortOrder INT DEFAULT 0
);
```
- [ ] 在 Vol.Pro 代码生成器选择 `warehouse_variable`,生成全套 CRUD 代码
- [ ] 管理端字典 "变量列表" 绑定到 `warehouse_variable.VariableName`
- [ ] 规则条件/动作的 `ValueId` 下拉框改为从 `warehouse_variable` 查询JOIN `base_device.DeviceId`
> **F2 提交点**: `Fix-P1: B4-batch+批量离线+凭据安全+前端地址+DeviceId映射+变量表`
---
## 阶段 F3: P2 改善项修复(预计 2h
#### F3.1 [P2-1] 适配器异常日志
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs`
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs`
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.Kms/KmsAdapter.cs`
- [ ] 所有 `catch { return false; }` → `catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck: {ex.Message}"); return false; }`
- [ ] `dotnet build` → 0 错误
#### F3.2 [P2-2] 规则引擎滞后窗
- [ ] 在 `warehouse_rulecondition` 表新增字段:
```sql
ALTER TABLE warehouse_rulecondition ADD
RecoveryThreshold_Numeric DECIMAL(18,2) NULL, -- 恢复阈值(下界)
RecoveryThreshold_Switch NVARCHAR(50) NULL; -- 恢复开关状态
```
- [ ] `RuleEngineService.EvaluateCondition` 中加逻辑:
```csharp
bool wasTriggered = cond.LastTriggered.HasValue;
if (wasTriggered)
return Compare(actualValue, "大于等于", cond.RecoveryThreshold_Numeric);
else
return Compare(actualValue, cond.CompareOperator, cond.TargetValue_Number);
```
#### F3.3 [P2-3] 条件级冷却
- [ ] `warehouse_rulecondition` 表新增 `LastTriggered DATETIME NULL`、`LastTriggerValue DECIMAL(18,2) NULL`
- [ ] `RuleEngineService.EvaluateCondition` 中:
- 如果 `DateTime.Now - cond.LastTriggered < rule.CooldownSec` → 跳过此条件
- 触发时更新 `LastTriggered` 和 `LastTriggerValue`
#### F3.4 [P2-4] 生产环境移除 console.log
- [ ] 编辑 `warehouse/vite.config.ts`
```typescript
build: {
terserOptions: { compress: { drop_console: true } }
}
```
- [ ] 开发环境保留 `console.log`(仅 build 时移除)
- [ ] `npm run build` → 确认无 console.log 残留
#### F3.5 [P2-5] 统一 gateway API 封装
- [ ] 复制 `warehouse/src/api/gateway.ts` → `web.vite/src/api/gateway.js`
- 修改 `GW_BASE` 为 `window.apiConfig.gatewayUrl || 'http://localhost:5100'`
- [ ] `web.vite/src/views/warehouse/device_manager/base_device.vue`
- 删除内联 `const GW =` + `fetch()` → 改为 `import { gwGet, gwPost } from '@/api/gateway.js'`
- 所有 `fetch(\`\${GW}/api/gateway/...\`)` → `gwGet(...)` / `gwPost(...)`
> **F3 提交点**: `Fix-P2: 异常日志+滞后窗+条件冷却+console清理+API统一`
---
## 阶段 F4: P3 优化项(预计 1h
#### F4.1 [P3-1] 规则引擎并发动作执行
- [ ] 在规则引擎实现方案的 `ExecuteActionsAsync` 中:
```csharp
var tasks = actions.Select(a => ExecuteSingleActionAsync(a, rule));
await Task.WhenAll(tasks);
async Task ExecuteSingleActionAsync(Action a, Rule r) {
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try { await DoAction(a, r, cts.Token); }
catch (OperationCanceledException) { Log($"[RuleEngine] 动作超时: {a.id}"); }
}
```
#### F4.2 [P3-2] 清理 bin/obj + .gitignore
- [ ] `.gitignore` 追加规则(如未在 F2.3 中完成):
```
**/bin/
**/obj/
gateway/src/IntegrationGateway.Host/bin/
api_sqlsugar/**/bin/
```
- [ ] `git rm -r --cached` 所有 bin/obj 目录
#### F4.3 [P3-3] 网关 Swagger
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`:
```csharp
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ...
app.UseSwagger();
app.UseSwaggerUI();
```
- [ ] 浏览器访问 `http://localhost:5100/swagger` 验证
#### F4.4 [P3-4] 同步设计文档路由数
- [ ] 编辑 `doc/设计文档/对接网关设计文档.md` → 路由表从 14 条更新为当前实际数
- [ ] 确认以下设计文档一致: 对接网关设计文档、规则引擎方案、KMS 设计文档
> **F4 提交点**: `Fix-P3: 并发动作+清理bin+Swagger+文档同步`
---
## 任务总览
| 编号 | 问题 | 涉及文件 | 预计 |
|:---:|------|------|:---:|
| P0-1 | RealtimePollJob 空壳 | RealtimePollJob.cs | 1h |
| P0-2 | A1 自注册 | Program.cs | 30min |
| P0-3 | B 组认证 | Program.cs + appsettings | 30min |
| P1-1 | B4-batch | Program.cs | 30min |
| P1-2 | 批量离线 | HeartbeatMonitorJob.cs | 20min |
| P1-3 | 凭据安全 | appsettings + .gitignore + bin | 20min |
| P1-4 | 前端地址 | base_device.vue + gateway.ts | 20min |
| P1-5 | DeviceId 映射 | RuleEngineService | 30min |
| P1-6 | 变量表 | SQL + 代码生成 + 前端 | 1h |
| P2-1 | 异常日志 | OwlAdapter + MC4Adapter + KmsAdapter | 20min |
| P2-2 | 滞后窗 | SQL + RuleEngineService | 30min |
| P2-3 | 条件冷却 | SQL + RuleEngineService | 20min |
| P2-4 | console 清理 | vite.config.ts | 10min |
| P2-5 | API 统一 | gateway.js + base_device.vue | 30min |
| P3-1 | 并发动作 | RuleEngineService | 15min |
| P3-2 | bin 清理 | .gitignore + git rm | 5min |
| P3-3 | Swagger | Program.cs | 10min |
| P3-4 | 文档同步 | 设计文档 | 15min |
> **总计**: 18 项 / 25 文件 / ~9h

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,218 @@
# warehouse 客户端改造 — 任务清单
> **基准文档**: 客户端改造方案 v1.0
> **分支**: gateway-dev
> **原则**: 改动仅在 warehouse/ 目录下,不影响 VolPro 管理端
---
## Phase W0: 基础设施(预计 0.5h
### W0.1 网关 API 封装
- [ ] 创建 `warehouse/src/api/gateway.ts`
- 实现 `gwGet(url)` — GET 请求网关
- 实现 `gwPost(url, body)` — POST 请求网关
- 网关基址常量 `GW_BASE = 'http://localhost:5100'`
### W0.2 设备数据模型
- [ ]`warehouse/src/api/gateway.ts` 中定义 TypeScript 接口:
- `Camera { id, name, location, status, adapterCode, streamUrl, hasPtz }`
- `StandardDevice { deviceId, adapterCode, sourceId, deviceName, deviceCategory, deviceGroup, isOnline, ... }`
- `StreamUrls { wsFlv, httpFlv, hls, ... }`
### W0.3 网关 CORS 确认
- [ ] 确认网关 `Program.cs` 中已启用 CORS`AllowAnyOrigin``AllowCredentials`
- [ ] 如未启用,在网关 `Program.cs` 中加 `builder.Services.AddCors()` + `app.UseCors()`
### W0.4 构建验证
- [ ] `npm run dev` 启动 warehouse确认无编译错误
> **W0 提交点**: `PhaseW0_infra — gateway.ts + 数据模型 + CORS 就绪`
---
## Phase W1: 实时视频页改造(预计 2h
### W1.1 获取真实摄像机列表
- [ ] 编辑 `warehouse/src/view/video/Live.vue`
- [ ] `onMounted` 中调用 `gwGet('/api/gateway/devices?adapter=Owl:main&page=1&size=100')`
- [ ]`StandardDevice[]` 映射到 `Camera[]`
- [ ] 替换硬编码的 5 台 Mock 摄像机
- [ ] 保留在线/离线统计计算逻辑
### W1.2 视频流播放
- [ ] 选中摄像机时调用 `gwGet('/api/gateway/streams/{adapterCode}/{sourceId}/live')`
- [ ] 获取 WS-FLV 地址后赋值给 `<video>` 标签的 `src`
- [ ] 替换当前占位 `<div class="video-placeholder">`
### W1.3 云台控制面板
- [ ] 在视频画面旁边增加云台方向键区域(↑↓←→ + ZOOM + 停止)
- [ ] 调用 `gwPost('/api/gateway/streams/{adapter}/{id}/ptz', { direction, action, speed })`
- [ ] mouseup/mouseleave 时发送 stop
### W1.4 构建验证
- [ ] 页面加载 → 显示真实 Owl 摄像机列表
- [ ] 点击摄像机 → 播放实时视频流
- [ ] 方向键 → Owl PTZ 响应
> **W1 提交点**: `PhaseW1_video_live — 真实摄像机列表 + WS-FLV播放 + 云台控制`
---
## Phase W2: 视频墙 + 录像回放(预计 2h
### W2.1 视频墙改造
- [ ] 编辑 `warehouse/src/view/video/VideoWall.vue`
- [ ] 页面加载时获取所有 Owl 摄像机列表
- [ ] 同时为前 N 路摄像机获取流地址
- [ ] 多路 `<video>` 标签并行播放(注意浏览器并发限制,建议 4 路)
### W2.2 录像回放改造
- [ ] 编辑 `warehouse/src/view/video/History.vue`
- [ ] 增加时间选择控件(开始时间 + 结束时间)
- [ ] 调用 `gwGet('/api/gateway/streams/{adapter}/{id}/playback?start=&end=')`
- [ ] 返回的 HLS 地址赋值给播放器
### W2.3 构建验证
- [ ] 视频墙同时显示 4 路画面
- [ ] 回放页时间轴选择 → HLS 播放
> **W2 提交点**: `PhaseW2_video_wall — 多路视频墙 + HLS回放`
---
## Phase W3: IoT 实时数据改造(预计 2h
### W3.1 IoT 设备列表
- [ ] 编辑 `warehouse/src/view/environment/EnvVarManagement.vue`
- [ ] 调用 `gwGet('/api/gateway/tree?adapter=MC4:31ku')` 获取对象树
- [ ] 递归遍历,筛选 `DeviceGroup=IoT设备` 的叶子节点
- [ ] 渲染为设备列表/表格
### W3.2 实时值轮询
- [ ] 对每个 IoT 设备调用 `gwGet('/api/gateway/realtime/{adapter}/{deviceId}')`
- [ ] 每 5s 轮询一次(`setInterval`
- [ ] 数据绑定到 ECharts 曲线图
- [ ] 数值异常时高亮显示
### W3.3 构建验证
- [ ] 环境页面显示 MC4.0 IoT 设备列表
- [ ] 选中设备 → 曲线图实时更新温度/湿度值
> **W3 提交点**: `PhaseW3_iot — IoT设备列表 + 5s轮询 + ECharts实时曲线`
---
## Phase W4: 设备详情页改造(预计 2h
### W4.1 设备类型识别
- [ ] 编辑 `warehouse/src/view/DeviceInfo.vue`
- [ ] 从 props 或全局状态获取当前选中设备(`StandardDevice`
- [ ] 根据 `DeviceCategory` + `DeviceGroup` 决定显示哪些选项卡
### W4.2 实时画面 Tab视频设备
- [ ] 视频设备:调 B6a 获取流地址 → `<video>` 播放
- [ ] 替代当前 `randomVideoImage` 占位图
### W4.3 实时曲线 TabIoT 设备)
- [ ] IoT 设备:调 B4 获取实时值 → ECharts 曲线
- [ ] 替代当前 Mock 数据生成的图表
### W4.4 设备控制 Tab
- [ ] 调 B5 发送控制指令
- [ ] 替代当前 Mock `handleTurnOn/handleTurnOff`
### W4.5 构建验证
- [ ] 地图点击设备 → 弹窗显示正确选项卡
- [ ] 视频设备显示真实画面
- [ ] IoT 设备显示实时曲线
> **W4 提交点**: `PhaseW4_device_info — 设备详情全选项卡对接真实数据`
---
## Phase W5: 报警页面对接(预计 2h
### W5.1 告警列表
- [ ] 编辑 `warehouse/src/view/emergency-alarm/EmergencyAlarmRecord.vue`
- [ ] 编辑 `warehouse/src/view/intrusion-alarm/AlarmRecord.vue`
- [ ] 页面加载时调用 `gwGet('/api/gateway/alarms/{adapter}?from=&to=&page=&size=')`
- [ ] 渲染告警表格(时间/等级/描述/状态)
### W5.2 告警确认 + 结束
- [ ] 增加"确认"和"结束"按钮
- [ ]`gwPost('/api/gateway/alarms/{adapter}/{alarmId}/confirm')`
- [ ]`gwPost('/api/gateway/alarms/{adapter}/{alarmId}/end')`
### W5.3 告警颜色映射
- [ ] 提示=蓝色、普通=黄色、重要=橙色、紧急=红色
### W5.4 告警联动地图
- [ ] 点击告警报文 → 地图定位到对应设备位置(通过 base_device.Location/Lat/Lng
### W5.5 构建验证
- [ ] 告警列表显示 MC4.0/Owl 真实告警
- [ ] 确认/结束按钮可用
- [ ] 点击告警 → 地图跳转
> **W5 提交点**: `PhaseW5_alarm — 真实告警列表 + 确认/结束 + 地图联动`
---
## Phase W6: 3D 地图设备标记(预计 3h
### W6.1 设备标记数据
- [ ] 编辑 `warehouse/src/view/DataView.vue`
- [ ] 初始化时调 VolPro API 获取 `base_device WHERE Enable=启用`
- [ ] 遍历设备,提取 `MapModelId``MapModelScale``MapModelRotation`
### W6.2 VgoMap 加载设备模型
- [ ] 调用 `window.$map.addModel({ modelId, position, scale, rotation })`API 需验证)
- [ ] 无 MapModelId 的设备用默认图标标记
- [ ] 在线设备绿色、离线设备灰色
### W6.3 点击标记 → 设备详情
- [ ] 监听 VgoMap 的 `click` 事件
- [ ] 命中设备标记时弹出 DeviceInfo.vue
### W6.4 告警设备闪烁
- [ ] 收到告警推送时,对应设备标记红色闪烁 10s
- [ ] 确认/结束告警后恢复
### W6.5 构建验证
- [ ] 3D 地图中显示设备模型或标记
- [ ] 点击标记弹出设备详情
- [ ] 告警设备红色闪烁
> **W6 提交点**: `PhaseW6_map — 3D地图设备标记 + 点击弹窗 + 告警闪烁`
---
## Phase W7: 联调 + 异常处理(预计 3h
### W7.1 异常兜底
- [ ] 网关不可达时显示"网关连接失败"提示
- [ ] 取流失败时显示"无法获取流地址"
- [ ] 网络超时 10s 后自动重试一次
### W7.2 限流保护
- [ ] IoT 轮询间隔 ≥ 5s
- [ ] 视频墙并发取流 ≤ 4 路
- [ ] 告警列表分页 ≤ 100 条/页
### W7.3 全链路联调
- [ ] 网关启动 → 管理端添加设备 → warehouse 大屏可见
- [ ] 视频实时播放 + 云台控制
- [ ] IoT 曲线实时更新
- [ ] 告警触发 → 大屏弹窗 + 地图闪烁
### W7.4 性能验证
- [ ] 大屏帧率 ≥ 30fps
- [ ] 视频延迟 ≤ 2s
- [ ] 页面首次加载 ≤ 5s
> **W7 提交点**: `PhaseW7_integration — 全链路联调通过 + 异常处理 + 限流保护`
---
> **总周期**: W0-W7 预计 16 小时(含 3h 联调)

View File

@@ -0,0 +1,251 @@
# warehouse 客户端改造方案 v1.0
> **版本**: 1.0
> **日期**: 2025-05-17
> **现状**: warehouse 大屏客户端使用硬编码 Mock 数据,尚未对接网关
> **目标**: 所有子系统(视频/IoT/告警/地图)通过 IntegrationGateway 获取真实数据
---
## 1. 仓库客户端现状分析
### 1.1 技术栈
| 层面 | 选型 |
|------|------|
| 框架 | Vue 3 + TypeScript + Vite |
| UI | Element Plus |
| 图表 | ECharts |
| 实时通信 | SignalR |
| 3D 地图 | VgoMap SDK |
| HTTP | Axios`api/http.js` |
| 状态管理 | Pinia + Vuex |
| API 基址 | `window.apiConfig.baseURL`(默认 `http://localhost:9100` |
### 1.2 页面清单与数据现状
| 页面 | 文件 | 当前数据源 | 需改造 |
|------|------|------|:---:|
| 3D 地图 | `view/Map.vue` | VgoMap SDK无设备标记 | ✅ |
| 数据大屏 | `view/DataView.vue` | VolPro API安全提示等+ Mock | ✅ |
| 实时视频 | `view/video/Live.vue` | **硬编码 5 个 Mock 摄像头** | ✅ |
| 视频墙 | `view/video/VideoWall.vue` | 需查看 | ❓ |
| 录像回放 | `view/video/History.vue` | 需查看 | ❓ |
| 设备详情 | `view/DeviceInfo.vue` | Props 传入 Mock 数据 | ✅ |
| 环境变量 | `view/environment/EnvVarManagement.vue` | 需查看 | ✅ |
| 紧急报警 | `view/emergency-alarm/EmergencyAlarmRecord.vue` | Mock / VolPro API | ✅ |
| 入侵报警 | `view/intrusion-alarm/AlarmRecord.vue` | Mock | ✅ |
| 门禁 | `view/access/AccessRecord.vue` | Mock | ⏭️ Phase 3 |
| 巡更 | `view/patrol/` | Mock | ⏭️ Phase 3 |
| 钥匙 | `view/key/` | Mock | ⏭️ Phase 3 |
| 车辆 | `view/carmanager/` | Mock | ⏭️ Phase 3 |
| 访客 | `view/visitor/` | Mock | ⏭️ Phase 3 |
| 无人机 | `view/drone/` | Mock | ⏭️ Phase 3 |
| 对讲 | `view/intercom/` | Mock | ⏭️ Phase 3 |
### 1.3 HTTP 层现状
`api/http.js` 封装了 Axios通过 `window.apiConfig.baseURL` 配置后端地址(默认 `http://localhost:9100`),支持 POST/GET 两种方式,自动附带 JWT Token。网关接口`:5100`)和 Vol.Pro 接口(`:9100`)同域还是跨域取决于部署架构。
---
## 2. 数据流改造
### 2.1 架构决策:直连网关 vs 经 Vol.Pro 中转
| 方案 | 路径 | 优点 | 缺点 |
|------|------|------|------|
| A. 直连网关 | warehouse → GW(:5100) | 延迟低,零额外开发 | 需网关暴露 CORS |
| B. 经 Vol.Pro 中转 | warehouse → VP(:9100) → GW(:5100) | 统一鉴权,网关不外露 | 延迟 +1 跳,需写 Controller |
**推荐方案 A**warehouse 客户端与大屏同一网络,网关开 CORS 即可。视频流和实时数据对延迟敏感,不宜多一跳。
### 2.2 服务路由总览
```
warehouse 客户端
├── Vol.Pro API (axios baseURL) → http://localhost:9100 (权限/字典/CRUD)
└── 网关 B 接口 (直接 fetch) → http://localhost:5100 (设备/视频/IoT/告警)
```
---
## 3. 逐页面改造方案
### 3.1 实时视频Live.vue
**现状**:硬编码 5 个 Mock 摄像机,无真实流
**改造**
1. 页面加载时调用网关 `GET /api/gateway/devices?adapter=Owl:main&page=1&size=100`
2. 摄像机列表从 `StandardDevice[]` 映射到 `Camera[]` 接口
3. 选中摄像机后调用 `GET /api/gateway/streams/{adapterCode}/{sourceId}/live` 获取 WS-FLV 地址
4.`<video>` 标签 `autoplay muted` 播放(替换占位 div
5. 增加云台方向控制面板(复用 `base_device.vue` 的云台 UI
6. 增加录像回放入口(调 B6b 获取 HLS 地址)
**改动量**~150 行
### 3.2 视频墙VideoWall.vue+ 回放History.vue
**现状**:需查看文件
**改造**
- 视频墙:多路视频同屏,从网关获取多个通道的流地址并行播放
- 回放:时间轴选择 → `GET /api/gateway/streams/{adapter}/{id}/playback?start=&end=`
**改动量**~100 行
### 3.3 环境变量EnvVarManagement.vue
**现状**:需查看文件,预期是表格/图表展示温湿度等
**改造**
1. 从网关获取 MC4 IoT 设备列表:`GET /api/gateway/tree?adapter=MC4:31ku`
2. 筛选 `DeviceGroup=IoT设备` 的设备
3. 轮询 `GET /api/gateway/realtime/{adapter}/{deviceId}` 获取实时值
4. 替换 Mock 数据,驱动 ECharts 曲线图实时更新
**改动量**~120 行
### 3.4 设备详情DeviceInfo.vue
**现状**:从 DataView.vue 接收 props展示设备基础信息 + 实时画面(占位图)+ 曲线Mock+ 控制按钮
**改造**
1. 设备类型判断从 mock type → `row.DeviceCategory` 字段
2. "实时画面" Tab — 视频设备时调网关 B6a 显示真实流
3. "实时曲线" Tab — IoT 设备时调网关 B4 获取实时值
4. "设备控制" Tab — 调网关 B5 写值
5. 在线率/达标率 — 从 `row.IsOnline` 和实时值计算
**改动量**~200 行
### 3.5 紧急报警 + 入侵报警
**现状**:部分调 VolPro API部分 Mock
**改造**
1. 告警列表从网关 `GET /api/gateway/alarms/{adapter}?from=&to=` 获取
2. 确认/结束告警 → `POST /api/gateway/alarms/{adapter}/{alarmId}/confirm`
3. 告警等级颜色映射(提示/普通/重要/紧急)
4. 点击告警行 → 联动地图定位到设备
**改动量**~100 行
### 3.6 3D 地图Map.vue / DataView.vue
**现状**VgoMap 加载 3D 模型,无设备标记
**改造**
1. DataView.vue 初始化时调 VolPro API 获取 `base_device WHERE Enable=启用` 的设备列表
2. 遍历设备,读取 `MapModelId``MapModelScale``MapModelRotation` 字段
3. 调用 VgoMap SDK 在 3D 场景中放置设备标记(`map.addMarker` 或模型加载 API
4. 设备在线状态 → 标记颜色(绿/灰)
5. 点击标记 → 弹出 DeviceInfo.vue 卡片
6. 告警设备 → 红色闪烁标记
**改动量**~150 行
---
## 4. 数据模型映射
### 4.1 StandardDevice → Camera
```typescript
interface Camera {
id: string // StandardDevice.SourceId
name: string // StandardDevice.Name
location: string // StandardDevice.Extra?.location || ''
status: string // StandardDevice.IsOnline ? 'online' : 'offline'
adapterCode: string // StandardDevice.AdapterCode (用于后续 API 调用)
streamUrl?: string // 通过 B6a 动态获取
hasPtz: boolean // StandardDevice.Extra?.hasPtz === '1'
}
```
### 4.2 StandardDevice → MapMarker
```typescript
interface MapMarker {
deviceId: number // StandardDevice.DeviceId
modelId: string // StandardDevice.MapModelId
scale: number // StandardDevice.MapModelScale
rotation: JSON // StandardDevice.MapModelRotation
isOnline: boolean // StandardDevice.IsOnline
category: string // StandardDevice.DeviceCategory
}
```
### 4.3 PointValue → ChartData
```typescript
interface ChartSeries {
name: string // `点位${pv.PointIndex}`
data: number[] // [pv.Value, ...] (时间序列)
timeLabels: string[] // [pv.UpdateTime, ...]
}
```
---
## 5. API 封装层
### 5.1 新增 `api/gateway.js`
```typescript
const GW_BASE = 'http://localhost:5100';
export async function gwGet(url: string) {
const resp = await fetch(`${GW_BASE}${url}`);
return resp.json();
}
export async function gwPost(url: string, body?: object) {
const resp = await fetch(`${GW_BASE}${url}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined
});
return resp.json();
}
```
### 5.2 现有 `api/http.js` 不修改
Vol.Pro 后端 API权限、字典、CRUD、安全提示继续走 `api/http.js`,不加网关逻辑。
---
## 6. 与 VolPro 管理端的职责分工
| 功能 | 管理端web.vite | 大屏端warehouse |
|------|:---:|:---:|
| 设备 CRUD | ✅ 设备管理页面 | ❌ |
| 网关注册/心跳 | ✅ A1-A4 | ❌ |
| 区域树管理 | ✅ GetRegionTree | ❌ |
| 实时视频播放 | ✅ 预览按钮 | ✅ 多路视频墙 |
| 云台控制 | ✅ 方向键 | ✅ 方向键 |
| 视频回放 | ✅ 回放按钮 | ✅ 时间轴回放 |
| IoT 实时曲线 | ❌ | ✅ ECharts 大屏 |
| 告警弹窗联动 | ❌ | ✅ 告警+地图+预案 |
| 3D 模型绑定 | ✅ 编辑面板 | ✅ 加载标记 |
---
## 7. 实施计划
| 阶段 | 内容 | 预计工时 |
|------|------|:---:|
| W1 | 新增 `api/gateway.js` + 实时视频页改造 | 2h |
| W2 | 视频墙 + 录像回放改造 | 2h |
| W3 | 环境变量 IoT 实时数据改造 | 2h |
| W4 | 设备详情页对接真实数据 | 2h |
| W5 | 报警页面对接网关告警 | 2h |
| W6 | 3D 地图加载设备标记 | 3h |
| W7 | 联调 + 异常处理 + 限流 | 3h |
---
> **风险**: 网关 CORS 配置、VgoMap SDK 设备标记 API 需要验证

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 个工作日(不含联调等待时间)

View File

@@ -0,0 +1,333 @@
# 规则引擎实现方案 v1.0
> **版本**: 1.0
> **日期**: 2025-05-24
> **基准**: 现有 warehouse_rule / warehouse_rulecondition / warehouse_ruleaction 表
> **目标**: 实现规则驱动的实时监测 → 条件比对 → 自动执行动作的完整闭环
---
## 1. 架构决策
### 1.1 部署位置:集成在 Vol.Pro 框架中 ✓
| 维度 | Vol.Pro 集成 | 网关集成 |
|------|:--:|:--:|
| 规则存储 | ✅ 规则表在本库 | ❌ 需从 Vol.Pro 下发 |
| 规则管理 UI | ✅ 已有完整 CRUD | ❌ 无管理界面 |
| 定时调度 | ✅ 已集成 Quartz | ❌ 无调度框架 |
| 前端推送 | ✅ SignalR Hub 已就绪 | ❌ 无推送能力 |
| 适配器数据源 | ⚠️ 需通过 B4 轮询 | ✅ 直接调用 |
| 状态维护 | ✅ 数据库持久化 | ❌ 网关无状态 |
| 实现复杂度 | ⭐⭐ | ⭐⭐⭐⭐ |
**结论**:规则引擎部署在 Vol.Pro 端,通过网关 B 组接口获取实时数据。与网关的 5~10 秒轮询延迟对于仓储环境规则(温湿度超标、人数越限)完全可接受。
### 1.2 核心设计思想
```
┌─────────────────────────────────────────────────────────────┐
│ Vol.Pro 规则引擎 │
│ │
│ Quartz RuleEngineJob (每10秒) │
│ │ │
│ ├─ 1. 加载启用的规则 (warehouse_rule WHERE Enable=1) │
│ ├─ 2. 按 AdapterCode 分组去重设备列表 │
│ ├─ 3. 调网关 B4 批量获取实时值 │
│ ├─ 4. 逐规则评估条件 (AND/OR 组合) │
│ └─ 5. 条件匹配 → 执行动作链 │
│ ├── 控制设备 → 网关 B5/B10 │
│ ├── 生成告警 → iot_alarm 表 │
│ └── 推送前端 → SignalR Hub │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. 数据库改动
### 2.1 warehouse_rule 表新增字段
```sql
ALTER TABLE warehouse_rule ADD
Enable NVARCHAR(50) DEFAULT '启用', -- 启用/停用
Priority INT DEFAULT 0, -- 优先级(数字越大越优先)
LastEvaluated DATETIME NULL, -- 上次评估时间
LastTriggered DATETIME NULL, -- 上次触发时间
CooldownSec INT DEFAULT 60; -- 冷却时间(秒,防止重复触发)
```
### 2.2 warehouse_ruleaction 表确认字段
当前已含 `Alert` (生成告警/是/否) 和 `AlertMessage` (告警内容),需补充:
```sql
ALTER TABLE warehouse_ruleaction ADD
ActionType NVARCHAR(255) DEFAULT '控制', -- 动作类型: 控制/告警/通知
ExtraJson NVARCHAR(MAX) NULL; -- 扩展JSON(如控制指令参数)
```
### 2.3 新增规则执行日志表
```sql
CREATE TABLE warehouse_rulelog (
LogID INT IDENTITY PRIMARY KEY,
RuleID INT NOT NULL, -- 关联 warehouse_rule
ConditionMet NVARCHAR(50), -- 条件是否满足(满足/不满足)
ActionResult NVARCHAR(MAX), -- 动作执行结果JSON
EvaluatedAt DATETIME DEFAULT GETDATE(), -- 评估时间
Detail NVARCHAR(MAX) NULL -- 执行详情
);
```
---
## 3. 规则引擎核心设计
### 3.1 规则评估流程
```csharp
/// <summary>
/// 规则引擎核心服务。由 Quartz RuleEngineJob 每 10s 调用一次。
/// 顺序:
/// 1. 加载所有启用规则(含条件和动作)
/// 2. 从 gateway 批量获取实时值
/// 3. 逐规则评估条件 → 触发动作 → 写日志
/// </summary>
public class RuleEngineService
{
// 注入
private readonly IHttpClientFactory _httpClient;
private readonly ISignalRHub _hub; // 前端推送
private readonly Iwarehouse_ruleRepository _ruleRepo;
public async Task EvaluateAllAsync()
{
var rules = await LoadEnabledRulesAsync(); // 1. 加载规则
var adapters = rules.SelectMany(r => r.AdapterCodes).Distinct();
var realtimeData = await BatchFetchRealtimeAsync(adapters); // 2. 批量取实时值
foreach (var rule in rules)
{
if (await EvaluateRuleAsync(rule, realtimeData)) // 3. 评估条件
{
await ExecuteActionsAsync(rule); // 4. 执行动作
}
await LogEvaluationAsync(rule); // 5. 写日志
}
}
}
```
### 3.2 条件评估模型
```csharp
/// <summary>评估单条规则的所有条件</summary>
async Task<bool> EvaluateRuleAsync(Rule rule, Dictionary<string, List<PointValue>> realtimeData)
{
// 从 realtimeData 中查找每个条件的实际值
var results = new List<bool>();
foreach (var cond in rule.Conditions)
{
var actualValue = FindValue(realtimeData, cond.DeviceId, cond.ValueId);
bool met = Compare(actualValue, cond.CompareOperator, cond.TargetValue);
results.Add(met);
}
// 按 JudgmentMode 组合条件结果
return rule.JudgmentMode == "AND"
? results.All(r => r)
: results.Any(r => r);
}
double? FindValue(Dictionary<...> data, int deviceId, int valueId)
{
// 从批量取回的数据中定位对应设备+变量的实时值
// 映射: valueId → 适配器点位索引
}
bool Compare(double? actual, string op, double target) => op switch
{
"大于" => (actual ?? double.MinValue) > target,
"小于" => (actual ?? double.MaxValue) < target,
"等于" => actual == target,
"大于等于" => (actual ?? double.MinValue) >= target,
"小于等于" => (actual ?? double.MaxValue) <= target,
"不等于" => actual != target,
_ => false
};
```
### 3.3 动作执行模型
```csharp
/// <summary>按优先级执行规则的所有动作</summary>
async Task ExecuteActionsAsync(Rule rule)
{
// 冷却检查:防止重复触发
if (rule.LastTriggered.HasValue &&
(DateTime.Now - rule.LastTriggered.Value).TotalSeconds < rule.CooldownSec)
return;
foreach (var action in rule.Actions.OrderByDescending(a => a.Priority))
{
switch (action.Type)
{
case "控制":
// 调网关 B5 或 B10 向目标设备发控制指令
// 例如: POST /api/gateway/realtime/MC4:31ku/control { deviceId, pointIndex, value }
await ControlDeviceAsync(action);
break;
case "告警":
if (action.Alert == "是")
{
// 写入 iot_alarm 表
await CreateAlarmAsync(rule, action);
}
break;
case "通知":
// 通过 SignalR 推送前端弹窗
await _hub.SendAsync("RuleTriggered", new { rule.Title, action.AlertMessage });
break;
}
}
rule.LastTriggered = DateTime.Now;
}
```
---
## 4. 定时调度
### 4.1 新增 RuleEngineJob
```csharp
/// <summary>规则引擎定时任务。挂载到 Vol.Pro Quartz 调度。</summary>
public class RuleEngineJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var engine = ServiceProvider.GetService<RuleEngineService>();
await engine.EvaluateAllAsync();
}
}
```
### 4.2 Quartz 配置
在 Vol.Pro 管理端 → Quartz 管理 → 新建 Job
```
JobName: RuleEngineJob
Cron: 0/10 * * * * ? (每 10 秒)
ClassName: Warehouse.Services.RuleEngineJob
```
---
## 5. 数据字典补充
| 字典键 | 字典值 | 用途 |
|------|------|------|
| 条件判断方式 | AND / OR | warehouse_rule.JudgmentMode |
| 比较运算 | 大于 / 小于 / 等于 / 大于等于 / 小于等于 / 不等于 | warehouse_rulecondition.CompareOperator |
| 比对类型 | 数值 / 开关状态 / 字符串 | warehouse_rulecondition.Type |
| 开关状态 | 开 / 关 | TargetValue_Switch |
| 动作类型 | 控制 / 告警 / 通知 | warehouse_ruleaction.ActionType |
---
## 6. 前端改动
### 6.1 规则管理页增强
基于现有 `warehouse_rule.vue`(已在管理端运行),无需重建页面。仅优化表单绑定:
- 条件表格中"设备"列绑定到 `allDevices` 动态字典
- "变量"列绑定到对应变量的动态字典
- 动作表格中"生成告警"列改为 select(是/否)
- 新增 `ExtraJson` 字段(高级模式)提供 JSON 编辑器
### 6.2 前端告警接收
```javascript
// warehouse 大屏端 - SignalR 订阅规则推送
connection.on("RuleTriggered", (data) => {
showMessage({
title: data.title,
type: "alarm",
content: data.alertMessage
});
});
```
---
## 7. Gateway 端配套
### 7.1 现有接口即用
| 规则需求 | 网关接口 | 状态 |
|------|------|:--:|
| 获取 MC4 IoT 实时值 | `GET /api/gateway/realtime/{adapter}/{deviceId}` (B4) | ✅ 已实现 |
| 向 MC4 设备发控制指令 | `POST /api/gateway/realtime/{adapter}/control` (B5) | ✅ 已实现 |
| 获取 Owl 人数统计 | `GET /api/gateway/devices?adapter=Owl:main` (B2) | ✅ 已实现 |
| 远程开门/道闸 | `POST /api/gateway/control/{adapter}` (B10) | ✅ 已实现 |
### 7.2 新增:批量实时值查询(可选优化)
当前 B4 需要逐设备调M 条规则 × N 个条件会导致过多 HTTP 调用。建议新增批量接口:
```
POST /api/gateway/realtime/{adapter}/batch
Body: { "deviceIds": ["sid1", "sid2", ...] }
Return: { "sid1": [{pointIndex, value}], "sid2": [{...}] }
```
此接口为**可选优化**。初期可直接逐设备调 B4每设备 ~100ms10 设备 = 1s可接受
---
## 8. 实施计划
| 阶段 | 内容 | 涉及文件 | 预计 |
|:---:|------|------|:---:|
| R1 | 补充数据库表字段 + 新增 warehouse_rulelog 表 + 建字典 | SQL + 管理端 | 1h |
| R2 | 实现 RuleEngineService (规则评估 + 动作执行) | 1 个新文件 | 3h |
| R3 | 实现 RuleEngineJob + Quartz 注册 | 1 文件 + 管理端配置 | 30min |
| R4 | 网关批量实时值接口 (B4-batch) | Program.cs | 30min |
| R5 | 规则管理页 UI 增强(动态字典绑定) | warehouse_rule.vue | 1h |
| R6 | warehouse 大屏端 SignalR 规则推送接收 | warehouse DataView.vue | 30min |
| R7 | 联调验证 | — | 2h |
| **合计** | — | **8 文件** | **~8.5h** |
---
## 9. 规则示例
**示例 1温湿度超标自动开空调**
```
规则标题: 库房31温度过高自动开空调
判断方式: AND
条件:
- 设备: MC4:31ku/温湿度变送器1 | 变量: 温度 | 比较: 大于 | 目标值: 28
动作:
- 设备: MC4:31ku/空调控制器1 | 动作类型: 控制 | 目标值开关: 开
```
**示例 2摄像机人数越限告警**
```
规则标题: 仓库A区人数超限告警
判断方式: AND
条件:
- 设备: Owl:main/摄像头1 | 变量: 人数统计 | 比较: 大于等于 | 目标值: 50
动作:
- 动作类型: 告警 | 生成告警: 是 | 告警内容: "仓库A区人数已达{value}人,请立即检查"
```
---
> **结论**: 规则引擎作为 Vol.Pro 内部 Quartz Job 运行,每 10s 拉网关数据评估零网关状态改造。8 个文件 ~8.5h 可完成闭环。

View File

@@ -1,54 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Host", "src\IntegrationGateway.Host\IntegrationGateway.Host.csproj", "{8F605B6B-5217-4119-A75E-05FFB4E42347}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationGateway.Core", "src\IntegrationGateway.Core\IntegrationGateway.Core.csproj", "{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x64.ActiveCfg = Debug|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x64.Build.0 = Debug|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x86.ActiveCfg = Debug|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Debug|x86.Build.0 = Debug|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|Any CPU.Build.0 = Release|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x64.ActiveCfg = Release|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x64.Build.0 = Release|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x86.ActiveCfg = Release|Any CPU
{8F605B6B-5217-4119-A75E-05FFB4E42347}.Release|x86.Build.0 = Release|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x64.ActiveCfg = Debug|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x64.Build.0 = Debug|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x86.ActiveCfg = Debug|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Debug|x86.Build.0 = Debug|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|Any CPU.Build.0 = Release|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x64.ActiveCfg = Release|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x64.Build.0 = Release|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x86.ActiveCfg = Release|Any CPU
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8F605B6B-5217-4119-A75E-05FFB4E42347} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{D1F85A10-E56A-44E8-96B8-7BC3C91E513B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,9 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/IntegrationGateway.Adapters.Kms/IntegrationGateway.Adapters.Kms.csproj" />
<Project Path="src/IntegrationGateway.Adapters.MC4/IntegrationGateway.Adapters.MC4.csproj" />
<Project Path="src/IntegrationGateway.Adapters.Owl/IntegrationGateway.Adapters.Owl.csproj" />
</Folder>
<Project Path="src/IntegrationGateway.Core/IntegrationGateway.Core.csproj" />
<Project Path="src/IntegrationGateway.Host/IntegrationGateway.Host.csproj" />
</Solution>

8
gateway/NuGet.Config Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="nuget.org_v2" value="https://www.nuget.org/api/v2/" />
</packageSources>
</configuration>

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\IntegrationGateway.Core\IntegrationGateway.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,337 @@
using IntegrationGateway.Core.Abstractions;
using IntegrationGateway.Core.Infrastructure;
using IntegrationGateway.Core.Models;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
namespace IntegrationGateway.Adapters.Kms;
/// <summary>
/// KMS 智能钥匙柜适配器。
/// 实现: IHasFlatDevices + IHasAlarms。
///
/// 设备模型:柜体为父设备(IsParent=是),锁孔为子设备(ParentSourceId=柜体SourceId)。
/// AdapterCode: "KMS:{InstanceName}"。
/// 限流5 QPS。
///
/// 按设计文档 §6 KmsAdapter 完整实现。
/// </summary>
public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusinessLogs, IAcceptsDataSync
{
private readonly HttpClient _http;
private readonly KmsAuthHelper _auth;
private readonly RateLimiter _limiter = new(5);
/// <summary>适配器编码,格式 "KMS:{实例名}"</summary>
public string AdapterCode { get; }
/// <summary>人类可读的适配器名称</summary>
public string DisplayName => $"KMS ({AdapterCode})";
/// <summary>适配器能力声明</summary>
public AdapterCapabilities Capabilities => new() { HasFlatDevices = true, HasAlarms = true };
/// <summary>创建 KmsAdapter 实例</summary>
/// <param name="adapterCode">适配器编码</param>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">KMS 服务地址</param>
/// <param name="clientId">KMS 客户端 ID</param>
/// <param name="clientSecret">KMS 客户端密钥</param>
public KmsAdapter(string adapterCode, HttpClient http, string baseUrl, string clientId, string clientSecret)
{
AdapterCode = adapterCode;
_http = http;
_auth = new KmsAuthHelper(http, baseUrl, clientId, clientSecret);
}
/// <summary>初始化适配器:获取 KMS Token</summary>
public async Task InitializeAsync() => await _auth.GetTokenAsync();
// ═══════════════════════════════════════════
// IGatewayAdapter — 健康检查2.18.1 心跳)
// ═══════════════════════════════════════════
/// <summary>调用 KMS 心跳接口确认可达性</summary>
public async Task<bool> HealthCheckAsync()
{
try
{
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.GetAsync("/prod-api/heartBeat");
return resp.IsSuccessStatusCode;
}
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
}
// ═══════════════════════════════════════════
// IHasFlatDevices — 设备列表2.18.4 柜体+钥匙)
// ═══════════════════════════════════════════
/// <summary>
/// 获取 KMS 所有柜体及其锁孔,映射为 StandardDevice 列表。
/// 柜体为父设备IsParent=是锁孔为子设备ParentSourceId=柜体SourceId
/// </summary>
public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync("/prod-api/getOpenerList",
new StringContent("{}", Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var data = await resp.Content.ReadFromJsonAsync<KmsOpenerListResponse>()!;
var devices = new List<StandardDevice>();
foreach (var locker in data.Rows ?? new())
{
devices.Add(MapLockerToDevice(locker));
if (locker.LockholeList != null)
devices.AddRange(locker.LockholeList.Select(h => MapLockholeToDevice(h, locker.LockerId)));
}
return new PagedResult<StandardDevice> { Items = devices, Total = devices.Count };
}
/// <summary>KMS 柜体 → StandardDevice父设备</summary>
private static StandardDevice MapLockerToDevice(KmsLocker locker) => new()
{
SourceId = $"locker_{locker.LockerId}",
Name = locker.LockerName ?? $"柜体{locker.LockerId}",
Category = "智能钥匙柜",
Group = "门禁设备",
IsParent = true,
IsOnline = true,
Extra = new Dictionary<string, object?>
{
["lockerCode"] = locker.LockerCode,
["lockholeCount"] = locker.LockholeList?.Count ?? 0
}
};
/// <summary>KMS 锁孔 → StandardDevice子设备</summary>
private static StandardDevice MapLockholeToDevice(KmsLockhole hole, int lockerId) => new()
{
SourceId = $"lockhole_{lockerId}_{hole.LockholeSort}",
Name = hole.OpenerName ?? $"锁孔{hole.LockholeSort}",
Category = "钥匙位",
Group = "门禁设备",
IsParent = false,
IsOnline = hole.OpenerState == "在位",
ParentSourceId = $"locker_{lockerId}",
Extra = new Dictionary<string, object?>
{
["openerId"] = hole.OpenerId,
["openerType"] = hole.OpenerType,
["openerState"] = hole.OpenerState
}
};
// ═══════════════════════════════════════════
// IHasAlarms — 告警2.18.7 告警列表)
// ═══════════════════════════════════════════
/// <summary>分页查询 KMS 告警列表,映射到 StandardAlarm</summary>
public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(
int page, int size, DateTime from, DateTime to, string? level = null, string? state = null)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync("/prod-api/getWarningList",
new StringContent("{}", Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var data = await resp.Content.ReadFromJsonAsync<KmsWarningListResponse>()!;
var alarms = (data.Rows ?? new()).Select(w => new StandardAlarm
{
AlarmId = w.Uuid ?? "",
AdapterCode = AdapterCode,
Level = "普通", // KMS 不区分告警等级,统一"普通"
Title = $"{w.LockerName} 锁孔{w.LockholeSort}: {w.OpenerName}",
Content = w.Remark,
OccurTime = DateTime.TryParse(w.WarningTime, out var t) ? t : DateTime.MinValue,
Status = w.Type == 1 ? "未确认" : "已结束"
}).ToList();
return new PagedResult<StandardAlarm> { Items = alarms, Total = data.Total };
}
/// <summary>确认告警(调 KMS 标准告警确认接口)</summary>
public async Task ConfirmAlarmAsync(string alarmId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
await client.PostAsync($"/prod-api/kms/warning/confirm/{alarmId}", null);
}
/// <summary>结束告警KMS 第三方接口不提供,留空实现)</summary>
public Task EndAlarmAsync(string alarmId)
{
// KMS 第三方接口 (2.18.7) 不提供告警结束 API
return Task.CompletedTask;
}
// ═══════════════════════════════════════════
// 扩展方法 — 2.18 第三方接口全覆盖
// ═══════════════════════════════════════════
/// <summary>2.18.6 查询借还记录列表</summary>
public async Task<PagedResult<KmsRecord>> GetBorrowRecordsAsync(DateTime? from = null, DateTime? to = null)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = "{}"; // 联调时加入时间范围参数
var resp = await client.PostAsync("/prod-api/getRecordList",
new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var data = await resp.Content.ReadFromJsonAsync<KmsRecordListResponse>()!;
return new PagedResult<KmsRecord> { Items = data.Rows ?? new(), Total = data.Total };
}
/// <summary>2.18.5 查询授权记录列表</summary>
public async Task<PagedResult<KmsPermission>> GetPermissionListAsync(DateTime? from = null, DateTime? to = null)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = "{}"; // 联调时加入时间范围
var resp = await client.PostAsync("/prod-api/getPermissionList",
new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var data = await resp.Content.ReadFromJsonAsync<KmsPermissionListResponse>()!;
return new PagedResult<KmsPermission> { Items = data.Rows ?? new(), Total = data.Total };
}
/// <summary>2.18.3 从 Vol.Pro 向 KMS 批量同步员工</summary>
public async Task BatchSyncStaffAsync(List<KmsStaff> staffList)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsJsonAsync("/prod-api/batchSyncStaff", new { staff = staffList });
resp.EnsureSuccessStatusCode();
}
/// <summary>2.18.2 从 Vol.Pro 向 KMS 批量删除员工</summary>
public async Task BatchDeleteStaffAsync(List<string> staffUuids)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsJsonAsync("/prod-api/batchDeleteStaff", staffUuids);
resp.EnsureSuccessStatusCode();
}
/// <summary>2.4.3 远程授权开门</summary>
public async Task RemoteAuthorizeAsync(KmsRemotePermissionRequest request)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsJsonAsync("/prod-api/kms/permission/remote", request);
resp.EnsureSuccessStatusCode();
}
/// <summary>2.18.8 代理 KMS 第三方登录/事件记录</summary>
public async Task<string?> ThirdPlatLoginAsync(string username)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync($"/thirdPlatlogin?username={Uri.EscapeDataString(username)}", null);
if (resp.StatusCode == System.Net.HttpStatusCode.Redirect)
return resp.Headers.Location?.ToString();
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsStringAsync();
}
// ═══════════════════════════════════════════
// IAcceptsControl — 设备控制(远程开门)
// ═══════════════════════════════════════════
/// <summary>向设备下发控制指令(如远程开门)</summary>
public async Task<ControlResult> SendControlAsync(string sourceDeviceId, string command, Dictionary<string, object?> parameters)
{
await _limiter.WaitAsync();
try
{
if (command == "open" || command == "authorize")
{
var req = new KmsRemotePermissionRequest
{
StaffIds = parameters.TryGetValue("staffIds", out var s) && s is List<int> sl ? sl : null,
OpenerIds = parameters.TryGetValue("lockholeSort", out var lh) ? new List<int> { (int)(long)lh! } : null,
Type = command == "authorize" ? 2 : 1
};
await RemoteAuthorizeAsync(req);
}
return new ControlResult { Success = true };
}
catch (Exception ex)
{
return new ControlResult { Success = false, Message = ex.Message };
}
}
// ═══════════════════════════════════════════
// IHasBusinessLogs — 业务记录查询
// ═══════════════════════════════════════════
/// <summary>按类型查询业务记录</summary>
public async Task<PagedResult<BusinessLogEntry>> GetBusinessLogsAsync(
string logType, DateTime? from, DateTime? to, int page, int size, Dictionary<string, string>? filters = null)
{
if (logType == "borrow" || logType == "handover")
{
var records = await GetBorrowRecordsAsync(from, to);
return new PagedResult<BusinessLogEntry>
{
Items = records.Items.Select(r => new BusinessLogEntry
{
LogId = r.Uuid ?? "", LogType = logType,
DeviceSourceId = $"lockhole_{r.LockerName}_{r.LockholeSort}",
StaffName = r.StaffName, Description = r.OpenerName,
CreatedAt = DateTime.TryParse(r.BorrowTime, out var bt) ? bt : null
}).ToList(),
Total = records.Total
};
}
if (logType == "permission")
{
var perms = await GetPermissionListAsync(from, to);
return new PagedResult<BusinessLogEntry>
{
Items = perms.Items.Select(p => new BusinessLogEntry
{
LogId = p.Uuid ?? "", LogType = "permission",
StaffName = p.LendStaffName, Description = p.OpenerCnName,
CreatedAt = DateTime.TryParse(p.ApplyTime, out var at) ? at : null
}).ToList(),
Total = perms.Total
};
}
return new PagedResult<BusinessLogEntry> { Items = new(), Total = 0 };
}
// ═══════════════════════════════════════════
// IAcceptsDataSync — 数据同步写入
// ═══════════════════════════════════════════
/// <summary>向 KMS 批量同步数据</summary>
public async Task<SyncResult> SyncDataAsync(string dataType, List<object> items)
{
if (dataType != "staff") return new SyncResult { SuccessCount = 0, FailCount = 0, Message = $"不支持的数据类型: {dataType}" };
try
{
var staffList = items.Cast<KmsStaff>().ToList();
await BatchSyncStaffAsync(staffList);
return new SyncResult { SuccessCount = staffList.Count };
}
catch (Exception ex) { return new SyncResult { FailCount = items.Count, Message = ex.Message }; }
}
/// <summary>从 KMS 批量删除数据</summary>
public async Task<SyncResult> DeleteDataAsync(string dataType, List<string> ids)
{
if (dataType != "staff") return new SyncResult { SuccessCount = 0, FailCount = 0, Message = $"不支持的数据类型: {dataType}" };
try
{
await BatchDeleteStaffAsync(ids);
return new SyncResult { SuccessCount = ids.Count };
}
catch (Exception ex) { return new SyncResult { FailCount = ids.Count, Message = ex.Message }; }
}
}

View File

@@ -0,0 +1,70 @@
using System.Net.Http.Json;
using System.Text.Json;
namespace IntegrationGateway.Adapters.Kms;
/// <summary>
/// KMS Bearer Token 认证辅助。
/// 认证流程: POST /prod-api/getToken?clientId=x&clientSecret=y → { code:200, token:"xxx" }
/// Token 缓存 25 分钟KMS 有效期 30 分钟,留 5 分钟余量)。
/// </summary>
public class KmsAuthHelper
{
private readonly HttpClient _http;
private readonly string _baseUrl;
private readonly string _clientId;
private readonly string _clientSecret;
private string? _token;
private DateTime _tokenExpiry = DateTime.MinValue;
/// <summary>
/// 创建 KMS 认证辅助
/// </summary>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">KMS 服务地址</param>
/// <param name="clientId">KMS 客户端 ID</param>
/// <param name="clientSecret">KMS 客户端密钥</param>
public KmsAuthHelper(HttpClient http, string baseUrl, string clientId, string clientSecret)
{
_http = http;
_baseUrl = baseUrl.TrimEnd('/');
_clientId = clientId;
_clientSecret = clientSecret;
}
/// <summary>
/// 获取有效的 Bearer Token。缓存有效则直接返回否则重新获取。
/// </summary>
public async Task<string> GetTokenAsync()
{
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry)
return _token;
var url = $"{_baseUrl}/prod-api/getToken?clientId={Uri.EscapeDataString(_clientId)}&clientSecret={Uri.EscapeDataString(_clientSecret)}";
var resp = await _http.PostAsync(url, null);
resp.EnsureSuccessStatusCode();
var result = await resp.Content.ReadFromJsonAsync<KmsTokenResponse>()
?? throw new Exception("KMS Token 响应为空");
if (result.Code != 200)
throw new Exception($"KMS 认证失败: code={result.Code}");
_token = result.Token;
_tokenExpiry = DateTime.UtcNow.AddMinutes(25);
return _token;
}
/// <summary>
/// 创建一个已认证的 HttpClient自动附带 Authorization: Bearer 头。
/// </summary>
public async Task<HttpClient> GetAuthenticatedClientAsync()
{
var token = await GetTokenAsync();
var client = new HttpClient { BaseAddress = new Uri(_baseUrl) };
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
return client;
}
/// <summary>强制清除缓存的 Token下次调用 GetTokenAsync 将重新登录</summary>
public void Invalidate() => _token = null;
}

View File

@@ -0,0 +1,254 @@
/// <summary>
/// KMS 钥匙柜 API 响应模型。
/// 按设计文档 §4 KmsModels 完整定义,覆盖全部 38 个 KMS 接口。
/// </summary>
namespace IntegrationGateway.Adapters.Kms;
// ═══════════════════════════════════════════
// 认证
// ═══════════════════════════════════════════
/// <summary>POST /prod-api/getToken 响应</summary>
public class KmsTokenResponse
{
public int Code { get; set; }
public string Token { get; set; } = "";
public string? Msg { get; set; }
}
// ═══════════════════════════════════════════
// 2.18 第三方接口 DTO
// ═══════════════════════════════════════════
/// <summary>2.18.4 POST /prod-api/getOpenerList 响应</summary>
public class KmsOpenerListResponse
{
public int Code { get; set; }
public string? Msg { get; set; }
public List<KmsLocker>? Rows { get; set; }
}
/// <summary>KMS 柜体</summary>
public class KmsLocker
{
public int LockerId { get; set; }
public string? LockerName { get; set; }
public string? LockerCode { get; set; }
public List<KmsLockhole>? LockholeList { get; set; }
}
/// <summary>KMS 锁孔(含钥匙信息)</summary>
public class KmsLockhole
{
public int LockholeSort { get; set; }
public int OpenerId { get; set; }
public string? OpenerName { get; set; }
public string? OpenerType { get; set; }
public string? OpenerState { get; set; }
}
/// <summary>2.18.7 POST /prod-api/getWarningList 响应</summary>
public class KmsWarningListResponse
{
public int Code { get; set; }
public string? Msg { get; set; }
public int Total { get; set; }
public List<KmsWarning>? Rows { get; set; }
}
/// <summary>KMS 告警条目</summary>
public class KmsWarning
{
public string? Uuid { get; set; }
public string? LockerName { get; set; }
public int LockholeSort { get; set; }
public string? OpenerName { get; set; }
public int Type { get; set; }
public string? WarningTime { get; set; }
public string? Remark { get; set; }
public string? StaffName { get; set; }
}
/// <summary>2.18.6 POST /prod-api/getRecordList 响应</summary>
public class KmsRecordListResponse
{
public int Code { get; set; }
public string? Msg { get; set; }
public int Total { get; set; }
public List<KmsRecord>? Rows { get; set; }
}
/// <summary>KMS 借还记录</summary>
public class KmsRecord
{
public string? Uuid { get; set; }
public string? LockerName { get; set; }
public int LockholeSort { get; set; }
public string? OpenerName { get; set; }
public string? StaffName { get; set; }
public string? BorrowTime { get; set; }
public string? ReturnTime { get; set; }
public string? Type { get; set; }
}
// ═══════════════════════════════════════════
// 2.3-2.17 标准接口 DTO
// ═══════════════════════════════════════════
/// <summary>KMS 交接记录</summary>
public class KmsHandoverInfo
{
public string? Id { get; set; }
public string? HandoverId { get; set; }
public int OpenerId { get; set; }
public string? OpenerName { get; set; }
public int LockerId { get; set; }
public string? LockerName { get; set; }
public int LockholeSort { get; set; }
public string? OpenerType { get; set; }
public string? OpenerState { get; set; }
public string? LendStaffName { get; set; }
public string? BorrowTime { get; set; }
}
/// <summary>KMS 授权记录列表响应</summary>
public class KmsPermissionListResponse
{
public int Code { get; set; }
public int Total { get; set; }
public List<KmsPermission>? Rows { get; set; }
}
/// <summary>KMS 授权记录</summary>
public class KmsPermission
{
public string? Uuid { get; set; }
public string? LockerName { get; set; }
public string? OpenerCnName { get; set; }
public string? LendStaffName { get; set; }
public string? BackStaffName { get; set; }
public string? ApplyTime { get; set; }
public string? BackTime { get; set; }
}
/// <summary>KMS 员工列表响应</summary>
public class KmsStaffListResponse
{
public int Code { get; set; }
public int Total { get; set; }
public List<KmsStaff>? Rows { get; set; }
}
/// <summary>KMS 员工</summary>
public class KmsStaff
{
public string? Uuid { get; set; }
public string? Name { get; set; }
public string? CardNo { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public int? DeptId { get; set; }
public int? GroupId { get; set; }
public int State { get; set; }
public int Type { get; set; }
}
/// <summary>KMS 柜体列表响应</summary>
public class KmsLockerListResponse
{
public int Code { get; set; }
public int Total { get; set; }
public List<KmsLockerInfo>? Rows { get; set; }
}
/// <summary>KMS 柜体详细信息</summary>
public class KmsLockerInfo
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Code { get; set; }
public int State { get; set; }
public int? DeptId { get; set; }
public List<KmsLockhole>? LockholeList { get; set; }
}
/// <summary>KMS 锁孔列表响应</summary>
public class KmsLockholeListResponse
{
public int Code { get; set; }
public int Total { get; set; }
public List<KmsLockholeInfo>? Rows { get; set; }
}
/// <summary>KMS 锁孔详细信息</summary>
public class KmsLockholeInfo
{
public int Id { get; set; }
public int LockerId { get; set; }
public int LockholeSort { get; set; }
public int State { get; set; }
public int? OpenerId { get; set; }
}
/// <summary>KMS 钥匙列表响应</summary>
public class KmsOpenerListResponse2
{
public int Code { get; set; }
public int Total { get; set; }
public List<KmsOpenerInfo>? Rows { get; set; }
}
/// <summary>KMS 钥匙详细信息</summary>
public class KmsOpenerInfo
{
public int Id { get; set; }
public string? CnName { get; set; }
public string? Number { get; set; }
public int Type { get; set; }
public int State { get; set; }
public int? LockerId { get; set; }
}
/// <summary>KMS 员工可借钥匙列表响应</summary>
public class KmsStaffOpenerListResponse
{
public int Code { get; set; }
public List<KmsStaffOpener>? Data { get; set; }
}
/// <summary>KMS 员工可借钥匙</summary>
public class KmsStaffOpener
{
public int Id { get; set; }
public int StaffId { get; set; }
public int OpenerId { get; set; }
public int Type { get; set; }
}
/// <summary>2.4.3 远程授权请求(联调时确认字段)</summary>
public class KmsRemotePermissionRequest
{
public List<int>? StaffIds { get; set; }
public List<int>? OpenerIds { get; set; }
public int Type { get; set; }
}
/// <summary>2.18.3 批量同步员工请求</summary>
public class KmsBatchSyncStaffRequest
{
public List<KmsStaff> Staff { get; set; } = new();
}
// ═══════════════════════════════════════════
// 通用包装
// ═══════════════════════════════════════════
/// <summary>KMS 通用分页响应</summary>
public class KmsApiResponse<T>
{
public int Code { get; set; }
public string? Msg { get; set; }
public int Total { get; set; }
public List<T>? Rows { get; set; }
public T? Data { get; set; }
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\IntegrationGateway.Core\IntegrationGateway.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,281 @@
using IntegrationGateway.Core.Abstractions;
using IntegrationGateway.Core.Infrastructure;
using IntegrationGateway.Core.Models;
using System.Text;
using System.Text.Json;
namespace IntegrationGateway.Adapters.MC4;
/// <summary>
/// MC4.0 动环监控子系统适配器。
///
/// 实现的能力接口:
/// - IHasOwnDeviceTree对象树区域→设备层级
/// - IHasPoints实时点位值读取 + 反向控制写值
/// - IHasAlarms告警查询、确认、结束
///
/// 限流2 QPSMC4.0 API 推荐值)
/// 分页转换:网关 page/size ↔ MC4.0 skip/limit
/// </summary>
public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
{
private readonly HttpClient _http;
private readonly Mc4AuthHelper _auth;
/// <summary>令牌桶限流器2 QPS</summary>
private readonly RateLimiter _limiter = new(2);
/// <summary>适配器编码,格式 "MC4:实例名"</summary>
public string AdapterCode { get; }
/// <summary>人类可读的适配器名称</summary>
public string DisplayName => $"MC4 ({AdapterCode})";
/// <summary>适配器能力声明</summary>
public AdapterCapabilities Capabilities => new()
{
HasObjectTree = true, HasPoints = true, HasAlarms = true, AcceptsControl = true
};
/// <summary>创建 Mc4Adapter 实例</summary>
/// <param name="adapterCode">适配器编码</param>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">MC4.0 服务地址</param>
public Mc4Adapter(string adapterCode, HttpClient http, string baseUrl)
{
AdapterCode = adapterCode;
_http = http;
_auth = new Mc4AuthHelper(http, baseUrl);
}
/// <summary>初始化适配器:获取 MC4.0 Token</summary>
public async Task InitializeAsync() => await _auth.GetTokenAsync();
/// <summary>健康检查:尝试调用 MC4.0 认证接口确认可达性</summary>
public async Task<bool> HealthCheckAsync()
{
try
{
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync("/api/central/auth/conf/get", null);
return resp.IsSuccessStatusCode;
}
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
}
// ═══════════════════════════════════════════
// IHasOwnDeviceTree 实现
// ═══════════════════════════════════════════
/// <summary>
/// 获取 MC4.0 完整对象树。
/// Type=1 的节点为区域Type=2 的节点为设备。
/// </summary>
public async Task<List<DeviceTreeNode>> GetObjectTreeAsync()
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync("/api/central/object/tree", null);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var tree = JsonSerializer.Deserialize<List<Mc4TreeNode>>(json)!;
return tree.Select(MapNode).ToList();
}
/// <summary>MC4.0 树节点 → DeviceTreeNode 映射</summary>
private static DeviceTreeNode MapNode(Mc4TreeNode n) => new()
{
Id = n.Id,
SourceId = n.Id.ToString(),
Name = n.Name ?? n.Id.ToString(),
Type = n.Type,
ObjectType = n.ObjectType,
Tag = n.Tag,
Option = n.Option ?? new Dictionary<string, object?>(),
Children = n.Children?.Select(MapNode).ToList() ?? new()
};
// ═══════════════════════════════════════════
// IHasPoints 实现
// ═══════════════════════════════════════════
/// <summary>获取指定设备的所有实时点位值</summary>
public async Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = JsonSerializer.Serialize(new { id = int.Parse(sourceDeviceId) });
var resp = await client.PostAsync("/api/central/device/point/value/get",
new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var values = JsonSerializer.Deserialize<List<Mc4PointValue>>(json)!;
return values.Select(v => new PointValue
{
SourceDeviceId = sourceDeviceId,
PointIndex = v.Index,
Value = v.Value,
UpdateTime = v.Time != null ? DateTime.Parse(v.Time) : null,
Interval = v.Interval
}).ToList();
}
/// <summary>向指定设备的指定点位写入控制值</summary>
public async Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = JsonSerializer.Serialize(new { id = int.Parse(sourceDeviceId), index = pointIndex, value });
await client.PostAsync("/api/central/point/value/set",
new StringContent(body, Encoding.UTF8, "application/json"));
}
// ═══════════════════════════════════════════
// IHasAlarms 实现
// ═══════════════════════════════════════════
/// <summary>
/// 分页查询告警列表。
/// 内部完成 page/size → skip/limit 转换。
/// </summary>
public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to,
string? level = null, string? state = null)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = JsonSerializer.Serialize(new Mc4AlarmQuery
{
From = from.ToString("yyyy-MM-dd HH:mm:ss"),
To = to.ToString("yyyy-MM-dd HH:mm:ss"),
Skip = (page - 1) * size,
Limit = size,
Sort = 1 // 按时间降序
});
var resp = await client.PostAsync("/api/central/alarm/query",
new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<Mc4AlarmQueryResult>(json)!;
return new PagedResult<StandardAlarm>
{
Items = result.List?.Select(a => new StandardAlarm
{
AlarmId = a.Id ?? "",
DeviceId = a.Sid?.ToString(),
AdapterCode = AdapterCode,
Level = MapAlarmLevel(a.Level),
Title = a.Desc ?? "",
OccurTime = DateTime.TryParse(a.Stime, out var st) ? st : DateTime.MinValue,
Status = MapAlarmState(a.State),
ActualValue = a.Soption?.Value,
ThresholdValue = a.Eoption?.Value
}).ToList() ?? new(),
Total = result.Total
};
}
/// <summary>确认告警(同时写回 MC4.0</summary>
public async Task ConfirmAlarmAsync(string alarmId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = JsonSerializer.Serialize(new { id = alarmId, option = new { } });
await client.PostAsync("/api/central/alarm/confirm",
new StringContent(body, Encoding.UTF8, "application/json"));
}
/// <summary>结束告警(同时写回 MC4.0</summary>
public async Task EndAlarmAsync(string alarmId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var body = JsonSerializer.Serialize(new { id = alarmId, option = new { } });
await client.PostAsync("/api/central/alarm/end",
new StringContent(body, Encoding.UTF8, "application/json"));
}
/// <summary>MC4.0 告警等级数字 → 中文映射</summary>
private static string MapAlarmLevel(int level) => level switch
{
1 => "提示", 2 => "普通", 3 => "重要", 4 => "紧急", _ => "提示"
};
/// <summary>MC4.0 告警状态数字 → 中文映射</summary>
private static string MapAlarmState(int state) => state switch
{
1 => "未确认", 2 => "已确认", 3 => "已结束", _ => "未确认"
};
}
// ═══════════════════════════════════════════
// MC4.0 JSON 反序列化模型(内部使用)
// ═══════════════════════════════════════════
/// <summary>MC4.0 对象树节点</summary>
public class Mc4TreeNode
{
public int Id { get; set; }
public string? Name { get; set; }
/// <summary>节点类型1=区域2=设备</summary>
public int Type { get; set; }
public int ObjectType { get; set; }
public string? Tag { get; set; }
public Dictionary<string, object?>? Option { get; set; }
public List<Mc4TreeNode>? Children { get; set; }
}
/// <summary>MC4.0 点位值</summary>
public class Mc4PointValue
{
public int Id { get; set; }
public int Index { get; set; }
public double Value { get; set; }
public string? Time { get; set; }
public int Interval { get; set; }
}
/// <summary>MC4.0 告警查询请求体</summary>
public class Mc4AlarmQuery
{
public string? Sid { get; set; }
public string From { get; set; } = "";
public string To { get; set; } = "";
/// <summary>跳过的记录数(= (page-1)*size</summary>
public int Skip { get; set; }
/// <summary>每页条数</summary>
public int Limit { get; set; }
/// <summary>排序方式1=时间降序</summary>
public int Sort { get; set; }
}
/// <summary>MC4.0 告警查询响应</summary>
public class Mc4AlarmQueryResult
{
public int Total { get; set; }
public List<Mc4AlarmItem>? List { get; set; }
}
/// <summary>MC4.0 告警条目</summary>
public class Mc4AlarmItem
{
public string? Id { get; set; }
/// <summary>设备 SID</summary>
public int? Sid { get; set; }
public string? Desc { get; set; }
public string? EngDesc { get; set; }
public int Level { get; set; }
public int State { get; set; }
public string? Stime { get; set; }
public string? Etime { get; set; }
public string? Ctime { get; set; }
public string? Cuser { get; set; }
public int Type { get; set; }
/// <summary>告警触发时阈值信息</summary>
public Mc4Option? Soption { get; set; }
/// <summary>告警结束时阈值信息</summary>
public Mc4Option? Eoption { get; set; }
}
/// <summary>MC4.0 告警阈值信息</summary>
public class Mc4Option
{
public double? Value { get; set; }
public string? TypeName { get; set; }
}

View File

@@ -0,0 +1,62 @@
using System.Text.Json;
namespace IntegrationGateway.Adapters.MC4;
/// <summary>
/// MC4.0 子系统的 Token 认证辅助类。
///
/// 认证流程:
/// 1. POST /api/central/auth/conf/get 获取临时 Token
/// 2. Token 有效期约 8 小时,缓存在内存中
/// 3. 后续请求在 header["token"] 中携带 Token
///
/// 注意MC4.0 使用自定义 header "token" 而非标准 Authorization 头。
/// </summary>
public class Mc4AuthHelper
{
private readonly HttpClient _http;
private readonly string _baseUrl;
/// <summary>缓存的认证 Token</summary>
private string? _token;
/// <summary>Token 过期时间UTC默认 8 小时</summary>
private DateTime _tokenExpiry = DateTime.MinValue;
/// <summary>创建 MC4.0 认证辅助</summary>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">MC4.0 服务地址</param>
public Mc4AuthHelper(HttpClient http, string baseUrl)
{
_http = http; _baseUrl = baseUrl.TrimEnd('/');
}
/// <summary>获取有效的 Token。缓存有效则直接返回否则重新获取。</summary>
public async Task<string> GetTokenAsync()
{
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
var resp = await _http.PostAsync($"{_baseUrl}/api/central/auth/conf/get", null);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<Mc4AuthResponse>(json);
_token = result?.Token ?? "";
_tokenExpiry = DateTime.UtcNow.AddHours(8);
return _token!;
}
/// <summary>
/// 创建一个已认证的 HttpClient自动在 header["token"] 中附带 Token。
/// </summary>
public async Task<HttpClient> GetAuthenticatedClientAsync()
{
var token = await GetTokenAsync();
var client = new HttpClient { BaseAddress = new Uri(_baseUrl) };
client.DefaultRequestHeaders.Add("token", token);
return client;
}
/// <summary>强制清除缓存 Token</summary>
public void Invalidate() => _token = null;
/// <summary>MC4.0 认证响应</summary>
public class Mc4AuthResponse { public string? Token { get; set; } }
}

Some files were not shown because too many files have changed in this diff Show More