Compare commits
5 Commits
c8f36ad3b4
...
phase/0-in
| Author | SHA1 | Date | |
|---|---|---|---|
| 46b679ca4e | |||
| 7550903aa8 | |||
| 5fd71a20b9 | |||
| f0add6e0b9 | |||
| 9277330922 |
@@ -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; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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) : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)]属性,否则会异常
|
||||
}
|
||||
}
|
||||
@@ -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)]属性,否则会异常
|
||||
}
|
||||
}
|
||||
@@ -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)]属性,否则会异常
|
||||
}
|
||||
}
|
||||
@@ -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)]属性,否则会异常
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; } = "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
|
||||
*/
|
||||
using VolPro.Core.BaseProvider;
|
||||
using VolPro.Entity.DomainModels;
|
||||
|
||||
namespace Warehouse.IServices
|
||||
{
|
||||
public partial interface Ibase_deviceService : IService<base_device>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
|
||||
*/
|
||||
using VolPro.Core.BaseProvider;
|
||||
using VolPro.Entity.DomainModels;
|
||||
|
||||
namespace Warehouse.IServices
|
||||
{
|
||||
public partial interface Igateway_nodesService : IService<gateway_nodes>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
|
||||
*/
|
||||
using VolPro.Core.BaseProvider;
|
||||
using VolPro.Entity.DomainModels;
|
||||
|
||||
namespace Warehouse.IServices
|
||||
{
|
||||
public partial interface Iiot_alarmService : IService<iot_alarm>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
|
||||
*/
|
||||
using VolPro.Core.BaseProvider;
|
||||
using VolPro.Entity.DomainModels;
|
||||
|
||||
namespace Warehouse.IServices
|
||||
{
|
||||
public partial interface Iiot_devicedataService : IService<iot_devicedata>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
|
||||
*/
|
||||
using VolPro.Core.BaseProvider;
|
||||
using VolPro.Entity.DomainModels;
|
||||
|
||||
namespace Warehouse.IServices
|
||||
{
|
||||
public partial interface Ivideo_channelService : IService<video_channel>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
*代码由框架生成,任何更改都可能导致被代码生成器覆盖
|
||||
*/
|
||||
using VolPro.Core.BaseProvider;
|
||||
using VolPro.Entity.DomainModels;
|
||||
|
||||
namespace Warehouse.IServices
|
||||
{
|
||||
public partial interface Ivideo_recordService : IService<video_record>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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} 台设备离线");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; } = "";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
*Author:jxx
|
||||
*Contact:283591387@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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
*Author:jxx
|
||||
*Contact:283591387@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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
*Author:jxx
|
||||
*Contact:283591387@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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
*Author:jxx
|
||||
*Contact:283591387@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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
*Author:jxx
|
||||
*Contact:283591387@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>(); } }
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
*Author:jxx
|
||||
*Contact:283591387@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>(); } }
|
||||
}
|
||||
}
|
||||
311
doc/db_init.sql
311
doc/db_init.sql
@@ -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
Binary file not shown.
@@ -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/` 前缀可能变化)。
|
||||
@@ -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
|
||||
424
doc/整合方案/SecMPS_整合方案_v3.0.md
Normal file
424
doc/整合方案/SecMPS_整合方案_v3.0.md
Normal 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 build),Vol.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
@@ -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 HealthCheckAsync(2.18.1)
|
||||
- [ ] POST `/prod-api/heartBeat` (空 body `{}`)
|
||||
- [ ] 返回 `resp.IsSuccessStatusCode`
|
||||
- [ ] 异常捕获返回 false
|
||||
|
||||
### K3.4 GetDevicesAsync(2.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 GetAlarmsAsync(2.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 GetBorrowRecordsAsync(2.18.6)
|
||||
- [ ] POST `/prod-api/getRecordList`
|
||||
- [ ] 参数:`from`, `to` DateTime?(联调时确认请求体格式)
|
||||
- [ ] 返回 `PagedResult<KmsRecord>`
|
||||
|
||||
### K4.2 GetPermissionListAsync(2.18.5)
|
||||
- [ ] POST `/prod-api/getPermissionList`
|
||||
- [ ] 参数:`from`, `to` DateTime?
|
||||
- [ ] 返回 `PagedResult<KmsPermission>`
|
||||
|
||||
### K4.3 BatchSyncStaffAsync(2.18.3)
|
||||
- [ ] POST `/prod-api/batchSyncStaff`
|
||||
- [ ] 请求体:`new { staff = staffList }`
|
||||
- [ ] `resp.EnsureSuccessStatusCode()`
|
||||
|
||||
### K4.4 BatchDeleteStaffAsync(2.18.2)
|
||||
- [ ] POST `/prod-api/batchDeleteStaff`
|
||||
- [ ] 请求体:`List<string>` (staffUuid 数组)
|
||||
- [ ] `resp.EnsureSuccessStatusCode()`
|
||||
|
||||
### K4.5 RemoteAuthorizeAsync(2.4.3)
|
||||
- [ ] POST `/prod-api/kms/permission/remote`
|
||||
- [ ] 请求体:`KmsRemotePermissionRequest`(联调确认字段)
|
||||
|
||||
### K4.6 ThirdPlatLoginAsync(2.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 制订
|
||||
@@ -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
|
||||
@@ -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 实时曲线 Tab(IoT 设备)
|
||||
- [ ] 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 联调)
|
||||
@@ -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 需要验证
|
||||
54
gateway/IntegrationGateway.sln
Normal file
54
gateway/IntegrationGateway.sln
Normal 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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 QPS(MC4.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; }
|
||||
}
|
||||
@@ -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; } }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 QPS(Owl 推荐值)
|
||||
/// 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; }
|
||||
}
|
||||
@@ -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; } }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user