Compare commits

5 Commits

169 changed files with 3036 additions and 19579 deletions

View File

@@ -1,293 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,175 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,154 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,87 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,42 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,28 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,22 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,22 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,22 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,22 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,120 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,123 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果数据库字段发生变化请在代码生器重新生成此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

@@ -1,127 +0,0 @@
/*
*设备管理扩展 — 区域树 + 点位设备列表
*所有改动在 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

@@ -1,246 +0,0 @@
/*
*网关节点管理 — 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

@@ -1,33 +0,0 @@
/*
*接口编写处...
*如果接口需要做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

@@ -1,33 +0,0 @@
/*
*接口编写处...
*如果接口需要做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

@@ -1,33 +0,0 @@
/*
*接口编写处...
*如果接口需要做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

@@ -1,33 +0,0 @@
/*
*接口编写处...
*如果接口需要做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

@@ -1,21 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下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

@@ -1,21 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下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

@@ -1,21 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下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

@@ -1,21 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下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

@@ -1,21 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下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

@@ -1,21 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*如果要增加方法请在当前目录下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

@@ -69,14 +69,6 @@ namespace VolPro.WebApi
services.AddSession();
services.AddMemoryCache();
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 =>
{
options.Filters.Add(typeof(ApiAuthorizeFilter));

View File

@@ -1,18 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,18 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,18 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,18 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,18 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,18 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +0,0 @@
/*
*所有关于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

@@ -1,19 +0,0 @@
/*
*所有关于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

@@ -1,17 +0,0 @@
/*
*所有关于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

@@ -1,13 +0,0 @@
/*
*所有关于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

@@ -1,13 +0,0 @@
/*
*所有关于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

@@ -1,13 +0,0 @@
/*
*所有关于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

@@ -1,24 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,24 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,24 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,24 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,24 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,24 +0,0 @@
/*
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
*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

@@ -1,73 +0,0 @@
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

@@ -1,58 +0,0 @@
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>
/// 心跳超时检测任务。扫描心跳超时 30 秒的网关节点,标记为离线,
/// 并级联标记该节点下所有设备为离线。
/// Cron 建议: 每 15 秒 ("0/15 * * * * ?")
/// </summary>
public class HeartbeatMonitorJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
var gwSvc = sp.GetService<Igateway_nodesService>();
var devSvc = sp.GetService<Ibase_deviceService>();
if (gwSvc == 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 = "离线";
await gwSvc.FindAsIQueryable(x => x.NodeId == node.NodeId)
.FirstAsync(); // 确保实体被跟踪
// 直接通过 DbContext 更新
var dbProp = gwSvc.GetType().BaseType?.GetProperty("DbContext");
if (dbProp != null) continue; // fallback: 通过 FindAsIQueryable 重新获取更新
Console.WriteLine($"[HeartbeatMonitorJob] 网关 {node.NodeCode} 心跳超时,标记离线");
// 级联标记该网关下所有设备离线
if (devSvc != null)
{
var devices = await devSvc.FindAsIQueryable(
x => x.NodeId == node.NodeId && x.IsOnline == "在线")
.ToListAsync();
foreach (var dev in devices)
{
dev.IsOnline = "离线";
}
Console.WriteLine($"[HeartbeatMonitorJob] 级联 {devices.Count} 台设备离线");
}
}
}
}

View File

@@ -1,20 +0,0 @@
using Quartz;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
namespace VolPro.Warehouse.Services;
/// <summary>
/// 实时数据轮询任务Phase 2 完善)。
/// 定时轮询 MC4.0 IoT 设备实时值 → 更新 iot_devicedata。
/// Cron 建议: 每 10 秒 ("0/10 * * * * ?")
/// </summary>
public class RealtimePollJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
// TODO: Phase 2 — 遍历在线 MC4 网关,轮询实时值写入 iot_devicedata
return Task.CompletedTask;
}
}

View File

@@ -1,43 +0,0 @@
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

@@ -1,116 +0,0 @@
/*
*所有关于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

@@ -1,196 +0,0 @@
/*
*所有关于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

@@ -1,82 +0,0 @@
/*
*所有关于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

@@ -1,41 +0,0 @@
/*
*所有关于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

@@ -1,41 +0,0 @@
/*
*所有关于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

@@ -1,41 +0,0 @@
/*
*所有关于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

@@ -1,22 +0,0 @@
/*
*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

@@ -1,22 +0,0 @@
/*
*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

@@ -1,22 +0,0 @@
/*
*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

@@ -1,22 +0,0 @@
/*
*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

@@ -1,22 +0,0 @@
/*
*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

@@ -1,22 +0,0 @@
/*
*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,163 +1,168 @@
-- ============================================
-- SecMPS v3.0 数据库建表脚本6张表
-- SecMPS v2.0 数据库建表脚本
-- 数据库: gljs_main
-- 扩展表已合并到 Base_Device.ExtraData(JSON)
-- ============================================
-- ============================================
USE gljs_main;
-- 1. 统一设备主表
-- ExtraData(JSON) 承载所有适配器特有字段
-- DeviceGroup 路由到正确的网关Adapter和前端按钮组
-- ============================================
DROP TABLE IF EXISTS base_device;
CREATE TABLE base_device (
DeviceId INT AUTO_INCREMENT COMMENT '设备ID',
DeviceName NVARCHAR(100) NOT NULL COMMENT '设备名称',
AdapterCode NVARCHAR(50) COMMENT '来源适配器(类型:实例)',
SourceId NVARCHAR(100) COMMENT '源系统设备ID',
DeviceCategory NVARCHAR(50) NOT NULL COMMENT '设备种类(数据字典:门磁/空调/智能断路器/人行道闸/车辆道闸/485钥匙柜/网络钥匙柜/紧急报警按钮/红外报警器/门禁一体机/除湿_恒湿机/空调控制器/烟雾报警器/气体报警器/温湿度变送器/摄像机/硬盘录像机/动环采集器)',
DeviceGroup NVARCHAR(20) NOT NULL COMMENT '设备分组(数据字典:视频设备/IoT设备/门禁设备/道闸设备/报警设备)',
PointId INT NULL COMMENT '所属点位ID',
NodeId INT NULL COMMENT '所属网关节点ID',
IsParent NVARCHAR(20) NOT NULL DEFAULT '' COMMENT '是否父设备(数据字典:是/否)',
ParentDeviceId INT NULL COMMENT '父设备ID(自引用,子设备挂父设备下)',
IsOnline NVARCHAR(20) DEFAULT '离线' COMMENT '在线状态(数据字典:在线/离线)',
IpAddress NVARCHAR(50) COMMENT 'IP地址',
Port INT COMMENT '端口',
Location NVARCHAR(200) COMMENT '安装位置',
Lat DOUBLE COMMENT '纬度',
Lng DOUBLE COMMENT '经度',
MapModelId NVARCHAR(100) COMMENT '三维地图模型ID',
MapModelScale FLOAT DEFAULT 1.0 COMMENT '模型缩放比例',
MapModelRotation NVARCHAR(100) COMMENT '模型旋转角度(JSON)',
ExtraData TEXT COMMENT '适配器扩展数据JSON(Owl/MC4/门禁字段均存于此)',
LastSyncTime DATETIME COMMENT '上次同步时间',
Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)',
Remark NVARCHAR(500) COMMENT '备注',
CreateID INT COMMENT '创建人ID',
Creator NVARCHAR(50) COMMENT '创建人',
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
ModifyID INT COMMENT '修改人ID',
Modifier NVARCHAR(50) COMMENT '修改人',
ModifyDate DATETIME COMMENT '修改时间',
PRIMARY KEY (DeviceId),
INDEX IX_Sync (AdapterCode, SourceId),
INDEX IX_Point (PointId),
INDEX IX_Parent (ParentDeviceId),
INDEX IX_Gateway (GatewayNodeId),
INDEX IX_Group (DeviceGroup)
) COMMENT '统一设备主表';
CREATE TABLE IF NOT EXISTS Base_Device (
DeviceId CHAR(36) NOT NULL PRIMARY KEY,
DeviceName NVARCHAR(100) NOT NULL,
AdapterCode NVARCHAR(50) NOT NULL,
SourceId NVARCHAR(100) NOT NULL,
DeviceCategory INT NOT NULL DEFAULT 1,
DeviceType NVARCHAR(50),
RegionId INT NULL,
IsParent TINYINT NOT NULL DEFAULT 0,
ParentDeviceId CHAR(36) NULL,
IsOnline TINYINT NOT NULL DEFAULT 0,
IpAddress NVARCHAR(50),
Port INT,
Location NVARCHAR(200),
Lat DOUBLE,
Lng DOUBLE,
MapModelId NVARCHAR(100),
MapModelScale FLOAT DEFAULT 1.0,
MapModelRotation NVARCHAR(100),
ExtraData TEXT,
LocalOverrides TEXT,
SyncVersion BIGINT DEFAULT 0,
LastSyncTime DATETIME,
Enable TINYINT DEFAULT 1,
Remark NVARCHAR(500),
CreateID INT,
Creator NVARCHAR(50),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
ModifyID INT,
Modifier NVARCHAR(50),
ModifyDate DATETIME,
UNIQUE INDEX IX_Base_Device_Adapter_Source (AdapterCode, SourceId),
INDEX IX_Base_Device_RegionId (RegionId),
INDEX IX_Base_Device_ParentId (ParentDeviceId)
);
-- ============================================
-- 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 '视频通道表';
-- 2. 视频设备扩展表
CREATE TABLE IF NOT EXISTS Device_Video_Ext (
ExtId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
OwlDeviceId NVARCHAR(64) NOT NULL,
Protocol INT DEFAULT 1,
Manufacturer NVARCHAR(100),
Model NVARCHAR(100),
ChannelCount INT DEFAULT 0,
OwlStatus NVARCHAR(500),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_VideoExt_Owl (OwlDeviceId),
INDEX IX_VideoExt_Device (DeviceId)
);
-- ============================================
-- 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 '录像记录表';
-- 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. 设备数据归档表
-- 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 '设备数据归档表';
-- 4. 录像记录表
CREATE TABLE IF NOT EXISTS Video_Record (
RecordId CHAR(36) NOT NULL PRIMARY KEY,
ChannelId CHAR(36) NOT NULL,
OwlRecordId INT NOT NULL,
App NVARCHAR(50),
Stream NVARCHAR(100),
StartedAt DATETIME NOT NULL,
EndedAt DATETIME,
Duration DOUBLE DEFAULT 0,
FilePath NVARCHAR(500),
FileSize BIGINT DEFAULT 0,
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX IX_Record_Channel (ChannelId),
INDEX IX_Record_Time (StartedAt)
);
-- ============================================
-- 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 '告警记录表';
-- 5. IoT设备扩展表
CREATE TABLE IF NOT EXISTS Device_IoT_Ext (
ExtId CHAR(36) NOT NULL PRIMARY KEY,
DeviceId CHAR(36) NOT NULL,
Mc4DeviceId INT NOT NULL,
ObjectType INT,
Tag NVARCHAR(100),
ParentId INT,
Mc4Option NVARCHAR(500),
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX IX_IoTExt_Mc4 (Mc4DeviceId),
INDEX IX_IoTExt_Device (DeviceId)
);
-- ============================================
-- 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 '网关节点注册表';
-- 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)
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,256 +0,0 @@
# 钥匙柜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

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,329 +0,0 @@
# 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

@@ -1,19 +0,0 @@
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

@@ -1,218 +0,0 @@
# 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

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

View File

@@ -1,9 +0,0 @@
<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>

View File

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

View File

@@ -1,13 +0,0 @@
<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

@@ -1,337 +0,0 @@
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 { 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

@@ -1,70 +0,0 @@
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

@@ -1,254 +0,0 @@
/// <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

@@ -1,13 +0,0 @@
<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

@@ -1,281 +0,0 @@
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 { 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

@@ -1,62 +0,0 @@
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; } }
}

View File

@@ -1,13 +0,0 @@
<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

@@ -1,278 +0,0 @@
using IntegrationGateway.Core.Abstractions;
using IntegrationGateway.Core.Infrastructure;
using IntegrationGateway.Core.Models;
using System.Text.Json;
using System.Net.Http.Json;
namespace IntegrationGateway.Adapters.Owl;
/// <summary>
/// Owl 视频监控子系统适配器。
///
/// 实现的能力接口:
/// - IHasFlatDevices设备列表NVR和通道列表
/// - IHasStreams实时取流、录像回放、云台控制、截图
/// - IHasRecordings录像文件查询
/// - IAcceptsMetadataPush设备元数据回写如改名
///
/// 限流5 QPSOwl 推荐值)
/// PTZ 限制:仅支持 continuous 方向移动 + stop不支持预设位
/// </summary>
public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAcceptsMetadataPush
{
private readonly HttpClient _http;
private readonly OwlAuthHelper _auth;
/// <summary>令牌桶限流器5 QPS</summary>
private readonly RateLimiter _limiter = new(5);
/// <summary>适配器编码,格式 "Owl:实例名"</summary>
public string AdapterCode { get; }
/// <summary>人类可读的适配器名称</summary>
public string DisplayName => $"Owl ({AdapterCode})";
/// <summary>适配器能力声明</summary>
public AdapterCapabilities Capabilities => new()
{
HasFlatDevices = true, HasStreams = true, HasPtz = true, HasRecordings = true, AcceptsMetadataPush = true
};
/// <summary>创建 OwlAdapter 实例</summary>
/// <param name="adapterCode">适配器编码</param>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">Owl 服务地址</param>
/// <param name="username">登录用户名</param>
/// <param name="password">登录密码</param>
public OwlAdapter(string adapterCode, HttpClient http, string baseUrl, string username, string password)
{
AdapterCode = adapterCode;
_http = http;
_auth = new OwlAuthHelper(http, baseUrl, username, password);
}
/// <summary>初始化适配器:获取 Owl JWT Token</summary>
public async Task InitializeAsync() => await _auth.GetTokenAsync();
/// <summary>健康检查:尝试访问 Owl /health 端点</summary>
public async Task<bool> HealthCheckAsync()
{
try
{
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.GetAsync("/health");
return resp.IsSuccessStatusCode;
}
catch { return false; }
}
// ═══════════════════════════════════════════
// IHasFlatDevices 实现
// ═══════════════════════════════════════════
/// <summary>分页获取 NVR 设备列表</summary>
public async Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var url = $"/devices?page={page}&size={size}";
if (!string.IsNullOrEmpty(keyword)) url += $"&key={Uri.EscapeDataString(keyword)}";
var json = await client.GetStringAsync(url);
var owl = JsonSerializer.Deserialize<OwlPagedResult<OwlDevice>>(json)!;
return new PagedResult<StandardDevice>
{
Items = owl.Items.Select(MapDevice).ToList(),
Total = owl.Total
};
}
// ═══════════════════════════════════════════
// IHasStreams 实现
// ═══════════════════════════════════════════
/// <summary>获取通道实时视频流地址</summary>
public async Task<StreamUrls> GetLiveUrlAsync(string channelId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync($"/channels/{channelId}/play", null);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var play = JsonSerializer.Deserialize<OwlPlayResponse>(json)!;
return MapStreamUrls(play);
}
/// <summary>获取历史录像回放地址HLS VOD 格式)</summary>
public async Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
var token = await _auth.GetTokenAsync();
return new StreamUrls
{
Hls = $"{client.BaseAddress}recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}"
};
}
/// <summary>云台方向控制continuous 模式,仅方向移动)</summary>
public async Task PtzControlAsync(string channelId, string direction, float speed)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control",
new { action = "continuous", direction, speed });
}
/// <summary>云台停止</summary>
public async Task PtzStopAsync(string channelId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
await client.PostAsJsonAsync($"/channels/{channelId}/ptz/control",
new { action = "stop" });
}
/// <summary>获取通道实时截图</summary>
public async Task<StreamUrls> GetSnapshotAsync(string channelId)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var resp = await client.PostAsync($"/channels/{channelId}/snapshot",
new StringContent("{}", System.Text.Encoding.UTF8, "application/json"));
var json = await resp.Content.ReadAsStringAsync();
var snap = JsonSerializer.Deserialize<OwlSnapshotResponse>(json)!;
return new StreamUrls { Hls = snap.Link };
}
// ═══════════════════════════════════════════
// IHasRecordings 实现
// ═══════════════════════════════════════════
/// <summary>分页查询录像文件记录</summary>
public async Task<PagedResult<StandardRecording>> GetRecordingsAsync(
string channelId, DateTime start, DateTime end, int page, int size)
{
await _limiter.WaitAsync();
var client = await _auth.GetAuthenticatedClientAsync();
var startMs = new DateTimeOffset(start).ToUnixTimeMilliseconds();
var endMs = new DateTimeOffset(end).ToUnixTimeMilliseconds();
var json = await client.GetStringAsync(
$"/recordings?cid={channelId}&start_ms={startMs}&end_ms={endMs}&page={page}&size={size}");
var owl = JsonSerializer.Deserialize<OwlPagedResult<OwlRecording>>(json)!;
return new PagedResult<StandardRecording>
{
Items = owl.Items.Select(r => new StandardRecording
{
Id = r.Id, ChannelId = r.Cid,
StartedAt = r.StartedAt, EndedAt = r.EndedAt,
Duration = r.Duration, FilePath = r.Path, Size = r.Size
}).ToList(),
Total = owl.Total
};
}
// ═══════════════════════════════════════════
// IAcceptsMetadataPush 实现
// ═══════════════════════════════════════════
/// <summary>回写设备元数据(如改名)到 Owl</summary>
public async Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes)
{
var client = await _auth.GetAuthenticatedClientAsync();
var body = new Dictionary<string, object>();
if (changes.Name != null) body["name"] = changes.Name;
await client.PutAsJsonAsync($"/devices/{sourceDeviceId}", body);
return new MetadataPushResult { Success = true };
}
// ═══════════════════════════════════════════
// 内部映射方法
// ═══════════════════════════════════════════
/// <summary>Owl 设备 → StandardDevice 映射</summary>
private static StandardDevice MapDevice(OwlDevice d) => new()
{
SourceId = d.Id ?? "",
Name = d.Name ?? d.Id ?? "",
Category = "硬盘录像机",
Group = "视频设备",
IsOnline = d.IsOnline == "1",
IsParent = true,
IpAddress = d.Address,
Port = int.TryParse(d.Port, out var port) ? port : null,
Extra = new Dictionary<string, object?>
{
["owlDeviceId"] = d.Id,
["protocol"] = d.Protocol ?? "GB28181",
["transport"] = d.Transport
}
};
/// <summary>Owl 播放响应 → StreamUrls 映射(取第一个可用流)</summary>
private static StreamUrls MapStreamUrls(OwlPlayResponse play)
{
var item = play.Items?.FirstOrDefault();
return new StreamUrls
{
WsFlv = item?.WsFlv, HttpFlv = item?.HttpFlv, Hls = item?.Hls,
WebRtc = item?.WebRtc, Rtmp = item?.Rtmp, Rtsp = item?.Rtsp
};
}
}
// ═══════════════════════════════════════════
// Owl JSON 反序列化模型(内部使用)
// ═══════════════════════════════════════════
/// <summary>Owl API 分页响应</summary>
public class OwlPagedResult<T>
{
public List<T> Items { get; set; } = new();
public int Total { get; set; }
}
/// <summary>Owl 设备NVR</summary>
public class OwlDevice
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? IsOnline { get; set; }
public string? Protocol { get; set; }
public string? Address { get; set; }
public string? Port { get; set; }
public string? Transport { get; set; }
}
/// <summary>Owl 播放响应</summary>
public class OwlPlayResponse
{
public List<OwlPlayItem>? Items { get; set; }
}
/// <summary>Owl 播放流条目</summary>
public class OwlPlayItem
{
public string? WsFlv { get; set; }
public string? HttpFlv { get; set; }
public string? Hls { get; set; }
public string? WebRtc { get; set; }
public string? Rtmp { get; set; }
public string? Rtsp { get; set; }
}
/// <summary>Owl 截图响应</summary>
public class OwlSnapshotResponse
{
public string? Link { get; set; }
}
/// <summary>Owl 录像记录</summary>
public class OwlRecording
{
public int Id { get; set; }
public string? Cid { get; set; }
public DateTime StartedAt { get; set; }
public DateTime EndedAt { get; set; }
public double Duration { get; set; }
public string? Path { get; set; }
public long Size { get; set; }
}

View File

@@ -1,88 +0,0 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Net.Http.Json;
namespace IntegrationGateway.Adapters.Owl;
/// <summary>
/// Owl 子系统的 RSA 加密认证辅助类。
///
/// 认证流程:
/// 1. GET /login/key 获取 RSA 公钥Base64 编码)
/// 2. 用公钥加密 {username, password} JSON
/// 3. POST /login 发送加密后的凭据换取 JWT Token
///
/// Token 在内存中缓存约 2.5 天Owl 默认 3 天有效期),过期前自动刷新。
/// </summary>
public class OwlAuthHelper
{
private readonly HttpClient _http;
private readonly string _baseUrl;
private readonly string _username;
private readonly string _password;
/// <summary>缓存的 JWT Token</summary>
private string? _token;
/// <summary>Token 过期时间UTC</summary>
private DateTime _tokenExpiry = DateTime.MinValue;
/// <summary>创建 Owl 认证辅助</summary>
/// <param name="http">HttpClient 实例</param>
/// <param name="baseUrl">Owl 服务地址,如 http://localhost:15123</param>
/// <param name="username">Owl 登录用户名</param>
/// <param name="password">Owl 登录密码</param>
public OwlAuthHelper(HttpClient http, string baseUrl, string username, string password)
{
_http = http; _baseUrl = baseUrl.TrimEnd('/');
_username = username; _password = password;
}
/// <summary>
/// 获取有效的 JWT Token。如果缓存有效则直接返回否则执行完整登录流程。
/// </summary>
public async Task<string> GetTokenAsync()
{
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
// 第一步:获取 RSA 公钥
var keyResp = await _http.GetStringAsync($"{_baseUrl}/login/key");
var keyData = JsonSerializer.Deserialize<LoginKeyResponse>(keyResp);
var publicKey = Encoding.UTF8.GetString(Convert.FromBase64String(keyData!.Key!));
// 第二步RSA 加密凭据
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
var plain = JsonSerializer.Serialize(new { username = _username, password = _password });
var encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.Pkcs1);
var payload = JsonSerializer.Serialize(new { data = Convert.ToBase64String(encrypted) });
// 第三步:登录换取 Token
var resp = await _http.PostAsync($"{_baseUrl}/login",
new StringContent(payload, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var loginResult = await resp.Content.ReadFromJsonAsync<LoginResponse>();
_token = loginResult!.Token;
_tokenExpiry = DateTime.UtcNow.AddDays(2.5); // 保守设置Owl 默认 3 天
return _token;
}
/// <summary>强制清除缓存的 Token下次调用 GetTokenAsync 将重新登录</summary>
public void Invalidate() => _token = null;
/// <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>登录密钥响应</summary>
public class LoginKeyResponse { public string? Key { get; set; } }
/// <summary>登录响应</summary>
public class LoginResponse { public string Token { get; set; } = ""; public string? User { get; set; } }
}

View File

@@ -1,16 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 设备反向控制接口。适用于支持远程下发指令的子系统(如 KMS 远程开门、门禁开闸、道闸抬杆)。
/// 控制指令为通用键值对字典,适配器内部转换为子系统指令。
/// </summary>
public interface IAcceptsControl : IGatewayAdapter
{
/// <summary>向设备下发控制指令</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
/// <param name="command">指令名,如 "open"/"close"/"authorize"</param>
/// <param name="parameters">指令参数键值对</param>
Task<ControlResult> SendControlAsync(string sourceDeviceId, string command, Dictionary<string, object?> parameters);
}

View File

@@ -1,19 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 外部数据同步写入接口。适用于需要从 Vol.Pro 向子系统推送数据的场景(如员工同步)。
/// </summary>
public interface IAcceptsDataSync : IGatewayAdapter
{
/// <summary>批量写入数据到子系统</summary>
/// <param name="dataType">数据类型: "staff"/"department"</param>
/// <param name="items">待同步的数据对象列表</param>
Task<SyncResult> SyncDataAsync(string dataType, List<object> items);
/// <summary>批量删除子系统数据</summary>
/// <param name="dataType">数据类型</param>
/// <param name="ids">待删除的 ID 列表</param>
Task<SyncResult> DeleteDataAsync(string dataType, List<string> ids);
}

View File

@@ -1,14 +1,21 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 元数据回写接口。适用于支持管理端修改设备属性的子系统(如 Owl 设备改名)。
/// </summary>
public interface IAcceptsMetadataPush : IGatewayAdapter
public interface IAcceptsMetadataPush : IIntegrationAdapter
{
/// <summary>向子系统回写设备元数据变更</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
/// <param name="changes">变更集,仅非 null 字段会被更新</param>
Task<MetadataPushResult> PushMetadataAsync(string sourceDeviceId, MetadataChangeSet changes);
}
public class MetadataChangeSet
{
public string? Name { get; set; }
public string? IpAddress { get; set; }
public int? Port { get; set; }
public int? StreamMode { get; set; }
}
public class MetadataPushResult
{
public bool Success { get; set; }
public List<string> RejectedFields { get; set; } = new();
public string? Reason { get; set; }
}

View File

@@ -1,21 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 网关适配器基础接口。所有子系统适配器必须实现此接口。
/// 定义了适配器的元信息、生命周期和健康检查能力。
/// </summary>
public interface IGatewayAdapter
{
/// <summary>适配器编码,格式 "类型:实例",如 "Owl:main"、"MC4:31ku"</summary>
string AdapterCode { get; }
/// <summary>人类可读的适配器显示名称</summary>
string DisplayName { get; }
/// <summary>适配器能力声明(声明实现哪些能力接口)</summary>
AdapterCapabilities Capabilities { get; }
/// <summary>懒加载初始化(建立连接、获取认证 Token 等)。失败不阻塞网关启动。</summary>
Task InitializeAsync();
/// <summary>健康检查。返回 true 表示适配器及子系统可达。</summary>
Task<bool> HealthCheckAsync();
}

View File

@@ -2,25 +2,11 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 告警接口。适用于具有告警功能的子系统(如 MC4.0 / Owl AI 事件)。
/// 支持告警查询、确认和结束操作。
/// </summary>
public interface IHasAlarms : IGatewayAdapter
public interface IHasAlarms : IIntegrationAdapter
{
/// <summary>分页查询告警列表</summary>
/// <param name="page">页码</param>
/// <param name="size">每页条数</param>
/// <param name="from">告警开始时间下限</param>
/// <param name="to">告警开始时间上限</param>
/// <param name="level">告警等级过滤(可选)</param>
/// <param name="state">告警状态过滤(可选)</param>
Task<PagedResult<StandardAlarm>> GetAlarmsAsync(int page, int size, DateTime from, DateTime to,
string? level = null, string? state = null);
/// <summary>确认告警(同时写回子系统)</summary>
/// <param name="alarmId">子系统告警 ID</param>
int? confirmState = null, int? endState = null, List<int>? levels = null);
Task ConfirmAlarmAsync(string alarmId);
/// <summary>结束告警(同时写回子系统)</summary>
/// <param name="alarmId">子系统告警 ID</param>
Task EndAlarmAsync(string alarmId);
Task<int> GetPendingAlarmCountAsync();
}

View File

@@ -1,21 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 业务记录查询接口。适用于具有借还、交接、授权等业务日志的子系统。
/// 不走 StandardAlarm 通道,独立分页查询。
/// </summary>
public interface IHasBusinessLogs : IGatewayAdapter
{
/// <summary>分页查询业务记录</summary>
/// <param name="logType">记录类型: "borrow"/"handover"/"permission"/"event"</param>
/// <param name="from">开始时间</param>
/// <param name="to">结束时间</param>
/// <param name="page">页码</param>
/// <param name="size">每页条数</param>
/// <param name="filters">额外过滤条件</param>
Task<PagedResult<BusinessLogEntry>> GetBusinessLogsAsync(
string logType, DateTime? from, DateTime? to,
int page, int size, Dictionary<string, string>? filters = null);
}

View File

@@ -2,16 +2,11 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 扁平设备列表接口。适用于设备无层级关系或层级由网关自行构建的子系统(如 Owl/门禁/道闸)。
/// </summary>
public interface IHasFlatDevices : IGatewayAdapter
public interface IHasFlatDevices : IIntegrationAdapter
{
/// <summary>
/// 分页获取设备列表。
/// </summary>
/// <param name="page">页码(从 1 开始)</param>
/// <param name="size">每页条数</param>
/// <param name="keyword">设备名称模糊搜索关键词</param>
Task<PagedResult<StandardDevice>> GetDevicesAsync(int page, int size, string? keyword = null);
Task<StandardDevice?> GetDeviceAsync(string sourceDeviceId);
Task<List<StandardDevice>> GetAllDevicesAsync();
Task<PagedResult<StandardDevice>> GetChannelsAsync(int page, int size, string? parentDeviceId = null);
Task<List<StandardDevice>> GetAllChannelsAsync();
}

View File

@@ -2,12 +2,7 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 自有对象树接口。适用于具有层级对象树的子系统(如 MC4.0)。
/// 返回的 DeviceTreeNode 中 Type=1 为区域节点Type=2 为设备节点。
/// </summary>
public interface IHasOwnDeviceTree : IGatewayAdapter
public interface IHasOwnDeviceTree : IIntegrationAdapter
{
/// <summary>获取子系统的完整对象树</summary>
Task<List<DeviceTreeNode>> GetObjectTreeAsync();
}

View File

@@ -2,18 +2,9 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 实时点位值接口。适用于 IoT 动环类子系统(如 MC4.0)。
/// 支持读取设备测点实时值和反向控制写值。
/// </summary>
public interface IHasPoints : IGatewayAdapter
public interface IHasPoints : IIntegrationAdapter
{
/// <summary>获取指定设备的全部实时点位值</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
Task<List<PointValue>> GetRealtimeValuesAsync(string sourceDeviceId);
/// <summary>向指定设备的指定点位写入控制值</summary>
/// <param name="sourceDeviceId">子系统设备原始 ID</param>
/// <param name="pointIndex">点位索引</param>
/// <param name="value">目标值</param>
Task<List<PointValue>> GetMultiPointValuesAsync(List<(string DeviceId, int PointIndex)> points);
Task SetPointValueAsync(string sourceDeviceId, int pointIndex, double value);
}

View File

@@ -1,18 +0,0 @@
using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 录像回放查询接口。适用于具有录像存储功能的子系统(如 Owl
/// </summary>
public interface IHasRecordings : IGatewayAdapter
{
/// <summary>分页查询录像记录</summary>
/// <param name="channelId">通道 ID</param>
/// <param name="start">录像开始时间下限</param>
/// <param name="end">录像开始时间上限</param>
/// <param name="page">页码</param>
/// <param name="size">每页条数</param>
Task<PagedResult<StandardRecording>> GetRecordingsAsync(
string channelId, DateTime start, DateTime end, int page, int size);
}

View File

@@ -2,27 +2,13 @@ using IntegrationGateway.Core.Models;
namespace IntegrationGateway.Core.Abstractions;
/// <summary>
/// 视频流接口。适用于视频监控类子系统(如 Owl
/// 支持实时取流、录像回放、云台控制和截图。
/// </summary>
public interface IHasStreams : IGatewayAdapter
public interface IHasStreams : IIntegrationAdapter
{
/// <summary>获取实时视频流地址</summary>
/// <param name="channelId">通道 ID</param>
Task<StreamUrls> GetLiveUrlAsync(string channelId);
/// <summary>获取历史录像回放地址HLS VOD</summary>
/// <param name="channelId">通道 ID</param>
/// <param name="start">回放开始时间</param>
/// <param name="end">回放结束时间</param>
Task<StreamUrls> GetPlaybackUrlAsync(string channelId, DateTime start, DateTime end);
/// <summary>云台方向控制continuous 模式)</summary>
/// <param name="channelId">通道 ID</param>
/// <param name="direction">方向up/down/left/right/zoom_in/zoom_out</param>
/// <param name="speed">速度 0.0-1.0</param>
Task PtzControlAsync(string channelId, string direction, float speed);
/// <summary>云台停止</summary>
Task PtzStopAsync(string channelId);
/// <summary>获取通道实时截图</summary>
Task StopPlayAsync(string channelId);
Task<StreamUrls> GetSnapshotAsync(string channelId);
Task PtzControlAsync(string channelId, string direction, float speed);
Task PtzStopAsync(string channelId);
Task<PagedResult<StandardRecording>> GetRecordingsAsync(string channelId, DateTime start, DateTime end, int page, int size);
}

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