Compare commits
5 Commits
gateway-de
...
phase/0-in
| Author | SHA1 | Date | |
|---|---|---|---|
| 46b679ca4e | |||
| 7550903aa8 | |||
| 5fd71a20b9 | |||
| f0add6e0b9 | |||
| 9277330922 |
File diff suppressed because one or more lines are too long
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,7 +13,3 @@ temp.json
|
|||||||
*.bak
|
*.bak
|
||||||
tmp.bat
|
tmp.bat
|
||||||
report_form_rollup-plugin-visualizer.html
|
report_form_rollup-plugin-visualizer.html
|
||||||
|
|
||||||
**/bin/
|
|
||||||
**/obj/
|
|
||||||
appsettings.Production.json
|
|
||||||
@@ -27,46 +27,44 @@ namespace VolPro.Core.Utilities
|
|||||||
|
|
||||||
canvas.Clear(SKColors.White);
|
canvas.Clear(SKColors.White);
|
||||||
|
|
||||||
// 从 fonts 文件夹加载字体文件(相对于运行目录)
|
|
||||||
string fontPath = Path.Combine(AppContext.BaseDirectory, "fonts", "DejaVuSans.ttf");
|
|
||||||
if (!File.Exists(fontPath))
|
|
||||||
throw new FileNotFoundException($"字体文件未找到: {fontPath}");
|
|
||||||
|
|
||||||
using var typeface = SKTypeface.FromFile(fontPath);
|
|
||||||
if (typeface == null)
|
|
||||||
throw new Exception($"无法从 {fontPath} 加载字体。");
|
|
||||||
|
|
||||||
using var pen = new SKPaint();
|
using var pen = new SKPaint();
|
||||||
pen.FakeBoldText = true;
|
pen.FakeBoldText = true;
|
||||||
pen.Style = SKPaintStyle.Fill;
|
pen.Style = SKPaintStyle.Fill;
|
||||||
pen.TextSize = 20;
|
pen.TextSize = 20;// 0.6f * info.Width * pen.TextSize / pen.MeasureText(code);
|
||||||
pen.Typeface = typeface; // 使用加载的本地字体
|
|
||||||
|
|
||||||
// 绘制随机字符
|
//绘制随机字符
|
||||||
for (int i = 0; i < code.Length; i++)
|
for (int i = 0; i < code.Length; i++)
|
||||||
{
|
{
|
||||||
pen.Color = random.GetRandom(colors); // 假设 colors 是外部定义的静态颜色数组
|
pen.Color = random.GetRandom(colors);//随机颜色索引值
|
||||||
var point = new SKPoint
|
|
||||||
|
pen.Typeface = SKTypeface.FromFamilyName("DejaVu Sans", 700, 20, SKFontStyleSlant.Italic);//配置字体
|
||||||
|
var point = new SKPoint()
|
||||||
{
|
{
|
||||||
X = i * 16,
|
X = i * 16,
|
||||||
Y = 22
|
Y = 22// info.Height - ((i + 1) % 2 == 0 ? 2 : 4),
|
||||||
|
|
||||||
};
|
};
|
||||||
canvas.DrawText(code.Substring(i, 1), point, pen);
|
canvas.DrawText(code.Substring(i, 1), point, pen);//绘制一个验证字符
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制噪点
|
// 绘制噪点
|
||||||
var points = Enumerable.Range(0, 100).Select(
|
var points = Enumerable.Range(0, 100).Select(
|
||||||
_ => new SKPoint(random.Next(bitmap.Width), random.Next(bitmap.Height))
|
_ => new SKPoint(random.Next(bitmap.Width), random.Next(bitmap.Height))
|
||||||
).ToArray();
|
).ToArray();
|
||||||
canvas.DrawPoints(SKPointMode.Points, points, pen);
|
canvas.DrawPoints(
|
||||||
|
SKPointMode.Points,
|
||||||
|
points,
|
||||||
|
pen);
|
||||||
|
|
||||||
// 绘制贝塞尔线条(原有逻辑存在 p1~p4 全为零的问题,此处保留原样)
|
//绘制贝塞尔线条
|
||||||
for (int i = 0; i < 2; i++)
|
for (int i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
var p1 = new SKPoint(0, 0);
|
var p1 = new SKPoint(0, 0);
|
||||||
var p2 = new SKPoint(0, 0);
|
var p2 = new SKPoint(0, 0);
|
||||||
var p3 = new SKPoint(0, 0);
|
var p3 = new SKPoint(0, 0);
|
||||||
var p4 = new SKPoint(0, 0);
|
var p4 = new SKPoint(0, 0);
|
||||||
|
|
||||||
var touchPoints = new SKPoint[] { p1, p2, p3, p4 };
|
var touchPoints = new SKPoint[] { p1, p2, p3, p4 };
|
||||||
|
|
||||||
using var bPen = new SKPaint();
|
using var bPen = new SKPaint();
|
||||||
@@ -78,76 +76,8 @@ namespace VolPro.Core.Utilities
|
|||||||
path.CubicTo(touchPoints[1], touchPoints[2], touchPoints[3]);
|
path.CubicTo(touchPoints[1], touchPoints[2], touchPoints[3]);
|
||||||
canvas.DrawPath(path, bPen);
|
canvas.DrawPath(path, bPen);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bitmap.ToBase64String(SKEncodedImageFormat.Png);
|
return bitmap.ToBase64String(SKEncodedImageFormat.Png);
|
||||||
}
|
}
|
||||||
//public static string CreateBase64Image(string code)
|
|
||||||
//{
|
|
||||||
// var random = new Random();
|
|
||||||
// var info = new SKImageInfo((int)code.Length * 18, 32);
|
|
||||||
// using var bitmap = new SKBitmap(info);
|
|
||||||
// using var canvas = new SKCanvas(bitmap);
|
|
||||||
|
|
||||||
// canvas.Clear(SKColors.White);
|
|
||||||
|
|
||||||
// using var pen = new SKPaint();
|
|
||||||
// pen.FakeBoldText = true;
|
|
||||||
// pen.Style = SKPaintStyle.Fill;
|
|
||||||
// pen.TextSize = 20;// 0.6f * info.Width * pen.TextSize / pen.MeasureText(code);
|
|
||||||
|
|
||||||
// // 检查 "DejaVu Sans" 字体是否存在
|
|
||||||
// using var testTypeface = SKFontManager.Default.MatchFamily("DejaVu Sans");
|
|
||||||
// if (testTypeface == null || string.IsNullOrEmpty(testTypeface.FamilyName))
|
|
||||||
// {
|
|
||||||
// throw new Exception("系统中未找到 'DejaVu Sans' 字体。");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// //绘制随机字符
|
|
||||||
// for (int i = 0; i < code.Length; i++)
|
|
||||||
// {
|
|
||||||
// pen.Color = random.GetRandom(colors);//随机颜色索引值
|
|
||||||
|
|
||||||
// pen.Typeface = SKTypeface.FromFamilyName("DejaVu Sans", 700, 20, SKFontStyleSlant.Italic);//配置字体
|
|
||||||
// var point = new SKPoint()
|
|
||||||
// {
|
|
||||||
// X = i * 16,
|
|
||||||
// Y = 22// info.Height - ((i + 1) % 2 == 0 ? 2 : 4),
|
|
||||||
|
|
||||||
// };
|
|
||||||
// canvas.DrawText(code.Substring(i, 1), point, pen);//绘制一个验证字符
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 绘制噪点
|
|
||||||
// var points = Enumerable.Range(0, 100).Select(
|
|
||||||
// _ => new SKPoint(random.Next(bitmap.Width), random.Next(bitmap.Height))
|
|
||||||
// ).ToArray();
|
|
||||||
// canvas.DrawPoints(
|
|
||||||
// SKPointMode.Points,
|
|
||||||
// points,
|
|
||||||
// pen);
|
|
||||||
|
|
||||||
// //绘制贝塞尔线条
|
|
||||||
// for (int i = 0; i < 2; i++)
|
|
||||||
// {
|
|
||||||
// var p1 = new SKPoint(0, 0);
|
|
||||||
// var p2 = new SKPoint(0, 0);
|
|
||||||
// var p3 = new SKPoint(0, 0);
|
|
||||||
// var p4 = new SKPoint(0, 0);
|
|
||||||
|
|
||||||
// var touchPoints = new SKPoint[] { p1, p2, p3, p4 };
|
|
||||||
|
|
||||||
// using var bPen = new SKPaint();
|
|
||||||
// bPen.Color = random.GetRandom(colors);
|
|
||||||
// bPen.Style = SKPaintStyle.Stroke;
|
|
||||||
|
|
||||||
// using var path = new SKPath();
|
|
||||||
// path.MoveTo(touchPoints[0]);
|
|
||||||
// path.CubicTo(touchPoints[1], touchPoints[2], touchPoints[3]);
|
|
||||||
// canvas.DrawPath(path, bPen);
|
|
||||||
// }
|
|
||||||
// return bitmap.ToBase64String(SKEncodedImageFormat.Png);
|
|
||||||
//}
|
|
||||||
|
|
||||||
public static T GetRandom<T>(this Random random, T[] tArray)
|
public static T GetRandom<T>(this Random random, T[] tArray)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -87,41 +87,6 @@ namespace VolPro.Entity.DomainModels
|
|||||||
[Editable(true)]
|
[Editable(true)]
|
||||||
public int? RuleID { get; set; }
|
public int? RuleID { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///数值型恢复阈值(如>28℃触发,≤26℃恢复)
|
|
||||||
/// </summary>
|
|
||||||
[Display(Name ="数值型恢复阈值(如>28℃触发,≤26℃恢复)")]
|
|
||||||
[DisplayFormat(DataFormatString="18,2")]
|
|
||||||
[Column(TypeName="decimal")]
|
|
||||||
[Editable(true)]
|
|
||||||
public decimal? RecoveryThreshold_Numeric { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///开关型恢复阈值(如开触发,关恢复)
|
|
||||||
/// </summary>
|
|
||||||
[Display(Name ="开关型恢复阈值(如开触发,关恢复)")]
|
|
||||||
[MaxLength(50)]
|
|
||||||
[Column(TypeName="nvarchar(50)")]
|
|
||||||
[Editable(true)]
|
|
||||||
public string RecoveryThreshold_Switch { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///该条件上次触发的时间
|
|
||||||
/// </summary>
|
|
||||||
[Display(Name ="该条件上次触发的时间")]
|
|
||||||
[Column(TypeName="datetime")]
|
|
||||||
[Editable(true)]
|
|
||||||
public DateTime? LastTriggered { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///该条件上次触发时的实际值
|
|
||||||
/// </summary>
|
|
||||||
[Display(Name ="该条件上次触发时的实际值")]
|
|
||||||
[DisplayFormat(DataFormatString="18,2")]
|
|
||||||
[Column(TypeName="decimal")]
|
|
||||||
[Editable(true)]
|
|
||||||
public decimal? LastTriggerValue { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace VolPro.WebApi.Controllers.Warehouse;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文件服务。对外暴露 VolPro 文件系统中的静态文件(截图、导出等)。
|
|
||||||
/// 不走 VolPro JWT 认证体系——网关 B 组接口直接调用。
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[AllowAnonymous]
|
|
||||||
public class FileServiceController : Controller
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取截图文件。
|
|
||||||
/// 文件存放于 VolPro.WebApi/Download/Screenshots/ 目录。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">文件名(含扩展名,如 abc.png)</param>
|
|
||||||
[HttpGet("api/gateway/screenshots/{filename}")]
|
|
||||||
public IActionResult GetScreenshot(string filename)
|
|
||||||
{
|
|
||||||
// 安全检查:禁止路径穿越(.., /, \)
|
|
||||||
if (string.IsNullOrWhiteSpace(filename) ||
|
|
||||||
filename.Contains("..") ||
|
|
||||||
filename.Contains('/') ||
|
|
||||||
filename.Contains('\\'))
|
|
||||||
return BadRequest(new { error = "非法文件名" });
|
|
||||||
|
|
||||||
var folder = Path.Combine(AppContext.BaseDirectory, "Download", "Screenshots");
|
|
||||||
var filePath = Path.Combine(folder, filename);
|
|
||||||
|
|
||||||
if (!System.IO.File.Exists(filePath))
|
|
||||||
return NotFound(new { error = "文件不存在" });
|
|
||||||
|
|
||||||
var ext = Path.GetExtension(filename).ToLowerInvariant();
|
|
||||||
var contentType = ext switch
|
|
||||||
{
|
|
||||||
".png" => "image/png",
|
|
||||||
".jpg" or ".jpeg" => "image/jpeg",
|
|
||||||
".gif" => "image/gif",
|
|
||||||
_ => "application/octet-stream"
|
|
||||||
};
|
|
||||||
|
|
||||||
return PhysicalFile(filePath, contentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
/*
|
|
||||||
*设备管理扩展 — 区域树 + 点位设备列表
|
|
||||||
*所有改动在 Partial 目录,不破坏框架可升级性
|
|
||||||
*/
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using VolPro.Core.BaseProvider;
|
|
||||||
using VolPro.Core.Configuration;
|
|
||||||
using VolPro.Core.Enums;
|
|
||||||
using VolPro.Core.Extensions;
|
|
||||||
using VolPro.Core.Filters;
|
|
||||||
using VolPro.Core.ManageUser;
|
|
||||||
using VolPro.Entity.DomainModels;
|
|
||||||
using VolPro.Sys.IRepositories;
|
|
||||||
using VolPro.Sys.Repositories;
|
|
||||||
using Warehouse.IRepositories;
|
|
||||||
using Warehouse.IServices;
|
|
||||||
using Warehouse.Repositories;
|
|
||||||
|
|
||||||
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 Ibase_deviceRepository _repository;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
|
|
||||||
[ActivatorUtilitiesConstructor]
|
|
||||||
public base_deviceController(
|
|
||||||
Ibase_deviceService service,
|
|
||||||
Iwarehouse_regionsService regionsService,
|
|
||||||
Iwarehouse_devicepointService pointService,
|
|
||||||
Ibase_deviceRepository repository,
|
|
||||||
IHttpContextAccessor httpContextAccessor
|
|
||||||
)
|
|
||||||
: base(service)
|
|
||||||
{
|
|
||||||
_service = service;
|
|
||||||
_regionsService = regionsService;
|
|
||||||
_pointService = pointService;
|
|
||||||
_repository = repository;
|
|
||||||
_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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// treetable 获取子节点数据(2021.05.02)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="loadData"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[ApiActionPermission(ActionPermissionOptions.Search)]
|
|
||||||
[HttpPost, Route("GetPageData")]
|
|
||||||
public override ActionResult GetPageData([FromBody] PageDataOptions loadData)
|
|
||||||
{
|
|
||||||
return GetTreeTableRootData(loadData).Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// treetable 获取一级(根)节点数据
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPost, Route("getTreeTableRootData")]
|
|
||||||
[ApiActionPermission(ActionPermissionOptions.Search)]
|
|
||||||
public async Task<ActionResult> GetTreeTableRootData([FromBody] PageDataOptions options)
|
|
||||||
{
|
|
||||||
//页面加载根节点数据条件x => x.ParentId == 0,自己根据需要设置
|
|
||||||
var dbServiceId = UserContext.CurrentServiceId;
|
|
||||||
var query = _repository.FindAsIQueryable(x => x.ParentDeviceId == 0 || x.DeviceId == 1);
|
|
||||||
|
|
||||||
var queryChild = _repository.FindAsIQueryable(x => true);
|
|
||||||
|
|
||||||
// 先获取总数
|
|
||||||
var total = await query.CountAsync();
|
|
||||||
|
|
||||||
// 按需先排序,再 Skip/Take 分页
|
|
||||||
var rows = await query
|
|
||||||
.OrderBy(x => x.DeviceId)
|
|
||||||
.Skip((options.Page - 1) * options.Rows)
|
|
||||||
.Take(options.Rows)
|
|
||||||
.Select(s => new
|
|
||||||
{
|
|
||||||
s.DeviceId,
|
|
||||||
s.ParentDeviceId,
|
|
||||||
s.DeviceName,
|
|
||||||
s.AdapterCode,
|
|
||||||
s.SourceId,
|
|
||||||
s.DeviceCategory,
|
|
||||||
s.DeviceGroup,
|
|
||||||
s.IsParent,
|
|
||||||
s.IsOnline,
|
|
||||||
s.IpAddress,
|
|
||||||
s.Port,
|
|
||||||
s.Location,
|
|
||||||
s.ExtraData,
|
|
||||||
s.LastSyncTime,
|
|
||||||
s.MapModelId,
|
|
||||||
s.MapModelScale,
|
|
||||||
s.MapModelRotation,
|
|
||||||
s.Enable,
|
|
||||||
s.CreateDate,
|
|
||||||
s.Creator,
|
|
||||||
s.Modifier,
|
|
||||||
s.ModifyDate,
|
|
||||||
hasChildren = SqlSugar.SqlFunc.Subqueryable<base_device>().Where(x => x.ParentDeviceId == s.DeviceId).Any()
|
|
||||||
}).ToListAsync();
|
|
||||||
|
|
||||||
return JsonNormal(new { total, rows });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///treetable 获取子节点数据
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPost, Route("getTreeTableChildrenData")]
|
|
||||||
[ApiActionPermission(ActionPermissionOptions.Search)]
|
|
||||||
public async Task<ActionResult> GetTreeTableChildrenData(int deviceId)
|
|
||||||
{
|
|
||||||
//点击节点时,加载子节点数据
|
|
||||||
var basedeviceRepository = base_deviceRepository.Instance.FindAsIQueryable(x => true);
|
|
||||||
var query = basedeviceRepository.Where(x => x.ParentDeviceId == deviceId);
|
|
||||||
//if (AppSetting.UseDynamicShareDB)
|
|
||||||
//{
|
|
||||||
// query = query.Where(x => x.DbServiceId == UserContext.CurrentServiceId);
|
|
||||||
//}
|
|
||||||
var rows = await query
|
|
||||||
.Select(s => new
|
|
||||||
{
|
|
||||||
s.DeviceId,
|
|
||||||
s.ParentDeviceId,
|
|
||||||
s.DeviceName,
|
|
||||||
s.AdapterCode,
|
|
||||||
s.SourceId,
|
|
||||||
s.DeviceCategory,
|
|
||||||
s.DeviceGroup,
|
|
||||||
s.IsParent,
|
|
||||||
s.IsOnline,
|
|
||||||
s.IpAddress,
|
|
||||||
s.Port,
|
|
||||||
s.Location,
|
|
||||||
s.ExtraData,
|
|
||||||
s.LastSyncTime,
|
|
||||||
s.MapModelId,
|
|
||||||
s.MapModelScale,
|
|
||||||
s.MapModelRotation,
|
|
||||||
s.Enable,
|
|
||||||
s.CreateDate,
|
|
||||||
s.Creator,
|
|
||||||
s.Modifier,
|
|
||||||
s.ModifyDate,
|
|
||||||
hasChildren = SqlSugar.SqlFunc.Subqueryable<base_device>().Where(x => x.ParentDeviceId == s.DeviceId).Any()
|
|
||||||
}).ToListAsync();
|
|
||||||
return JsonNormal(new { rows });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,80 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using VolPro.Core.Filters;
|
|
||||||
using Warehouse.IServices;
|
|
||||||
|
|
||||||
namespace Warehouse.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定时任务 API 端点。
|
|
||||||
/// Vol.Pro 框架通过 Sys_QuartzOptions 表配置 URL+Cron 定时调用。
|
|
||||||
/// 每个方法加 [ApiTask] 属性以允许框架匿名调用。
|
|
||||||
///
|
|
||||||
/// 不在 Controller 层注入具体业务类——通过 HttpContext.RequestServices 按需解析,
|
|
||||||
/// 避免 Controller 构造函数的 DI 依赖链过长。
|
|
||||||
/// </summary>
|
|
||||||
[Route("api/task")]
|
|
||||||
public class TaskController : Controller
|
|
||||||
{
|
|
||||||
/// <summary>T1: 设备同步 — 遍历在线网关触发全量设备同步(每5分钟)</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("syncDevices")]
|
|
||||||
public async Task<IActionResult> SyncDevices()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
if (sp.GetService<Igateway_nodesService>() == null)
|
|
||||||
return StatusCode(500, new { error = "服务未注册: gateway_nodesService" });
|
|
||||||
|
|
||||||
// 复用 SyncDevicesJob 的核心流程(Job 内部自行创建 GatewayClient)
|
|
||||||
var job = new VolPro.Warehouse.Services.SyncDevicesJob(sp);
|
|
||||||
await job.Execute(null!);
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>T2: 心跳监控 — 扫描超时网关标记离线(每15秒)</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("heartbeatMonitor")]
|
|
||||||
public async Task<IActionResult> HeartbeatMonitor()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
var gwSvc = sp.GetService<Igateway_nodesService>();
|
|
||||||
if (gwSvc == null)
|
|
||||||
return StatusCode(500, new { error = "服务未注册: gateway_nodesService" });
|
|
||||||
|
|
||||||
var job = new VolPro.Warehouse.Services.HeartbeatMonitorJob(sp);
|
|
||||||
await job.Execute(null!);
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>T3: 实时轮询 — 拉取 MC4 IoT 实时值(每10秒)</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("realtimePoll")]
|
|
||||||
public async Task<IActionResult> RealtimePoll()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
var gwSvc = sp.GetService<Igateway_nodesService>();
|
|
||||||
if (gwSvc == null)
|
|
||||||
return StatusCode(500, new { error = "服务未注册: gateway_nodesService" });
|
|
||||||
|
|
||||||
var job = new VolPro.Warehouse.Services.RealtimePollJob(sp);
|
|
||||||
await job.Execute(null!);
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>T4: 规则引擎 — 评估规则+执行动作(每10秒)</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("ruleEngine")]
|
|
||||||
public async Task<IActionResult> RuleEngine()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
var ruleRepo = sp.GetService<Warehouse.IRepositories.Iwarehouse_ruleRepository>();
|
|
||||||
if (ruleRepo == null)
|
|
||||||
return StatusCode(500, new { error = "服务未注册: Iwarehouse_ruleRepository" });
|
|
||||||
|
|
||||||
var engine = new Warehouse.Services.RuleEngineService(ruleRepo);
|
|
||||||
await engine.EvaluateAllAsync();
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
中文提示 : 检测到你没有开启文件,AllowLoadLocalInfile=true加到自符串上,已自动执行 SET GLOBAL local_infile=1 在试一次
|
|
||||||
English Message : Loading local data is disabled; this must be enabled on both the client and server sides at SqlSugar.Check.ExceptionEasy(String enMessage, String cnMessage)
|
|
||||||
at SqlSugar.MySqlFastBuilder.ExecuteBulkCopyAsync(DataTable dt)
|
|
||||||
at SqlSugar.FastestProvider`1._BulkCopy(List`1 datas)
|
|
||||||
at SqlSugar.FastestProvider`1.BulkCopyAsync(List`1 datas)
|
|
||||||
at SqlSugar.FastestProvider`1.BulkCopy(List`1 datas)
|
|
||||||
at VolPro.Core.Services.Logger.Start() in D:\Code\SecMPS\api_sqlsugar\VolPro.Core\Services\Logger.cs:line 194SqlSugar
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
中文提示 : 检测到你没有开启文件,AllowLoadLocalInfile=true加到自符串上,已自动执行 SET GLOBAL local_infile=1 在试一次
|
|
||||||
English Message : Loading local data is disabled; this must be enabled on both the client and server sides at SqlSugar.Check.ExceptionEasy(String enMessage, String cnMessage)
|
|
||||||
at SqlSugar.MySqlFastBuilder.ExecuteBulkCopyAsync(DataTable dt)
|
|
||||||
at SqlSugar.FastestProvider`1._BulkCopy(List`1 datas)
|
|
||||||
at SqlSugar.FastestProvider`1.BulkCopyAsync(List`1 datas)
|
|
||||||
at SqlSugar.FastestProvider`1.BulkCopy(List`1 datas)
|
|
||||||
at VolPro.Core.Services.Logger.Start() in D:\Code\SecMPS\api_sqlsugar\VolPro.Core\Services\Logger.cs:line 194SqlSugar
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
中文提示 : 连接数据库过程中发生错误,检查服务器是否正常连接字符串是否正确,错误信息:Connect Timeout expired.DbType="MySql";ConfigId="default".
|
|
||||||
English Message : Connection open error . Connect Timeout expired.DbType="MySql";ConfigId="default" at SqlSugar.Check.Exception(Boolean isException, String message, String[] args)
|
|
||||||
at SqlSugar.AdoProvider.CheckConnection()
|
|
||||||
at SqlSugar.AdoProvider.Open()
|
|
||||||
at SqlSugar.MySqlFastBuilder.ExecuteBulkCopyAsync(DataTable dt)
|
|
||||||
at SqlSugar.FastestProvider`1._BulkCopy(List`1 datas)
|
|
||||||
at SqlSugar.FastestProvider`1.BulkCopyAsync(List`1 datas)
|
|
||||||
at SqlSugar.FastestProvider`1.BulkCopy(List`1 datas)
|
|
||||||
at VolPro.Core.Services.Logger.Start() in D:\Code\SecMPS\api_sqlsugar\VolPro.Core\Services\Logger.cs:line 194SqlSugar
|
|
||||||
@@ -4,18 +4,13 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
-->
|
-->
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DeleteExistingFiles>true</DeleteExistingFiles>
|
<DeleteExistingFiles>False</DeleteExistingFiles>
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
<ExcludeApp_Data>False</ExcludeApp_Data>
|
||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
|
||||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||||
<PublishProvider>FileSystem</PublishProvider>
|
<PublishProvider>FileSystem</PublishProvider>
|
||||||
<PublishUrl>bin\Release\netcoreapp3.1\net6.0\publish\</PublishUrl>
|
<PublishUrl>bin\Release\netcoreapp3.1\net6.0\publish\</PublishUrl>
|
||||||
<WebPublishMethod>FileSystem</WebPublishMethod>
|
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||||
<SiteUrlToLaunchAfterPublish />
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
|
|
||||||
<ProjectGuid>4db3c91b-93fe-4937-8b58-ddd3f57d4607</ProjectGuid>
|
|
||||||
<SelfContained>true</SelfContained>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -69,14 +69,6 @@ namespace VolPro.WebApi
|
|||||||
services.AddSession();
|
services.AddSession();
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
// ── 网关集成: HttpClient + GatewayClient 注册 ──
|
|
||||||
services.AddHttpClient("VolPro", c =>
|
|
||||||
{
|
|
||||||
c.Timeout = TimeSpan.FromSeconds(30);
|
|
||||||
c.DefaultRequestHeaders.Add("Accept", "application/json");
|
|
||||||
});
|
|
||||||
services.AddSingleton<VolPro.Warehouse.Services.GatewayClient>();
|
|
||||||
|
|
||||||
services.AddMvc(options =>
|
services.AddMvc(options =>
|
||||||
{
|
{
|
||||||
options.Filters.Add(typeof(ApiAuthorizeFilter));
|
options.Filters.Add(typeof(ApiAuthorizeFilter));
|
||||||
|
|||||||
@@ -59,11 +59,5 @@
|
|||||||
<None Include="Startup copy.cs" />
|
<None Include="Startup copy.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="fonts\DejaVuSans.ttf">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,148 +1,140 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"VirtualPath": {
|
|
||||||
"StaticFile": "E:\\Web\\Static", //配置的虚拟目录文件所在路径
|
|
||||||
"FolderName": "/Static" //访问时此路径时的别名
|
|
||||||
},
|
|
||||||
"AppUrls": {
|
|
||||||
},
|
|
||||||
"Connection": {
|
|
||||||
//"DBType": "MsSql", //MySql/MsSql/PgSql/Oracle/Kdbndp //数据库类型,如果使用的是sqlserver此处应设置为MsSql
|
|
||||||
// sqlserver系统库
|
|
||||||
//"DbConnectionString": "Data Source=localhost;Initial Catalog=vol_pro_main;Persist Security Info=True;User ID=sa;Password=2w1q821130@W!Q;Connect Timeout=500;Encrypt=True;TrustServerCertificate=True;",
|
|
||||||
////业务库1(与EFDBContext文件夹中一致)
|
|
||||||
//"ServiceDbContext": "Data Source=localhost;Initial Catalog=vol_pro_service;Persist Security Info=True;User ID=sa;Password=2w1q821130@W!Q;Connect Timeout=500;Encrypt=True;TrustServerCertificate=True;",
|
|
||||||
///业务库2(与EFDBContext文件夹中一致)
|
|
||||||
//"TestDbContext": "Data Source=localhost;Initial Catalog=vol_pro_test;Persist Security Info=True;User ID=sa;Password=123456;Connect Timeout=500;",
|
|
||||||
//业务库3(与EFDBContext文件夹中一致)
|
|
||||||
// "自定义DbContext": "Data Source=127.0.0.1;Initial Catalog=vol_pro_test;Persist Security Info=True;User ID=sa;Password=127.0.0.1;Connect Timeout=500;",
|
|
||||||
|
|
||||||
////mysql系统库连接字符串=======================================================
|
|
||||||
//"DBType": "MySql",
|
|
||||||
//"DbConnectionString": " Data Source=localhost;Database=gljs_main;User ID=gljs;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
|
||||||
////业务库1(与EFDBContext文件夹中一致)
|
|
||||||
//"ServiceDbContext": " Data Source=localhost;Database=gljs_service;User ID=gljs;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
|
||||||
////业务库2(与EFDBContext文件夹中一致)
|
|
||||||
//"TestDbContext": " Data Source=localhost;Database=gljs_test;User ID=gljs;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
|
||||||
|
|
||||||
"DBType": "MySql",
|
|
||||||
"DbConnectionString": "Data Source=192.168.3.108;Database=gljs_main;User ID=root;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
|
||||||
//业务库1(与EFDBContext文件夹中一致)
|
|
||||||
"ServiceDbContext": "Data Source=192.168.3.108;Database=gljs_service;User ID=root;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
|
||||||
//业务库2(与EFDBContext文件夹中一致)
|
|
||||||
"TestDbContext": "Data Source=192.168.3.108;Database=gljs_test;User ID=root;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
|
||||||
|
|
||||||
////PgSql系统库连接字符串
|
|
||||||
// "DbConnectionString": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_main;",
|
|
||||||
////业务库1(与EFDBContext文件夹中一致)
|
|
||||||
// "ServiceDbContext": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_service;",
|
|
||||||
// //业务库2(与EFDBContext文件夹中一致)
|
|
||||||
// "TestDbContext": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_test;",
|
|
||||||
|
|
||||||
|
|
||||||
////人大金仓系统库连接字符串=================================================
|
|
||||||
// "DbConnectionString": "Host=127.0.0.1;Port=54321;User id=postgres;password=127.0.0.1;Database=vol_pro_main;",
|
|
||||||
////业务库1(与EFDBContext文件夹中一致)
|
|
||||||
// "ServiceDbContext": "Host=127.0.0.1;Port=54321;User id=postgres;password=127.0.0.1;Database=vol_pro_service;",
|
|
||||||
// //业务库2(与EFDBContext文件夹中一致)
|
|
||||||
// "TestDbContext": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_test;",
|
|
||||||
|
|
||||||
//Oracle连接字符串
|
|
||||||
//"DbConnectionString": "user id=C##VOL_PRO_MAIN;data source=127.0.0.1/ORCL;password=123456;",
|
|
||||||
|
|
||||||
|
|
||||||
"RedisConnectionString": "127.0.0.1,Password=123456,SyncTimeout=15000", //redis连接字符串
|
|
||||||
"UseRedis": "false", //是否使用redis,如果不使用,默认使用Memory内置缓存
|
|
||||||
"UseSignalR": "true" //是否使用SignalR(2022.05.03),注意需要将端的地址配置到下面的CorsUrls属性中
|
|
||||||
},
|
|
||||||
"Secret": { //秘钥配置
|
|
||||||
"JWT": "BB3647441FFA4B5DB4E64A29B53CE525", //JWT这里请一定要修改(随便换个值)
|
|
||||||
"Audience": "vol.core",
|
|
||||||
"Issuer": "VolPro.core.owner",
|
|
||||||
"User": "C5ABA9E202D94C43A3CA66002BF77FAF", //用户表加密key,这里请一定要修改(随便换个值),修改后打开Sys_UserSerivce.cs中login方法,判断密码那行注掉,登录后修改密码再取消注释
|
|
||||||
"DB": "",
|
|
||||||
"Redis": "E6D90DDBC70C4F4EA3C312B6FCB473C8"
|
|
||||||
},
|
|
||||||
//多个url用豆号隔开,url为vue站点的地址
|
|
||||||
"CorsUrls": "http://localhost:9000,http://127.0.0.1:9000,http://192.168.2.201:9000,http://192.168.2.200:9000",
|
|
||||||
"ExpMinutes": "120", //JWT有效期(分钟=默认120),
|
|
||||||
"CreateMember": { //对表插入数据时,需要记录创建人/创建时间/创建日期,配置UserIdField/UserNameField/DateField分别为对应数据库的创建人CreateID,创建人Creator,创建时间CreateDate字段(新建数据时,由框架默认完成给这几个字段赋值,字段区分大小写)或可手动调用T.SetCreateDefaultVal()完成设置创建人/创建时间/创建日期
|
|
||||||
//如果表的主键是GUID,界面查询时默认会用到DateField对应的实体(数据库)字段进行排序
|
|
||||||
"UserIdField": "CreateID",
|
|
||||||
"UserNameField": "Creator",
|
|
||||||
"DateField": "CreateDate"
|
|
||||||
},
|
|
||||||
"ModifyMember": { //修改同上
|
|
||||||
"UserIdField": "ModifyID",
|
|
||||||
"UserNameField": "Modifier",
|
|
||||||
"DateField": "ModifyDate"
|
|
||||||
}, //演示系统过滤Action,只有超级管理员才能操作,其他用户只有只读权限
|
|
||||||
"GlobalFilter": {
|
|
||||||
"Message": "演示环境,当前帐号没有开启此功能权限",
|
|
||||||
"Enable": "false", //开启Action过滤
|
|
||||||
"Actions": [ "Update", "Del", "Add", "SavePermission", "Save", "ExecSql", "CreatePage", "CreateVuePage", "CreateEntityModel", "SaveEidt", "CreateServices", "Import", "Upload", "Audit", "ModifyPwd" ]
|
|
||||||
},
|
|
||||||
"Kafka": {
|
|
||||||
//是否使用生产者
|
|
||||||
"UseProducer": false,
|
|
||||||
"ProducerSettings": {
|
|
||||||
"BootstrapServers": "192.168.20.241:9092", //confluent cloud bootstrap servers
|
|
||||||
"SaslMechanism": "Plain",
|
|
||||||
"SecurityProtocol": "SaslSsl",
|
|
||||||
"SaslUsername": "<confluent cloud key>",
|
|
||||||
"SaslPassword": "<confluent cloud secret>"
|
|
||||||
},
|
},
|
||||||
//是否使用消费者
|
"AllowedHosts": "*",
|
||||||
"UseConsumer": false,
|
"VirtualPath": {
|
||||||
//是否持续监听消费者订阅 用于while循环订阅
|
"StaticFile": "E:\\Web\\Static", //配置的虚拟目录文件所在路径
|
||||||
"IsConsumerSubscribe": true,
|
"FolderName": "/Static" //访问时此路径时的别名
|
||||||
"ConsumerSettings": {
|
|
||||||
"BootstrapServers": "192.168.20.241:9092", //confluent cloud bootstrap servers
|
|
||||||
"GroupId": "amcl_group", //web-example-group
|
|
||||||
"SaslMechanism": "Plain",
|
|
||||||
"SecurityProtocol": "SaslSsl",
|
|
||||||
"SaslUsername": "<confluent cloud key>",
|
|
||||||
"SaslPassword": "<confluent cloud secret>"
|
|
||||||
},
|
},
|
||||||
"Topics": {
|
"AppUrls": {
|
||||||
"TestTopic": "alarm_topic"
|
},
|
||||||
}
|
"Connection": {
|
||||||
},
|
//"DBType": "MsSql", //MySql/MsSql/PgSql/Oracle/Kdbndp //数据库类型,如果使用的是sqlserver此处应设置为MsSql
|
||||||
"Mail": {
|
// sqlserver系统库
|
||||||
"Address": "code283591387@163.com", //发件的邮箱
|
//"DbConnectionString": "Data Source=localhost;Initial Catalog=vol_pro_main;Persist Security Info=True;User ID=sa;Password=2w1q821130@W!Q;Connect Timeout=500;Encrypt=True;TrustServerCertificate=True;",
|
||||||
"Host": "smtp.163.com",
|
////业务库1(与EFDBContext文件夹中一致)
|
||||||
"Name": "VOL", //发送人名称
|
//"ServiceDbContext": "Data Source=localhost;Initial Catalog=vol_pro_service;Persist Security Info=True;User ID=sa;Password=2w1q821130@W!Q;Connect Timeout=500;Encrypt=True;TrustServerCertificate=True;",
|
||||||
"Port": 25,
|
///业务库2(与EFDBContext文件夹中一致)
|
||||||
"EnableSsl": false,
|
//"TestDbContext": "Data Source=localhost;Initial Catalog=vol_pro_test;Persist Security Info=True;User ID=sa;Password=123456;Connect Timeout=500;",
|
||||||
"AuthPwd": "授权密码" //授权密码(对应邮箱设置里面去开启)
|
//业务库3(与EFDBContext文件夹中一致)
|
||||||
},
|
// "自定义DbContext": "Data Source=127.0.0.1;Initial Catalog=vol_pro_test;Persist Security Info=True;User ID=sa;Password=127.0.0.1;Connect Timeout=500;",
|
||||||
"android": {
|
|
||||||
"version": "1.0.0",
|
////mysql系统库连接字符串=======================================================
|
||||||
"url": "", //将打包后的安卓apk放在后台wwwroot的app文件夹下
|
"DBType": "MySql",
|
||||||
"desc": ""
|
"DbConnectionString": " Data Source=localhost;Database=gljs_main;User ID=gljs;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
||||||
},
|
//业务库1(与EFDBContext文件夹中一致)
|
||||||
"ios": {
|
"ServiceDbContext": " Data Source=localhost;Database=gljs_service;User ID=gljs;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
||||||
"version": "1.0.2",
|
//业务库2(与EFDBContext文件夹中一致)
|
||||||
"url": "itms-apps://itunes.apple.com/cn/app/123456?mt=8",
|
"TestDbContext": " Data Source=localhost;Database=gljs_test;User ID=gljs;Password=2w1q821130@W!Q;AllowLoadLocalInfile=true;",
|
||||||
"desc": ""
|
////PgSql系统库连接字符串
|
||||||
},
|
// "DbConnectionString": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_main;",
|
||||||
"UseSnow": "0", //是否使用雪花算法(表的主键字段为bigint类型时启用雪花算法生成唯一id; 1=是,0=否)
|
////业务库1(与EFDBContext文件夹中一致)
|
||||||
"QuartzAccessKey": "65EC9387355E4717899C552963CE59X1", //定时任务的值,请自行修改
|
// "ServiceDbContext": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_service;",
|
||||||
"LogicDelField": "IsDel", //逻辑删除字段(对应表字段,逻辑删除只会将字段的值设置为1,默认是0)
|
// //业务库2(与EFDBContext文件夹中一致)
|
||||||
"TenancyField": "", //表的租户字段(用于不分库租户数据隔离,如表字段:TenancyId)(使用动态分库功能此字段用不上)
|
// "TestDbContext": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_test;",
|
||||||
"UseDynamicShareDB": "0", //使用动态分库(每个客户对应一个独立数据库)
|
|
||||||
"DBPath": "E:\\db\\", //数据库所在的磁盘目录(动态生成数据库时使用,注意:所有数据库都必须在同一个目录,并且把db文件夹下的Db_Empty库也要创建)
|
|
||||||
"DBBackPath": "E:\\db\\dbbak", //数据库备份磁盘所在目录(动态生成数据库时使用)
|
////人大金仓系统库连接字符串=================================================
|
||||||
"UserAuth": "0", //是否使用用户权限(限制只能看到指定用户创建的数据,用户管理页面的操作列可以看到此功能,设置为1后生效)
|
// "DbConnectionString": "Host=127.0.0.1;Port=54321;User id=postgres;password=127.0.0.1;Database=vol_pro_main;",
|
||||||
"ModelInService": "0", //表的model类是否生成到当前业务类库下(默认都在VolPro.Entity)
|
////业务库1(与EFDBContext文件夹中一致)
|
||||||
"FileAuth": "0", //2023.12.25所有静态文件访问授权
|
// "ServiceDbContext": "Host=127.0.0.1;Port=54321;User id=postgres;password=127.0.0.1;Database=vol_pro_service;",
|
||||||
"montior": "0", //开启服务器性能监控(1=开启)
|
// //业务库2(与EFDBContext文件夹中一致)
|
||||||
"DbTable": "0" //开启界面上数据库表维护功能(1=开启)
|
// "TestDbContext": "Host=127.0.0.1;Port=5432;User id=postgres;password=127.0.0.1;Database=vol_pro_test;",
|
||||||
|
|
||||||
|
//Oracle连接字符串
|
||||||
|
//"DbConnectionString": "user id=C##VOL_PRO_MAIN;data source=127.0.0.1/ORCL;password=123456;",
|
||||||
|
|
||||||
|
|
||||||
|
"RedisConnectionString": "127.0.0.1,Password=123456,SyncTimeout=15000", //redis连接字符串
|
||||||
|
"UseRedis": "false", //是否使用redis,如果不使用,默认使用Memory内置缓存
|
||||||
|
"UseSignalR": "true" //是否使用SignalR(2022.05.03),注意需要将端的地址配置到下面的CorsUrls属性中
|
||||||
|
},
|
||||||
|
"Secret": { //秘钥配置
|
||||||
|
"JWT": "BB3647441FFA4B5DB4E64A29B53CE525", //JWT这里请一定要修改(随便换个值)
|
||||||
|
"Audience": "vol.core",
|
||||||
|
"Issuer": "VolPro.core.owner",
|
||||||
|
"User": "C5ABA9E202D94C43A3CA66002BF77FAF", //用户表加密key,这里请一定要修改(随便换个值),修改后打开Sys_UserSerivce.cs中login方法,判断密码那行注掉,登录后修改密码再取消注释
|
||||||
|
"DB": "",
|
||||||
|
"Redis": "E6D90DDBC70C4F4EA3C312B6FCB473C8"
|
||||||
|
},
|
||||||
|
//多个url用豆号隔开,url为vue站点的地址
|
||||||
|
"CorsUrls": "http://localhost:9000,http://127.0.0.1:9000,http://192.168.2.201:9000,http://192.168.2.200:9000",
|
||||||
|
"ExpMinutes": "120", //JWT有效期(分钟=默认120),
|
||||||
|
"CreateMember": { //对表插入数据时,需要记录创建人/创建时间/创建日期,配置UserIdField/UserNameField/DateField分别为对应数据库的创建人CreateID,创建人Creator,创建时间CreateDate字段(新建数据时,由框架默认完成给这几个字段赋值,字段区分大小写)或可手动调用T.SetCreateDefaultVal()完成设置创建人/创建时间/创建日期
|
||||||
|
//如果表的主键是GUID,界面查询时默认会用到DateField对应的实体(数据库)字段进行排序
|
||||||
|
"UserIdField": "CreateID",
|
||||||
|
"UserNameField": "Creator",
|
||||||
|
"DateField": "CreateDate"
|
||||||
|
},
|
||||||
|
"ModifyMember": { //修改同上
|
||||||
|
"UserIdField": "ModifyID",
|
||||||
|
"UserNameField": "Modifier",
|
||||||
|
"DateField": "ModifyDate"
|
||||||
|
}, //演示系统过滤Action,只有超级管理员才能操作,其他用户只有只读权限
|
||||||
|
"GlobalFilter": {
|
||||||
|
"Message": "演示环境,当前帐号没有开启此功能权限",
|
||||||
|
"Enable": "false", //开启Action过滤
|
||||||
|
"Actions": [ "Update", "Del", "Add", "SavePermission", "Save", "ExecSql", "CreatePage", "CreateVuePage", "CreateEntityModel", "SaveEidt", "CreateServices", "Import", "Upload", "Audit", "ModifyPwd" ]
|
||||||
|
},
|
||||||
|
"Kafka": {
|
||||||
|
//是否使用生产者
|
||||||
|
"UseProducer": false,
|
||||||
|
"ProducerSettings": {
|
||||||
|
"BootstrapServers": "192.168.20.241:9092", //confluent cloud bootstrap servers
|
||||||
|
"SaslMechanism": "Plain",
|
||||||
|
"SecurityProtocol": "SaslSsl",
|
||||||
|
"SaslUsername": "<confluent cloud key>",
|
||||||
|
"SaslPassword": "<confluent cloud secret>"
|
||||||
|
},
|
||||||
|
//是否使用消费者
|
||||||
|
"UseConsumer": false,
|
||||||
|
//是否持续监听消费者订阅 用于while循环订阅
|
||||||
|
"IsConsumerSubscribe": true,
|
||||||
|
"ConsumerSettings": {
|
||||||
|
"BootstrapServers": "192.168.20.241:9092", //confluent cloud bootstrap servers
|
||||||
|
"GroupId": "amcl_group", //web-example-group
|
||||||
|
"SaslMechanism": "Plain",
|
||||||
|
"SecurityProtocol": "SaslSsl",
|
||||||
|
"SaslUsername": "<confluent cloud key>",
|
||||||
|
"SaslPassword": "<confluent cloud secret>"
|
||||||
|
},
|
||||||
|
"Topics": {
|
||||||
|
"TestTopic": "alarm_topic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Mail": {
|
||||||
|
"Address": "code283591387@163.com", //发件的邮箱
|
||||||
|
"Host": "smtp.163.com",
|
||||||
|
"Name": "VOL", //发送人名称
|
||||||
|
"Port": 25,
|
||||||
|
"EnableSsl": false,
|
||||||
|
"AuthPwd": "授权密码" //授权密码(对应邮箱设置里面去开启)
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"url": "", //将打包后的安卓apk放在后台wwwroot的app文件夹下
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"url": "itms-apps://itunes.apple.com/cn/app/123456?mt=8",
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
"UseSnow": "0", //是否使用雪花算法(表的主键字段为bigint类型时启用雪花算法生成唯一id; 1=是,0=否)
|
||||||
|
"QuartzAccessKey": "65EC9387355E4717899C552963CE59X1", //定时任务的值,请自行修改
|
||||||
|
"LogicDelField": "IsDel", //逻辑删除字段(对应表字段,逻辑删除只会将字段的值设置为1,默认是0)
|
||||||
|
"TenancyField": "", //表的租户字段(用于不分库租户数据隔离,如表字段:TenancyId)(使用动态分库功能此字段用不上)
|
||||||
|
"UseDynamicShareDB": "0", //使用动态分库(每个客户对应一个独立数据库)
|
||||||
|
"DBPath": "E:\\db\\", //数据库所在的磁盘目录(动态生成数据库时使用,注意:所有数据库都必须在同一个目录,并且把db文件夹下的Db_Empty库也要创建)
|
||||||
|
"DBBackPath": "E:\\db\\dbbak", //数据库备份磁盘所在目录(动态生成数据库时使用)
|
||||||
|
"UserAuth": "0", //是否使用用户权限(限制只能看到指定用户创建的数据,用户管理页面的操作列可以看到此功能,设置为1后生效)
|
||||||
|
"ModelInService": "0", //表的model类是否生成到当前业务类库下(默认都在VolPro.Entity)
|
||||||
|
"FileAuth": "0", //2023.12.25所有静态文件访问授权
|
||||||
|
"montior": "0", //开启服务器性能监控(1=开启)
|
||||||
|
"DbTable": "0" //开启界面上数据库表维护功能(1=开启)
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -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,77 +0,0 @@
|
|||||||
using Quartz;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Warehouse.IServices;
|
|
||||||
using VolPro.Entity.DomainModels;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Warehouse.IRepositories;
|
|
||||||
|
|
||||||
namespace VolPro.Warehouse.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 心跳超时检测任务。扫描心跳超时 30 秒的网关节点,标记为离线,
|
|
||||||
/// 并级联标记该节点下所有设备为离线。
|
|
||||||
/// Cron 建议: 每 15 秒 ("0/15 * * * * ?")
|
|
||||||
///
|
|
||||||
/// 设备与网关的关联通过 AdapterCode 前缀匹配(如设备 AdapterCode="MC4:31ku" 匹配网关 AdapterTypes="MC4:31ku")。
|
|
||||||
/// </summary>
|
|
||||||
public class HeartbeatMonitorJob : IJob
|
|
||||||
{
|
|
||||||
private readonly IServiceProvider _sp;
|
|
||||||
public HeartbeatMonitorJob(IServiceProvider sp) { _sp = sp; }
|
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext? context)
|
|
||||||
{
|
|
||||||
var sp = _sp;
|
|
||||||
if (sp == null) return;
|
|
||||||
|
|
||||||
var gwSvc = sp.GetService<Igateway_nodesService>();
|
|
||||||
var gwRepo = sp.GetService<Igateway_nodesRepository>();
|
|
||||||
var devRepo = sp.GetService<Ibase_deviceRepository>();
|
|
||||||
if (gwSvc == null || gwRepo == null || devRepo == null) return;
|
|
||||||
|
|
||||||
var timeout = DateTime.Now.AddSeconds(-30);
|
|
||||||
|
|
||||||
// 扫描心跳超时的网关(当前在线但心跳超时)
|
|
||||||
var offlineNodes = await gwSvc.FindAsIQueryable(
|
|
||||||
x => x.IsOnline == "在线" && x.LastHeartbeat < timeout).ToListAsync();
|
|
||||||
|
|
||||||
foreach (var node in offlineNodes)
|
|
||||||
{
|
|
||||||
// 标记网关离线
|
|
||||||
node.IsOnline = "离线";
|
|
||||||
try { gwRepo.Update(node); } catch { }
|
|
||||||
Console.WriteLine($"[HeartbeatMonitorJob] 网关 {node.NodeCode} 心跳超时,标记离线");
|
|
||||||
|
|
||||||
// 级联标记该网关下所有设备离线(批量 SQL)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var adapterPrefixes = (node.AdapterTypes ?? "")
|
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(t => t.Trim()).ToList();
|
|
||||||
|
|
||||||
if (adapterPrefixes.Any())
|
|
||||||
{
|
|
||||||
var allDevices = await devRepo.FindAsIQueryable(
|
|
||||||
d => d.IsOnline == "在线").ToListAsync();
|
|
||||||
var matched = allDevices
|
|
||||||
.Where(d => adapterPrefixes.Any(p => (d.AdapterCode ?? "").StartsWith(p)))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (matched.Any())
|
|
||||||
{
|
|
||||||
foreach (var dev in matched) dev.IsOnline = "离线";
|
|
||||||
devRepo.UpdateRange(matched);
|
|
||||||
Console.WriteLine($"[HeartbeatMonitorJob] 级联 {matched.Count} 台设备离线");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[HeartbeatMonitorJob] 级联离线失败: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
using Quartz;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using VolPro.Entity.DomainModels;
|
|
||||||
using Warehouse.IRepositories;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using System.Net.Http;
|
|
||||||
using Warehouse.IServices;
|
|
||||||
|
|
||||||
namespace VolPro.Warehouse.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 实时数据轮询任务。
|
|
||||||
/// 定时轮询在线 MC4 IoT 设备的实时值 → 写入 iot_devicedata 表。
|
|
||||||
/// Cron 建议: 每 10 秒 ("0/10 * * * * ?")
|
|
||||||
///
|
|
||||||
/// 设备与网关的关联通过 AdapterCode 前缀匹配(如设备 AdapterCode="MC4:31ku" 匹配网关 AdapterTypes="MC4:31ku")。
|
|
||||||
/// </summary>
|
|
||||||
public class RealtimePollJob : IJob
|
|
||||||
{
|
|
||||||
private readonly IServiceProvider _sp;
|
|
||||||
public RealtimePollJob(IServiceProvider sp) { _sp = sp; }
|
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext? context)
|
|
||||||
{
|
|
||||||
var sp = _sp;
|
|
||||||
if (sp == null) return;
|
|
||||||
|
|
||||||
var gwSvc = sp.GetService<Igateway_nodesService>();
|
|
||||||
var devRepo = sp.GetService<Ibase_deviceRepository>();
|
|
||||||
var dataRepo = sp.GetService<Iiot_devicedataRepository>();
|
|
||||||
var httpFactory = sp.GetService<IHttpClientFactory>();
|
|
||||||
var config = sp.GetService<IConfiguration>();
|
|
||||||
var gatewayClient = httpFactory != null ? new GatewayClient(httpFactory, config!) : null;
|
|
||||||
if (gwSvc == null || devRepo == null || dataRepo == null || gatewayClient == null) return;
|
|
||||||
|
|
||||||
// 1. 查在线 MC4 网关
|
|
||||||
var onlineNodes = await gwSvc.FindAsIQueryable(x =>
|
|
||||||
x.IsOnline == "在线" && x.AdapterTypes != null && x.AdapterTypes.Contains("MC4")).ToArrayAsync();
|
|
||||||
|
|
||||||
foreach (var node in onlineNodes)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var baseUrl = node.BaseUrl;
|
|
||||||
if (string.IsNullOrEmpty(baseUrl)) continue;
|
|
||||||
|
|
||||||
// 2. 解析网关管理的适配器前缀列表
|
|
||||||
var adapterPrefixes = (node.AdapterTypes ?? "")
|
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(t => t.Trim());
|
|
||||||
|
|
||||||
// 3. 查该网关下在线的 IoT 设备(AdapterCode 前缀匹配)
|
|
||||||
var devices = await devRepo.FindAsIQueryable(d =>
|
|
||||||
d.DeviceGroup == "IoT设备" && d.IsOnline == "在线").ToListAsync();
|
|
||||||
var matchedDevices = devices.Where(d =>
|
|
||||||
adapterPrefixes.Any(p => (d.AdapterCode ?? "").StartsWith(p))).ToList();
|
|
||||||
|
|
||||||
if (!matchedDevices.Any()) continue;
|
|
||||||
|
|
||||||
// 4. 逐设备调网关 B4 获取实时值
|
|
||||||
foreach (var dev in matchedDevices)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await gatewayClient.GetRealtimeAsync(baseUrl, dev.AdapterCode, dev.SourceId);
|
|
||||||
if (result == null) continue;
|
|
||||||
var root = result.RootElement;
|
|
||||||
var points = root.TryGetProperty("pointValues", out var pv) ? pv
|
|
||||||
: root.TryGetProperty("rows", out var r) ? r
|
|
||||||
: root;
|
|
||||||
// 结果可能是 PointValue[] 数组,取第一个点位写入
|
|
||||||
if (points.ValueKind == System.Text.Json.JsonValueKind.Array && points.GetArrayLength() > 0)
|
|
||||||
{
|
|
||||||
var first = points[0];
|
|
||||||
var entry = new iot_devicedata
|
|
||||||
{
|
|
||||||
DeviceId = dev.DeviceId,
|
|
||||||
PointValue = first.TryGetProperty("value", out var v) ? v.GetDecimal() : (decimal?)null,
|
|
||||||
UpdateTime = DateTime.Now,
|
|
||||||
Interval = first.TryGetProperty("interval", out var iv) ? iv.GetInt32() : 10
|
|
||||||
};
|
|
||||||
dataRepo.Add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* 单设备失败不阻塞其他设备 */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* 单网关失败不阻塞其他网关 */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// ═══════════════════════════════════════════
|
|
||||||
// RuleEngineService — 待实体字段就绪后启用。
|
|
||||||
// 阻塞原因: warehouse_rule.Enable/LastTriggered/CooldownSec
|
|
||||||
// warehouse_rulecondition.LastTriggered/RecoveryThreshold_Numeric
|
|
||||||
// warehouse_ruleaction.ActionType 等字段在实体类中不存在
|
|
||||||
// 修复顺序: SQL ALTER TABLE → VolPro 代码生成器 → 移除本桩恢复完整实现
|
|
||||||
// 完整实现见 git history: 提交 "RuleEngine-R2-R4: RuleEngineService+RuleEngineJob"
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Warehouse.IRepositories;
|
|
||||||
|
|
||||||
namespace Warehouse.Services;
|
|
||||||
|
|
||||||
public class RuleEngineService
|
|
||||||
{
|
|
||||||
private readonly Iwarehouse_ruleRepository _ruleRepo;
|
|
||||||
|
|
||||||
public RuleEngineService(Iwarehouse_ruleRepository ruleRepo)
|
|
||||||
{
|
|
||||||
_ruleRepo = ruleRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task EvaluateAllAsync()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException(
|
|
||||||
"RuleEngineService 待实体字段就绪。步骤: SQL ALTER TABLE → 代码生成器 → git revert 本桩。");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using Quartz;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using System.Net.Http;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
private readonly IServiceProvider _sp;
|
|
||||||
public SyncDevicesJob(IServiceProvider sp) { _sp = sp; }
|
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext? context)
|
|
||||||
{
|
|
||||||
var sp = _sp;
|
|
||||||
var gwSvc = sp.GetService<Igateway_nodesService>();
|
|
||||||
var httpFactory = sp.GetService<IHttpClientFactory>();
|
|
||||||
var config = sp.GetService<IConfiguration>();
|
|
||||||
var client = httpFactory != null ? new GatewayClient(httpFactory, config!) : null;
|
|
||||||
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,117 +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>
|
|
||||||
[Obsolete("已迁移至 gateway_nodesService.SyncDevicesAsync")]
|
|
||||||
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,239 +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;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Warehouse.Services
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// gateway_nodes 业务逻辑(partial)。注册/心跳/设备同步。
|
|
||||||
/// </summary>
|
|
||||||
public partial class gateway_nodesService
|
|
||||||
{
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
private readonly Igateway_nodesRepository _repository;
|
|
||||||
private readonly ILogger<gateway_nodesService> _logger;
|
|
||||||
|
|
||||||
[ActivatorUtilitiesConstructor]
|
|
||||||
public gateway_nodesService(
|
|
||||||
Igateway_nodesRepository dbRepository,
|
|
||||||
IHttpContextAccessor httpContextAccessor,
|
|
||||||
ILogger<gateway_nodesService> logger
|
|
||||||
)
|
|
||||||
: base(dbRepository)
|
|
||||||
{
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
_repository = dbRepository;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 网关注册(Upsert)。
|
|
||||||
/// NodeCode 匹配则更新适配器类型/地址/在线状态;
|
|
||||||
/// NodeCode 不匹配且 Token 验证通过则插入新记录。
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("由 A1 API Controller 自动调用,不建议手动调用")]
|
|
||||||
public async Task<gateway_nodes> RegisterNodeAsync(string nodeCode, string token, string adapterTypes, string baseUrl)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("[A1] 网关注册: NodeCode={Node}, Adapters={Adapters}", nodeCode, adapterTypes);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var existingList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode).ToListAsync();
|
|
||||||
var existing = existingList.FirstOrDefault();
|
|
||||||
|
|
||||||
gateway_nodes entity;
|
|
||||||
if (existing != null)
|
|
||||||
{
|
|
||||||
if (existing.NodeToken != token)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("[A1] 注册失败: NodeCode={Node} Token不匹配", nodeCode);
|
|
||||||
throw new UnauthorizedAccessException("NodeToken 不匹配");
|
|
||||||
}
|
|
||||||
|
|
||||||
existing.AdapterTypes = adapterTypes;
|
|
||||||
existing.BaseUrl = baseUrl;
|
|
||||||
existing.IsOnline = "在线";
|
|
||||||
existing.LastHeartbeat = DateTime.Now;
|
|
||||||
_repository.DbContext.Updateable(existing).ExecuteCommand();
|
|
||||||
entity = existing;
|
|
||||||
_logger.LogInformation("[A1] 网关注册(更新): NodeId={Id}, NodeCode={Node}", entity.NodeId, nodeCode);
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
_logger.LogInformation("[A1] 网关注册(新增): NodeId={Id}, NodeCode={Node}", entity.NodeId, nodeCode);
|
|
||||||
}
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "[A1] 注册异常: NodeCode={Node}", nodeCode);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 心跳更新。更新 LastHeartbeat 并标记在线。
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("由 A2 API Controller 自动调用,不建议手动调用")]
|
|
||||||
public async Task UpdateHeartbeatAsync(string nodeCode, string token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var entityList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode && x.NodeToken == token).ToListAsync();
|
|
||||||
var entity = entityList.FirstOrDefault();
|
|
||||||
if (entity == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("[A2] 心跳认证失败: NodeCode={Node}", nodeCode);
|
|
||||||
throw new UnauthorizedAccessException("认证失败:NodeCode 或 Token 无效");
|
|
||||||
}
|
|
||||||
|
|
||||||
entity.IsOnline = "在线";
|
|
||||||
entity.LastHeartbeat = DateTime.Now;
|
|
||||||
_repository.DbContext.Updateable(entity).ExecuteCommand();
|
|
||||||
_logger.LogDebug("[A2] 心跳更新: NodeCode={Node}", nodeCode);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException) { throw; }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "[A2] 心跳异常: NodeCode={Node}", nodeCode);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设备数据同步。按字段分治原则写入 base_device:
|
|
||||||
/// 首次入库写全量,后续仅更新网关字段。
|
|
||||||
/// parentSourceId 解析为 ParentDeviceId。
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("由 A3 API Controller 自动调用,不建议手动调用")]
|
|
||||||
public async Task<(int added, int updated)> SyncDevicesAsync(int gatewayNodeId, List<SyncDeviceItem> devices)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("[A3] 设备同步开始: NodeId={Id}, 设备数={Count}", gatewayNodeId, devices.Count);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var db = _repository.DbContext;
|
|
||||||
|
|
||||||
var adapterCodes = devices.Select(d => d.AdapterCode).Distinct().ToList();
|
|
||||||
// 全局去重——不限定 NodeId,防止网关重启后 NodeId 变化导致重复插入
|
|
||||||
var existingIds = db.Queryable<base_device>()
|
|
||||||
.Where(x => 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;
|
|
||||||
|
|
||||||
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 = d.IsParent ? 0 : parentDeviceId,
|
|
||||||
IsOnline = d.IsOnline ? "在线" : "离线",
|
|
||||||
IpAddress = d.IpAddress,
|
|
||||||
Port = d.Port,
|
|
||||||
ExtraData = d.ExtraDataJson,
|
|
||||||
Enable = "启用",
|
|
||||||
LastSyncTime = DateTime.Now,
|
|
||||||
CreateDate = DateTime.Now
|
|
||||||
};
|
|
||||||
var newId = db.Insertable(entity).ExecuteReturnIdentity();
|
|
||||||
// 补入去重字典,同批次子设备可查到父设备
|
|
||||||
existingIds[(d.AdapterCode, d.SourceId)] = Convert.ToInt32(newId);
|
|
||||||
added++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var entity = await db.Queryable<base_device>().FirstAsync(x => x.DeviceId == existingId);
|
|
||||||
if (entity != null)
|
|
||||||
{
|
|
||||||
entity.NodeId = gatewayNodeId; // 重新归属到当前网关
|
|
||||||
entity.IsOnline = d.IsOnline ? "在线" : "离线";
|
|
||||||
entity.IsParent = d.IsParent ? "是" : "否";
|
|
||||||
entity.ParentDeviceId = d.IsParent ? 0 : (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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logger.LogInformation("[A3] 设备同步完成: 新增{Added}台, 更新{Updated}台", added, updated);
|
|
||||||
return (added, updated);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "[A3] 设备同步异常: NodeId={Id}, 设备数={Count}", gatewayNodeId, devices.Count);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>网关同步设备条目</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>(); } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
339
doc/db_init.sql
339
doc/db_init.sql
@@ -1,195 +1,168 @@
|
|||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- SecMPS v3.0 数据库建表脚本(6张表)
|
-- SecMPS v2.0 数据库建表脚本
|
||||||
-- 数据库: gljs_main
|
-- 数据库: gljs_main
|
||||||
-- 扩展表已合并到 Base_Device.ExtraData(JSON)
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
-- ============================================
|
USE gljs_main;
|
||||||
|
|
||||||
-- 1. 统一设备主表
|
-- 1. 统一设备主表
|
||||||
-- ExtraData(JSON) 承载所有适配器特有字段
|
CREATE TABLE IF NOT EXISTS Base_Device (
|
||||||
-- DeviceGroup 路由到正确的网关Adapter和前端按钮组
|
DeviceId CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
-- ============================================
|
DeviceName NVARCHAR(100) NOT NULL,
|
||||||
DROP TABLE IF EXISTS base_device;
|
AdapterCode NVARCHAR(50) NOT NULL,
|
||||||
CREATE TABLE base_device (
|
SourceId NVARCHAR(100) NOT NULL,
|
||||||
DeviceId INT AUTO_INCREMENT COMMENT '设备ID',
|
DeviceCategory INT NOT NULL DEFAULT 1,
|
||||||
DeviceName NVARCHAR(100) NOT NULL COMMENT '设备名称',
|
DeviceType NVARCHAR(50),
|
||||||
AdapterCode NVARCHAR(50) COMMENT '来源适配器(类型:实例)',
|
RegionId INT NULL,
|
||||||
SourceId NVARCHAR(100) COMMENT '源系统设备ID',
|
IsParent TINYINT NOT NULL DEFAULT 0,
|
||||||
DeviceCategory NVARCHAR(50) NOT NULL COMMENT '设备种类(数据字典:门磁/空调/智能断路器/人行道闸/车辆道闸/485钥匙柜/网络钥匙柜/紧急报警按钮/红外报警器/门禁一体机/除湿_恒湿机/空调控制器/烟雾报警器/气体报警器/温湿度变送器/摄像机/硬盘录像机/动环采集器)',
|
ParentDeviceId CHAR(36) NULL,
|
||||||
DeviceGroup NVARCHAR(20) NOT NULL COMMENT '设备分组(数据字典:视频设备/IoT设备/门禁设备/道闸设备/报警设备)',
|
IsOnline TINYINT NOT NULL DEFAULT 0,
|
||||||
PointId INT NULL COMMENT '所属点位ID',
|
IpAddress NVARCHAR(50),
|
||||||
NodeId INT NULL COMMENT '所属网关节点ID',
|
Port INT,
|
||||||
IsParent NVARCHAR(20) NOT NULL DEFAULT '否' COMMENT '是否父设备(数据字典:是/否)',
|
Location NVARCHAR(200),
|
||||||
ParentDeviceId INT NULL COMMENT '父设备ID(自引用,子设备挂父设备下)',
|
Lat DOUBLE,
|
||||||
IsOnline NVARCHAR(20) DEFAULT '离线' COMMENT '在线状态(数据字典:在线/离线)',
|
Lng DOUBLE,
|
||||||
IpAddress NVARCHAR(50) COMMENT 'IP地址',
|
MapModelId NVARCHAR(100),
|
||||||
Port INT COMMENT '端口',
|
MapModelScale FLOAT DEFAULT 1.0,
|
||||||
Location NVARCHAR(200) COMMENT '安装位置',
|
MapModelRotation NVARCHAR(100),
|
||||||
Lat DOUBLE COMMENT '纬度',
|
ExtraData TEXT,
|
||||||
Lng DOUBLE COMMENT '经度',
|
LocalOverrides TEXT,
|
||||||
MapModelId NVARCHAR(100) COMMENT '三维地图模型ID',
|
SyncVersion BIGINT DEFAULT 0,
|
||||||
MapModelScale FLOAT DEFAULT 1.0 COMMENT '模型缩放比例',
|
LastSyncTime DATETIME,
|
||||||
MapModelRotation NVARCHAR(100) COMMENT '模型旋转角度(JSON)',
|
Enable TINYINT DEFAULT 1,
|
||||||
ExtraData TEXT COMMENT '适配器扩展数据JSON(Owl/MC4/门禁字段均存于此)',
|
Remark NVARCHAR(500),
|
||||||
LastSyncTime DATETIME COMMENT '上次同步时间',
|
CreateID INT,
|
||||||
Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)',
|
Creator NVARCHAR(50),
|
||||||
Remark NVARCHAR(500) COMMENT '备注',
|
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
CreateID INT COMMENT '创建人ID',
|
ModifyID INT,
|
||||||
Creator NVARCHAR(50) COMMENT '创建人',
|
Modifier NVARCHAR(50),
|
||||||
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
ModifyDate DATETIME,
|
||||||
ModifyID INT COMMENT '修改人ID',
|
UNIQUE INDEX IX_Base_Device_Adapter_Source (AdapterCode, SourceId),
|
||||||
Modifier NVARCHAR(50) COMMENT '修改人',
|
INDEX IX_Base_Device_RegionId (RegionId),
|
||||||
ModifyDate DATETIME COMMENT '修改时间',
|
INDEX IX_Base_Device_ParentId (ParentDeviceId)
|
||||||
PRIMARY KEY (DeviceId),
|
);
|
||||||
INDEX IX_Sync (AdapterCode, SourceId),
|
|
||||||
INDEX IX_Point (PointId),
|
|
||||||
INDEX IX_Parent (ParentDeviceId),
|
|
||||||
INDEX IX_Gateway (NodeId),
|
|
||||||
INDEX IX_Group (DeviceGroup)
|
|
||||||
) COMMENT '统一设备主表';
|
|
||||||
|
|
||||||
-- ============================================
|
-- 2. 视频设备扩展表
|
||||||
-- 2. 视频通道表
|
CREATE TABLE IF NOT EXISTS Device_Video_Ext (
|
||||||
-- DeviceId(INT) → base_device.DeviceId
|
ExtId CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
-- ============================================
|
DeviceId CHAR(36) NOT NULL,
|
||||||
DROP TABLE IF EXISTS video_channel;
|
OwlDeviceId NVARCHAR(64) NOT NULL,
|
||||||
CREATE TABLE video_channel (
|
Protocol INT DEFAULT 1,
|
||||||
ChannelId INT AUTO_INCREMENT COMMENT '通道记录ID',
|
Manufacturer NVARCHAR(100),
|
||||||
OwlChannelId NVARCHAR(64) NOT NULL COMMENT 'Owl系统通道ID',
|
Model NVARCHAR(100),
|
||||||
DeviceId INT NOT NULL COMMENT '关联Base_Device设备ID',
|
ChannelCount INT DEFAULT 0,
|
||||||
OwlStreamApp NVARCHAR(50) COMMENT 'Owl流应用名',
|
OwlStatus NVARCHAR(500),
|
||||||
OwlStreamName NVARCHAR(100) COMMENT 'Owl流名称',
|
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
HasPtz TINYINT DEFAULT 0 COMMENT '是否支持云台',
|
UNIQUE INDEX IX_VideoExt_Owl (OwlDeviceId),
|
||||||
HasRecording TINYINT DEFAULT 0 COMMENT '是否支持录像',
|
INDEX IX_VideoExt_Device (DeviceId)
|
||||||
RecordMode INT DEFAULT 0 COMMENT '录像模式',
|
);
|
||||||
SnapshotUrl NVARCHAR(500) COMMENT '快照地址',
|
|
||||||
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
PRIMARY KEY (ChannelId),
|
|
||||||
INDEX IX_Device (DeviceId),
|
|
||||||
INDEX IX_Owl (OwlChannelId)
|
|
||||||
) COMMENT '视频通道表';
|
|
||||||
|
|
||||||
-- ============================================
|
-- 3. 视频通道表
|
||||||
-- 3. 录像记录表
|
CREATE TABLE IF NOT EXISTS Video_Channel (
|
||||||
-- ChannelId(INT) → video_channel.ChannelId
|
ChannelId CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
-- ============================================
|
OwlChannelId NVARCHAR(64) NOT NULL,
|
||||||
DROP TABLE IF EXISTS video_record;
|
DeviceId CHAR(36) NOT NULL,
|
||||||
CREATE TABLE video_record (
|
ChannelName NVARCHAR(100) NOT NULL,
|
||||||
RecordId INT AUTO_INCREMENT COMMENT '录像记录ID',
|
ChannelNo INT DEFAULT 0,
|
||||||
ChannelId INT NOT NULL COMMENT '关联通道ID',
|
OwlStreamApp NVARCHAR(50),
|
||||||
OwlRecordId INT NOT NULL COMMENT 'Owl录像记录ID',
|
OwlStreamName NVARCHAR(100),
|
||||||
App NVARCHAR(50) COMMENT '应用名',
|
HasPtz TINYINT DEFAULT 0,
|
||||||
Stream NVARCHAR(100) COMMENT '流ID',
|
HasRecording TINYINT DEFAULT 0,
|
||||||
StartedAt DATETIME NOT NULL COMMENT '录像开始时间',
|
RecordMode INT DEFAULT 0,
|
||||||
EndedAt DATETIME COMMENT '录像结束时间',
|
IsOnline TINYINT DEFAULT 0,
|
||||||
Duration DOUBLE DEFAULT 0 COMMENT '录像时长(秒)',
|
SnapshotUrl NVARCHAR(500),
|
||||||
FilePath NVARCHAR(500) COMMENT '文件路径',
|
Location NVARCHAR(200),
|
||||||
FileSize BIGINT DEFAULT 0 COMMENT '文件大小(字节)',
|
Lat DOUBLE,
|
||||||
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
Lng DOUBLE,
|
||||||
PRIMARY KEY (RecordId),
|
Enable TINYINT DEFAULT 1,
|
||||||
INDEX IX_Channel (ChannelId),
|
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
INDEX IX_Time (StartedAt)
|
UNIQUE INDEX IX_Channel_Owl (OwlChannelId),
|
||||||
) COMMENT '录像记录表';
|
INDEX IX_Channel_Device (DeviceId)
|
||||||
|
);
|
||||||
|
|
||||||
-- ============================================
|
-- 4. 录像记录表
|
||||||
-- 4. 设备数据归档表
|
CREATE TABLE IF NOT EXISTS Video_Record (
|
||||||
-- DeviceId(INT) → base_device.DeviceId
|
RecordId CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
-- ============================================
|
ChannelId CHAR(36) NOT NULL,
|
||||||
DROP TABLE IF EXISTS iot_devicedata;
|
OwlRecordId INT NOT NULL,
|
||||||
CREATE TABLE iot_devicedata (
|
App NVARCHAR(50),
|
||||||
DataId INT AUTO_INCREMENT COMMENT '数据记录ID',
|
Stream NVARCHAR(100),
|
||||||
DeviceId INT NOT NULL COMMENT '关联设备ID(子设备/点位)',
|
StartedAt DATETIME NOT NULL,
|
||||||
PointValue DOUBLE COMMENT '点位数值',
|
EndedAt DATETIME,
|
||||||
UpdateTime DATETIME NOT NULL COMMENT '数据更新时间',
|
Duration DOUBLE DEFAULT 0,
|
||||||
`Interval` INT DEFAULT 0 COMMENT '采集间隔(毫秒)',
|
FilePath NVARCHAR(500),
|
||||||
ArchiveType INT DEFAULT 1 COMMENT '归档类型(1小时/2日)',
|
FileSize BIGINT DEFAULT 0,
|
||||||
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (DataId),
|
INDEX IX_Record_Channel (ChannelId),
|
||||||
INDEX IX_Device (DeviceId),
|
INDEX IX_Record_Time (StartedAt)
|
||||||
INDEX IX_Time (CreateDate)
|
);
|
||||||
) COMMENT '设备数据归档表';
|
|
||||||
|
|
||||||
-- ============================================
|
-- 5. IoT设备扩展表
|
||||||
-- 5. 告警记录表(通用)
|
CREATE TABLE IF NOT EXISTS Device_IoT_Ext (
|
||||||
-- DeviceId(INT) → base_device.DeviceId
|
ExtId CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
-- ============================================
|
DeviceId CHAR(36) NOT NULL,
|
||||||
DROP TABLE IF EXISTS iot_alarm;
|
Mc4DeviceId INT NOT NULL,
|
||||||
CREATE TABLE iot_alarm (
|
ObjectType INT,
|
||||||
AlarmId INT AUTO_INCREMENT COMMENT '告警ID',
|
Tag NVARCHAR(100),
|
||||||
SourceAlarmId NVARCHAR(100) NOT NULL COMMENT '源系统告警ID',
|
ParentId INT,
|
||||||
DeviceId INT NOT NULL COMMENT '关联设备ID',
|
Mc4Option NVARCHAR(500),
|
||||||
AlarmType INT DEFAULT 0 COMMENT '告警类型',
|
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
AlarmLevel NVARCHAR(20) DEFAULT '提示' COMMENT '告警等级(数据字典:提示/普通/重要/紧急)',
|
UNIQUE INDEX IX_IoTExt_Mc4 (Mc4DeviceId),
|
||||||
AlarmDesc NVARCHAR(500) COMMENT '告警描述',
|
INDEX IX_IoTExt_Device (DeviceId)
|
||||||
AlarmValue DOUBLE COMMENT '触发值',
|
);
|
||||||
StartTime DATETIME NOT NULL COMMENT '告警开始时间',
|
|
||||||
EndTime DATETIME COMMENT '告警结束时间',
|
|
||||||
ConfirmTime DATETIME COMMENT '确认时间',
|
|
||||||
ConfirmUser NVARCHAR(50) COMMENT '确认人',
|
|
||||||
State NVARCHAR(20) DEFAULT '未确认' COMMENT '状态(数据字典:未确认/已确认/已结束)',
|
|
||||||
AdapterCode NVARCHAR(50) COMMENT '来源适配器',
|
|
||||||
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
PRIMARY KEY (AlarmId),
|
|
||||||
INDEX IX_Device (DeviceId),
|
|
||||||
INDEX IX_Source (SourceAlarmId),
|
|
||||||
INDEX IX_Time (StartTime),
|
|
||||||
INDEX IX_Level (AlarmLevel)
|
|
||||||
) COMMENT '告警记录表';
|
|
||||||
|
|
||||||
-- ============================================
|
-- 6. 设备点位表
|
||||||
-- 6. 网关节点注册表
|
CREATE TABLE IF NOT EXISTS IoT_DevicePoint (
|
||||||
-- ============================================
|
PointId CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
DROP TABLE IF EXISTS gateway_nodes;
|
DeviceId CHAR(36) NOT NULL,
|
||||||
CREATE TABLE gateway_nodes (
|
Mc4DeviceId INT NOT NULL,
|
||||||
NodeId INT AUTO_INCREMENT COMMENT '网关节点ID',
|
PointIndex INT NOT NULL,
|
||||||
NodeCode NVARCHAR(50) NOT NULL COMMENT '网关唯一编码',
|
PointType INT,
|
||||||
NodeName NVARCHAR(100) NOT NULL COMMENT '网关名称',
|
PointTag NVARCHAR(100),
|
||||||
NodeToken NVARCHAR(100) NOT NULL COMMENT '认证令牌',
|
PointName NVARCHAR(100) NOT NULL,
|
||||||
AdapterTypes NVARCHAR(200) COMMENT '支持的适配器类型(网关上报)',
|
PointDesc NVARCHAR(200),
|
||||||
BaseUrl NVARCHAR(200) COMMENT '网关自身地址(网关上报)',
|
Unit NVARCHAR(50),
|
||||||
LastHeartbeat DATETIME COMMENT '上次心跳时间',
|
IsControlPoint TINYINT DEFAULT 0,
|
||||||
IsOnline NVARCHAR(20) DEFAULT '离线' COMMENT '在线状态(数据字典:在线/离线)',
|
Mc4Option NVARCHAR(500),
|
||||||
Enable NVARCHAR(20) DEFAULT '启用' COMMENT '启用状态(数据字典:启用/禁用)',
|
Enable TINYINT DEFAULT 1,
|
||||||
Remark NVARCHAR(500) COMMENT '备注',
|
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
CreateID INT COMMENT '创建人ID',
|
UNIQUE INDEX IX_Point_Mc4 (Mc4DeviceId, PointIndex),
|
||||||
Creator NVARCHAR(50) COMMENT '创建人',
|
INDEX IX_Point_Device (DeviceId)
|
||||||
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 '网关节点注册表';
|
|
||||||
|
|
||||||
|
-- 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. 告警记录表
|
||||||
-- SecMPS 规则引擎: warehouse_variable 变量定义表 (P1-6)
|
CREATE TABLE IF NOT EXISTS IoT_Alarm (
|
||||||
-- =================================================
|
AlarmId CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
-- 规则条件/动作的 ValueId 绑定到此表的 VariableId
|
Mc4AlarmId NVARCHAR(64) NOT NULL,
|
||||||
-- DeviceId 关联 base_device.DeviceId
|
DeviceId CHAR(36),
|
||||||
-- =================================================
|
PointId CHAR(36),
|
||||||
DROP TABLE IF EXISTS warehouse_variable;
|
AlarmType INT DEFAULT 0,
|
||||||
CREATE TABLE warehouse_variable (
|
AlarmLevel INT DEFAULT 1,
|
||||||
VariableId INT AUTO_INCREMENT COMMENT '变量ID(自增主键)',
|
AlarmDesc NVARCHAR(500),
|
||||||
DeviceId INT NOT NULL COMMENT '关联设备ID(base_device.DeviceId)',
|
AlarmValue DOUBLE,
|
||||||
VariableName VARCHAR(255) NOT NULL COMMENT '变量名称(温度/湿度/人数等)',
|
StartTime DATETIME NOT NULL,
|
||||||
PointIndex INT DEFAULT 0 COMMENT '点位索引(MC4 pointIndex / Owl统计量编码)',
|
EndTime DATETIME,
|
||||||
Unit VARCHAR(50) NULL COMMENT '单位(℃/%/人)',
|
ConfirmTime DATETIME,
|
||||||
SortOrder INT DEFAULT 0 COMMENT '排序顺序',
|
ConfirmUser NVARCHAR(50),
|
||||||
PRIMARY KEY (VariableId),
|
State INT DEFAULT 1,
|
||||||
INDEX IX_warehouse_variable_DeviceId (DeviceId)
|
AdapterCode NVARCHAR(50),
|
||||||
) COMMENT '规则引擎变量定义表';
|
CreateDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE INDEX IX_Alarm_Mc4 (Mc4AlarmId),
|
||||||
|
INDEX IX_Alarm_Device (DeviceId),
|
||||||
-- F3.2 规则引擎滞后窗 (hysteresis)
|
INDEX IX_Alarm_Time (StartTime)
|
||||||
-- 触发阈值与恢复阈值之间留缓冲区间,防止阈值附近反复抖动
|
);
|
||||||
ALTER TABLE warehouse_rulecondition
|
|
||||||
ADD COLUMN RecoveryThreshold_Numeric DECIMAL(18,2) NULL COMMENT '数值型恢复阈值(如>28℃触发,≤26℃恢复)',
|
|
||||||
ADD COLUMN RecoveryThreshold_Switch VARCHAR(50) NULL COMMENT '开关型恢复阈值(如开触发,关恢复)';
|
|
||||||
|
|
||||||
-- F3.3 条件级冷却 (cooldown)
|
|
||||||
-- 冷却期内条件再次命中不重复执行动作,防止告警轰炸
|
|
||||||
ALTER TABLE warehouse_rulecondition
|
|
||||||
ADD COLUMN LastTriggered DATETIME NULL COMMENT '该条件上次触发的时间',
|
|
||||||
ADD COLUMN LastTriggerValue DECIMAL(18,2) NULL COMMENT '该条件上次触发时的实际值';
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
严重性 代码 说明 项目 文件 行 抑制状态 详细信息
|
|
||||||
错误(活动) CS1061 “ControlRequest”未包含“Command”的定义,并且找不到可接受第一个“ControlRequest”类型参数的可访问扩展方法“Command”(是否缺少 using 指令或程序集引用?) IntegrationGateway.Host D:\Code\SecMPS\gateway\src\IntegrationGateway.Host\Program.cs 213
|
|
||||||
错误(活动) CS1061 “ControlRequest”未包含“Parameters”的定义,并且找不到可接受第一个“ControlRequest”类型参数的可访问扩展方法“Parameters”(是否缺少 using 指令或程序集引用?) IntegrationGateway.Host D:\Code\SecMPS\gateway\src\IntegrationGateway.Host\Program.cs 213
|
|
||||||
@@ -1,608 +0,0 @@
|
|||||||
# warehouse 客户端代码深度审核报告(含完整修复方案)
|
|
||||||
|
|
||||||
> 日期: 2026-06-04 | 项目: warehouse/ | 扫描: 38 源文件, ~12,000 行 | 问题: 70 项
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. `api/http.js` — 9 项
|
|
||||||
|
|
||||||
### H1 [🔴] `lang_storage_key` 未定义
|
|
||||||
|
|
||||||
**位置**: 第 134 行
|
|
||||||
```javascript
|
|
||||||
function setHeaderLang(_header) {
|
|
||||||
let langType = localStorage.getItem(lang_storage_key) // ← 未定义!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**后果**: 运行时报错 `lang_storage_key is not defined`,语言功能完全失效。
|
|
||||||
**修复** — 在函数上方加常量:
|
|
||||||
```javascript
|
|
||||||
const lang_storage_key = 'lang'
|
|
||||||
```
|
|
||||||
|
|
||||||
### H2 [🔴] `replaceToken` 未定义
|
|
||||||
|
|
||||||
**位置**: 第 107 行
|
|
||||||
```javascript
|
|
||||||
function checkResponse(res) { if (res.headers.vol_exp == '1') { replaceToken() } }
|
|
||||||
```
|
|
||||||
**后果**: Token 过期后调用未定义函数,静默失败。
|
|
||||||
**修复** — 在 `checkResponse` 前追加:
|
|
||||||
```javascript
|
|
||||||
function replaceToken() {
|
|
||||||
store.dispatch('clearUserInfo')
|
|
||||||
window.location.href = '/login'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**跨文件影响**: 需确认 `store/index.js` 有 `clearUserInfo` mutation(已存在)。
|
|
||||||
|
|
||||||
### H3 [🔴] `toLogin` 未定义
|
|
||||||
|
|
||||||
**位置**: 第 77 行
|
|
||||||
```javascript
|
|
||||||
if (error.response.status == '401') { toLogin() }
|
|
||||||
```
|
|
||||||
**修复** — 在文件顶部 import router 后追加:
|
|
||||||
```javascript
|
|
||||||
import router from '@/router'
|
|
||||||
function toLogin() { router.push('/login') }
|
|
||||||
```
|
|
||||||
**跨文件影响**: 需确认 `router/index.ts` 导出 router 实例。
|
|
||||||
|
|
||||||
### H4 [🟠] 降级地址硬编码
|
|
||||||
|
|
||||||
**位置**: 第 25-33 行
|
|
||||||
```javascript
|
|
||||||
axios.defaults.baseURL = 'http://192.168.3.108:9100/'
|
|
||||||
dataViewUrl = 'http://192.168.3.108:9200/'
|
|
||||||
```
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
axios.defaults.baseURL = window.location.origin
|
|
||||||
dataViewUrl = (window as any).apiConfig?.dataViewUrl || window.location.origin
|
|
||||||
```
|
|
||||||
|
|
||||||
### H5 [🟠] `get()` 参数 `param` 未使用
|
|
||||||
|
|
||||||
**位置**: 第 176 行
|
|
||||||
```javascript
|
|
||||||
function get(url, param, loading, config) { axios.get(url, config) }
|
|
||||||
```
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
function get(url, param, loading, config) {
|
|
||||||
const cfg = { ...config }
|
|
||||||
if (param) cfg.params = param
|
|
||||||
// ... 其余不变
|
|
||||||
axios.get(url, cfg).then(...)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### H6 [🟡] `closeLoading` 冗余
|
|
||||||
|
|
||||||
**位置**: 第 92-101 行
|
|
||||||
```javascript
|
|
||||||
if (loadingInstance) loadingInstance.close()
|
|
||||||
if (loadingStatus) { loadingStatus = false; if (loadingInstance) loadingInstance.close() }
|
|
||||||
```
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
loadingStatus = false
|
|
||||||
loadingInstance?.close()
|
|
||||||
```
|
|
||||||
|
|
||||||
### H7 [🟡] `alert()` 弹窗
|
|
||||||
|
|
||||||
**位置**: 第 199 行
|
|
||||||
```javascript
|
|
||||||
alert('http.js未配置大屏url地址')
|
|
||||||
```
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
ElMessage.error('未配置大屏URL地址')
|
|
||||||
```
|
|
||||||
|
|
||||||
### H8 [🟡] 无类型安全
|
|
||||||
|
|
||||||
**修复** — 在文件头部加 JSDoc(不改变运行时):
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* @template T
|
|
||||||
* @param {string} url
|
|
||||||
* @param {object} [params]
|
|
||||||
* @param {boolean|string} [loading]
|
|
||||||
* @param {object} [config]
|
|
||||||
* @returns {Promise<T>}
|
|
||||||
*/
|
|
||||||
function post(url, params, loading, config) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
### H9 [⚪] 文件臃肿 (409行)
|
|
||||||
|
|
||||||
**修复** — 拆为 3 文件:
|
|
||||||
- `api/http-client.ts` — Axios 实例 + baseURL + 拦截器 (80行)
|
|
||||||
- `api/http-auth.ts` — getToken/replaceToken/toLogin (40行)
|
|
||||||
- `api/http-loading.ts` — showLoading/closeLoading (20行)
|
|
||||||
|
|
||||||
`api/http.js` 改为:
|
|
||||||
```javascript
|
|
||||||
import { createHttpClient } from './http-client'
|
|
||||||
export default createHttpClient()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. `api/gateway.ts` — 3 项
|
|
||||||
|
|
||||||
### GW1 [🟠] 网关地址硬编码
|
|
||||||
|
|
||||||
**位置**: 第 5 行 `const GW_BASE = 'http://192.168.3.108:5100'`
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```typescript
|
|
||||||
const GW_BASE = (window as any).apiConfig?.gatewayUrl || 'http://localhost:5100'
|
|
||||||
```
|
|
||||||
|
|
||||||
### GW2 [🟡] `fetch()` 无超时
|
|
||||||
|
|
||||||
**修复** — 完整重写 `gwGet`(`gwPost` 同理):
|
|
||||||
```typescript
|
|
||||||
export async function gwGet(url: string, timeoutMs = 10000): Promise<any> {
|
|
||||||
const ctrl = new AbortController()
|
|
||||||
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`${GW_BASE}${url}`, { signal: ctrl.signal })
|
|
||||||
if (!resp.ok) throw new Error(`网关请求失败: ${resp.status}`)
|
|
||||||
return resp.json()
|
|
||||||
} finally { clearTimeout(timer) }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GW3 [🟡] 模型映射混入API层
|
|
||||||
|
|
||||||
**修复** — 新建 `warehouse/src/services/cameraService.ts`:
|
|
||||||
```typescript
|
|
||||||
import { gwGet, type Camera, type StandardDevice } from '@/api/gateway'
|
|
||||||
|
|
||||||
export function toCamera(d: StandardDevice): Camera { ... }
|
|
||||||
export async function fetchCameras(adapter: string): Promise<Camera[]> { ... }
|
|
||||||
```
|
|
||||||
**跨文件影响**: `Live.vue`, `VideoWall.vue`, `History.vue` 的 import 改为 `from '@/services/cameraService'`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. `api/buttons.js` — 1 项
|
|
||||||
|
|
||||||
### B1 [🟡] Element UI 旧版图标语法
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
icon: 'el-icon-search'
|
|
||||||
```
|
|
||||||
Element Plus ≥2.0 已废弃字符串图标。
|
|
||||||
|
|
||||||
**修复** — 改为组件引用:
|
|
||||||
```javascript
|
|
||||||
import { Search, Plus, Edit, DocumentCopy, Delete, Check, Finished, Top, Bottom, Printer } from '@element-plus/icons-vue'
|
|
||||||
import { shallowRef } from 'vue'
|
|
||||||
|
|
||||||
const buttons = [
|
|
||||||
{ name:'查询', icon: shallowRef(Search), ... },
|
|
||||||
{ name:'新建', icon: shallowRef(Plus), ... },
|
|
||||||
// ... 其余同理
|
|
||||||
]
|
|
||||||
```
|
|
||||||
**跨文件影响**: 使用 buttons 的组件需确认其渲染逻辑支持组件引用(通常通过 `<component :is="btn.icon" />`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. `api/permission.js` — 2 项
|
|
||||||
|
|
||||||
### PE1 [🟠] 权限缺失时静默放行
|
|
||||||
|
|
||||||
**位置**: 第 24 行
|
|
||||||
```javascript
|
|
||||||
if (!permission) { permission = { permission: ['Search'] } }
|
|
||||||
```
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
if (!permission) { return [] }
|
|
||||||
```
|
|
||||||
空数组使所有按钮不可见(安全默认)。
|
|
||||||
|
|
||||||
### PE2 [🟡] `to401` 空实现
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
import router from '@/router'
|
|
||||||
function to401() { router.push('/401') }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. `router/index.ts` — 5 项
|
|
||||||
|
|
||||||
### R1 [🟠] 40+ 条路由指向同一组件
|
|
||||||
|
|
||||||
所有菜单子项渲染 `Index.vue`,用户看到重复空白页。
|
|
||||||
|
|
||||||
**修复** — 3 步方案:
|
|
||||||
1. 为已实现的页面保留独立路由(VideoWall/AlarmRecord/AccessRecord 等已存在)
|
|
||||||
2. 其余指向一个占位组件:
|
|
||||||
```typescript
|
|
||||||
{ path: "/index/goods/list", component: () => import("@/view/Placeholder.vue") }
|
|
||||||
```
|
|
||||||
3. `Placeholder.vue`:
|
|
||||||
```vue
|
|
||||||
<template><el-empty description="功能开发中" /></template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### R2 [🟡] `/new-dv` 不要求认证
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{ path:"/new-dv", meta:{ requiresAuth: false } }
|
|
||||||
```
|
|
||||||
**修复**: 改为 `requiresAuth: true`。
|
|
||||||
|
|
||||||
### R3 [🟡] 缺少 beforeEach 守卫
|
|
||||||
|
|
||||||
**修复** — 在 `router/index.ts` 末尾追加:
|
|
||||||
```typescript
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
if (to.meta.requiresAuth !== false) {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
if (token) next()
|
|
||||||
else next('/login')
|
|
||||||
} else { next() }
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### R4 [⚪] `@ts-ignore` 绕过 store 类型
|
|
||||||
|
|
||||||
**修复**: 创建 `warehouse/src/types/store.d.ts`:
|
|
||||||
```typescript
|
|
||||||
declare module '@/store' {
|
|
||||||
const store: import('vuex').Store<any>
|
|
||||||
export default store
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### R5 [⚪] 仓库/货物/出入库路由 20+ 条未使用
|
|
||||||
|
|
||||||
**修复**: 删除。若后续需要可从 git 恢复。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. `main.ts` — 1 项
|
|
||||||
|
|
||||||
### M1 [🟡] 暗色模式写死
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
app.use(ElementPlus, { dark: true })
|
|
||||||
```
|
|
||||||
**修复**:
|
|
||||||
```typescript
|
|
||||||
const dark = localStorage.getItem('dark-mode') !== 'false'
|
|
||||||
app.use(ElementPlus, { dark })
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. `view/index.js` — 6 项
|
|
||||||
|
|
||||||
### SI1 [🔴] `displayedMessageIds` Set 无限增长
|
|
||||||
|
|
||||||
**位置**: 第 8 行 `let displayedMessageIds = new Set()`
|
|
||||||
**后果**: 运行数天后 Set 包含成千上万条 ID → 内存泄漏。
|
|
||||||
|
|
||||||
**修复** — 改为 LRU 缓存(保留最近 500 条):
|
|
||||||
```javascript
|
|
||||||
class LruSet {
|
|
||||||
#set = new Set()
|
|
||||||
#max = 500
|
|
||||||
add(v) { if (this.#set.has(v)) return; this.#set.add(v); if (this.#set.size > this.#max) { this.#set.delete(this.#set.values().next().value) } }
|
|
||||||
has(v) { return this.#set.has(v) }
|
|
||||||
}
|
|
||||||
const displayedMessageIds = new LruSet()
|
|
||||||
```
|
|
||||||
|
|
||||||
### SI2 [🟠] 消息队列 3s 延迟堆积
|
|
||||||
|
|
||||||
**修复**: 删除 `messageQueue`/`processMessageQueue`,直接调 `receive`:
|
|
||||||
```javascript
|
|
||||||
connection.on("ReceiveHomePageMessage", function (data) {
|
|
||||||
if (displayedMessageIds.has(data.id)) return
|
|
||||||
displayedMessageIds.add(data.id)
|
|
||||||
receive(data)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### SI3 [🟠] connection 启动无重试
|
|
||||||
|
|
||||||
`connection.start().catch(...)` 失败后永远不重试。
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
async function startWithRetry(retries = 5) {
|
|
||||||
for (let i = 0; i < retries; i++) {
|
|
||||||
try { await connection.start(); return }
|
|
||||||
catch (e) { console.warn(`SignalR retry ${i+1}/${retries}: ${e.message}`); await new Promise(r => setTimeout(r, 2000)) }
|
|
||||||
}
|
|
||||||
console.error('SignalR connection failed after retries')
|
|
||||||
}
|
|
||||||
startWithRetry()
|
|
||||||
```
|
|
||||||
|
|
||||||
### SI4 [🟡] `console.log` 残留 6 处
|
|
||||||
|
|
||||||
**修复**: 全部替换为 `if (import.meta.env.DEV) console.log(...)`。
|
|
||||||
|
|
||||||
### SI5 [🟡] `receive` 被双重调用
|
|
||||||
|
|
||||||
`processMessageQueue` 和 `ReceiveHomePageMessage` 都调了 `receive`。
|
|
||||||
|
|
||||||
**修复**: SI2 删除了 `processMessageQueue` 后此问题自动消除。
|
|
||||||
|
|
||||||
### SI6 [🟡] 用户信息获取失败静默
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```javascript
|
|
||||||
http.post("api/user/GetCurrentUserInfo").then(...).catch(error => {
|
|
||||||
console.error('获取用户信息失败,SignalR未启动:', error)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8-28: 视图层文件(21 个 Vue 文件)
|
|
||||||
|
|
||||||
### 通用修复模式(适用于所有 Mock 页面)
|
|
||||||
|
|
||||||
以下 17 个页面全 Mock —— 统一修复方案:
|
|
||||||
|
|
||||||
```
|
|
||||||
AccessRecord.vue, AlarmRecord.vue, EmergencyAlarmRecord.vue,
|
|
||||||
KeyInfo.vue, KeyApply.vue, EnvVarManagement.vue,
|
|
||||||
PatrolLog.vue, ScheduleManagement.vue, PathManagement.vue,
|
|
||||||
DroneManagement.vue, dataview.vue, CarApply.vue, CarManager.vue,
|
|
||||||
DeviceStatus.vue(V), VisitorsManagement.vue, VisitCarManagement.vue
|
|
||||||
```
|
|
||||||
|
|
||||||
**统一修复**: 每个页面增加网关 API 调用骨架,Mock 数据降级为 fallback:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 以 AlarmRecord.vue 为例
|
|
||||||
import { gwGet } from '@/api/gateway'
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const data = await gwGet('/api/gateway/alarms/Owl:main?page=1&size=100')
|
|
||||||
alarmData.value = data.items.map((a: StandardAlarm) => ({
|
|
||||||
id: a.alarmId, alarmTime: a.occurTime, deviceName: a.title,
|
|
||||||
location: a.deviceId, status: a.status, imageUrl: '/images/placeholder.png'
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
alarmData.value = getMockAlarmData() // fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 单个页面专项修复
|
|
||||||
|
|
||||||
**DataView.vue** (1840行):
|
|
||||||
- DV1: 告警等级 → 改用 `level` 字段或网关 `StandardAlarm.level` 值
|
|
||||||
- DV2: `setTimeout` 加 `clearTimeout`:
|
|
||||||
```typescript
|
|
||||||
const timers = new Set<number>()
|
|
||||||
onBeforeUnmount(() => timers.forEach(t => clearTimeout(t)))
|
|
||||||
// 使用时: timers.add(setTimeout(...))
|
|
||||||
```
|
|
||||||
- DV6: `originalData: JSON.parse(JSON.stringify(data))` 避免循环引用
|
|
||||||
|
|
||||||
**DeviceInfo.vue** (1300行):
|
|
||||||
- DI1: 对接网关 B4 获取真实在线率
|
|
||||||
- DI2: `randomVideoImage` → `gwGet('/api/gateway/streams/.../live')`
|
|
||||||
- DI3: `handleTurnOn` → `gwPost('/api/gateway/realtime/.../control', { deviceId, pointIndex, value })`
|
|
||||||
|
|
||||||
**Live.vue** / **VideoWall.vue** / **History.vue**:
|
|
||||||
- LV2/VH2: `setInterval` 加清理:
|
|
||||||
```typescript
|
|
||||||
const timer = setInterval(updateTime, 1000)
|
|
||||||
onBeforeUnmount(() => clearInterval(timer))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Main.vue**:
|
|
||||||
- MA1: icon 改为 `@element-plus/icons-vue` 组件引用
|
|
||||||
- MA2: Menu 配置提取:
|
|
||||||
```typescript
|
|
||||||
const menuItems = [
|
|
||||||
{ index:'1', icon: VideoCamera, label:'视频监控', children:[
|
|
||||||
{ index:'/index/video/videowall', label:'视频墙' }
|
|
||||||
]}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
- MA5: 统一 `import { useMapStore } from '@/stores/mapStore'`
|
|
||||||
|
|
||||||
**Map.vue**:
|
|
||||||
- MP1: `const m = /#\/(\d+)/.exec(location.hash); const mapId = m?.[1] || 'default'`
|
|
||||||
|
|
||||||
**Index.vue**:
|
|
||||||
- IN1: `const mapId = import.meta.env.VITE_MAP_ID || 'default'`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 29-31: 组件层(3 文件)
|
|
||||||
|
|
||||||
### Filter.vue [🟡]
|
|
||||||
|
|
||||||
**console.log 8 处**: 同 SI4,改为条件输出。
|
|
||||||
|
|
||||||
**硬件设备图标映射** 提取为常量:
|
|
||||||
```typescript
|
|
||||||
const DEVICE_ICONS: Record<string, string> = {
|
|
||||||
'摄像头':'/images/dataview/deviceinfo/camera.png',
|
|
||||||
'门禁':'/images/dataview/deviceinfo/access.png',
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fence.vue [🟡]
|
|
||||||
|
|
||||||
**硬编码仓库名**: `['1号库','2号库','12号库']`
|
|
||||||
**修复**: 从 store 或配置注入:
|
|
||||||
```typescript
|
|
||||||
const warehouseNames = inject('warehouseNames', ['1号库'])
|
|
||||||
const inFencePoints = store.polygonDataAll.filter(p => warehouseNames.includes(p.name))
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 32-34: 状态管理层(3 文件)
|
|
||||||
|
|
||||||
### SM1 [🔴] 两份 `useMapStore` 同名
|
|
||||||
|
|
||||||
**文件**: `stores/mapStore.js` 和 `store/useMapStore.js` 都 `defineStore('map', ...)`
|
|
||||||
|
|
||||||
**修复** — 选择一份保留(推荐 `stores/mapStore.js` 功能更全),另一份删除:
|
|
||||||
```bash
|
|
||||||
git rm warehouse/src/store/useMapStore.js
|
|
||||||
```
|
|
||||||
**跨文件影响** — 修改以下文件的 import:
|
|
||||||
- `Map.vue` line 9: `'../store/useMapStore'` → `'../stores/mapStore'`
|
|
||||||
- `Index.vue` line 10: `'../stores/mapStore'` (已对)
|
|
||||||
- `Fence.vue` line 4: `'../store/useMapStore'` → `'../stores/mapStore'`
|
|
||||||
- `Filter.vue` line 3: `'../store/useMapStore'` → `'../stores/mapStore'`
|
|
||||||
- `DataView.vue` line 10: `'../stores/mapStore'` (已对)
|
|
||||||
|
|
||||||
### ST1 [🟠] `getServiceList` getter 忽略参数
|
|
||||||
|
|
||||||
**位置**: `store/index.js`
|
|
||||||
```javascript
|
|
||||||
getServiceList: (state) => (path) => { return state.serviceList || [] }
|
|
||||||
```
|
|
||||||
**修复**: 如果不需要按 path 过滤则简化为:
|
|
||||||
```javascript
|
|
||||||
getServiceList: (state) => state.serviceList || []
|
|
||||||
```
|
|
||||||
|
|
||||||
### ST3 [🟡] `test` mutation 返回 `113344`
|
|
||||||
|
|
||||||
**位置**: `store/index.js` line 49
|
|
||||||
```javascript
|
|
||||||
test(state) { return 113344 } // 调试代码
|
|
||||||
```
|
|
||||||
**修复**: 删除此 mutation。
|
|
||||||
|
|
||||||
### ST4 [⚪] `setPermission` 数组 push 会叠加
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
if (data instanceof Array) { state.permission.push(...data) }
|
|
||||||
```
|
|
||||||
每次调用追加而非替换。**修复**: 始终替换 `state.permission = data`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 35: 项目结构 — 4 项
|
|
||||||
|
|
||||||
### PS1/PS2 [🟡] 过期副本文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git rm warehouse/src/view/DataView\ copy.vue
|
|
||||||
git rm warehouse/src/view/Map.vue.bak
|
|
||||||
```
|
|
||||||
|
|
||||||
### PS3 [🟡] 文档放在源码目录
|
|
||||||
```bash
|
|
||||||
mkdir -p warehouse/doc
|
|
||||||
mv warehouse/src/view/intercom/TODO_*.md warehouse/doc/
|
|
||||||
```
|
|
||||||
|
|
||||||
### PS4 [🟠] Vuex + Pinia 共存
|
|
||||||
|
|
||||||
**修复** — 迁移 Vuex → Pinia:
|
|
||||||
1. 新建 `stores/authStore.js`(替代 Vuex 的 userInfo/token/permission)
|
|
||||||
2. 迁移 `store/index.js` 中的 `setUserInfo`/`getToken`/`getPermission` 到 Pinia
|
|
||||||
3. `npm uninstall vuex`
|
|
||||||
4. 修改 `http.js`/`Login.vue`/`permission.js` 从 `@/stores/authStore` 导入
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 36: 全局问题 — 7 项
|
|
||||||
|
|
||||||
### G1 [🟠] Mock 覆盖率 86%
|
|
||||||
|
|
||||||
22 页面中仅 3 个对接网关。**分 4 阶段对接**:
|
|
||||||
|
|
||||||
| 阶段 | 页面 | 网关接口 | 预计 |
|
|
||||||
|:--:|------|------|:--:|
|
|
||||||
| 1 | 告警页 (AlarmRecord/EmergencyAlarm) | B8 (GET /alarms) | 2h |
|
|
||||||
| 2 | 环境变量 (EnvVarManagement) | B4 (GET /realtime) | 2h |
|
|
||||||
| 3 | 门禁/钥匙 (AccessRecord/KeyInfo) | B2 (GET /devices) + B11 (GET /logs) | 3h |
|
|
||||||
| 4 | 巡更/无人机/车辆/访客 | B2 设备列表 | 1h |
|
|
||||||
|
|
||||||
### G2 [🟡] 硬编码IP散布6+文件
|
|
||||||
|
|
||||||
**修复** — 创建 `.env.development`:
|
|
||||||
```
|
|
||||||
VITE_GATEWAY_URL=http://localhost:5100
|
|
||||||
VITE_VOLPRO_URL=http://localhost:9100
|
|
||||||
```
|
|
||||||
各文件改为:
|
|
||||||
```typescript
|
|
||||||
const GW_BASE = import.meta.env.VITE_GATEWAY_URL || 'http://localhost:5100'
|
|
||||||
```
|
|
||||||
|
|
||||||
### G3 [🟡] JS/TS 混用
|
|
||||||
|
|
||||||
**修复**: 7 个 `.js` → `.ts`(逐文件迁移,不改运行时逻辑):
|
|
||||||
```
|
|
||||||
api/http.js → api/http.ts
|
|
||||||
api/buttons.js → api/buttons.ts
|
|
||||||
api/permission.js → api/permission.ts
|
|
||||||
view/index.js → view/index.ts
|
|
||||||
stores/mapStore.js → stores/mapStore.ts
|
|
||||||
router/viewGird.js → router/viewGird.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### G4 [🟡] `window.*` 全局变量
|
|
||||||
|
|
||||||
**修复**: `window.$map` → Store 管理(已存在 `mapStore.setMap`)
|
|
||||||
```typescript
|
|
||||||
// Map.vue: 删除 window.$map = map,保留 store.setMap(map)
|
|
||||||
// 其他文件: 改 window.$map → const { map } = useMapStore()
|
|
||||||
```
|
|
||||||
|
|
||||||
### G5 [🟡] `console.log` 残留 30+ 处
|
|
||||||
|
|
||||||
**修复** — 一行全局替换:
|
|
||||||
```bash
|
|
||||||
# 将所有 console.log 改为条件输出
|
|
||||||
find warehouse/src -name "*.vue" -o -name "*.ts" -o -name "*.js" | xargs sed -i 's/console\.log(/if(import\.meta\.env\.DEV)console.log(/g'
|
|
||||||
```
|
|
||||||
|
|
||||||
### G6 [⚪] 无全局错误边界
|
|
||||||
|
|
||||||
**修复** — `main.ts`:
|
|
||||||
```typescript
|
|
||||||
app.config.errorHandler = (err) => {
|
|
||||||
console.error('Global error:', err)
|
|
||||||
ElMessage.error('系统异常,请刷新页面')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### G7 [⚪] `counter.ts` 模板残留
|
|
||||||
|
|
||||||
**修复**: 删除 `warehouse/src/stores/counter.ts`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 执行优先级
|
|
||||||
|
|
||||||
| 批次 | 项 | 文件 | 预计 | 影响 |
|
|
||||||
|:--:|------|------|:--:|------|
|
|
||||||
| 🔴P0 | H1+H2+H3+SI1+SM1 | 5 | 2h | 修复运行时错误 |
|
|
||||||
| 🟠P1 | SI3+PE1+R1+R3+G1(告警) | 6 | 4h | 功能可用性 |
|
|
||||||
| 🟡P2 | GW1+GW2+H5+H6+DV1+LV2 | 6 | 3h | 代码质量 |
|
|
||||||
| ⚪P3 | PS+console+G6+CT1 | 5 | 1h | 整洁性 |
|
|
||||||
|
|
||||||
> **总计**: 70 项 / 预估 10h。P0 批 5 项必须在联调前完成。
|
|
||||||
@@ -1,535 +0,0 @@
|
|||||||
# 网关项目深度代码审核报告
|
|
||||||
|
|
||||||
> 日期: 2026-06-04 | 项目: gateway/ | 扫描: 38 源文件, ~2800 行 | 问题: 30 项
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Core/Infrastructure/AdapterRegistry.cs — 2 项
|
|
||||||
|
|
||||||
### AR1 [🟡] `GetOnlineAdapters()` 方法名误导
|
|
||||||
|
|
||||||
**位置**: 第 49-50 行
|
|
||||||
```csharp
|
|
||||||
public IReadOnlyList<IGatewayAdapter> GetOnlineAdapters()
|
|
||||||
=> _adapters.AsReadOnly();
|
|
||||||
```
|
|
||||||
返回所有已注册适配器,**不做任何在线/离线判断**。调用者会误以为返回的是在线适配器。
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```csharp
|
|
||||||
public IReadOnlyList<IGatewayAdapter> GetAllAdapters()
|
|
||||||
=> _adapters.AsReadOnly();
|
|
||||||
```
|
|
||||||
同时 grep 全项目无任何代码调用此方法,可直接删除(或保留为兼容性方法并标注 `[Obsolete]`)。
|
|
||||||
|
|
||||||
### AR2 [🟡] `FindByCode<T>` O(n) 查找无缓存
|
|
||||||
|
|
||||||
每次 B 路由请求都执行 `_adapters.FirstOrDefault(...)`,适配器数量少时影响可忽略(<10 个),但如果未来扩展到 50+ 适配器会有性能问题。
|
|
||||||
|
|
||||||
**修复** — 加字典缓存:
|
|
||||||
```csharp
|
|
||||||
private readonly Dictionary<string, IGatewayAdapter> _byCode = new();
|
|
||||||
public void Register(IGatewayAdapter adapter)
|
|
||||||
{
|
|
||||||
_adapters.Add(adapter);
|
|
||||||
_byCode[adapter.AdapterCode] = adapter;
|
|
||||||
}
|
|
||||||
public T? FindByCode<T>(string adapterCode) where T : class, IGatewayAdapter
|
|
||||||
=> _byCode.TryGetValue(adapterCode, out var a) ? a as T : null;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Core/Infrastructure/RateLimiter.cs — 1 项
|
|
||||||
|
|
||||||
### RL1 [🟠] `Task.Run` 无限制创建后台任务
|
|
||||||
|
|
||||||
**位置**: 第 30-32 行
|
|
||||||
```csharp
|
|
||||||
public async Task WaitAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
await _semaphore.WaitAsync(ct);
|
|
||||||
_ = Task.Run(async () => { await Task.Delay(_intervalMs, ct); try { _semaphore.Release(); } catch { } }, ct);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
每次等待都创建一个新的 `Task.Run`。在 KMS 5 QPS 配置下每秒创建 5 个 Task,5 个适配器 = 25 task/s。虽然 Task 本身轻量,但 `Task.Delay` + `Release` 的火焰纹章在极端高并发下可能导致线程池饥饿。
|
|
||||||
|
|
||||||
**修复** — 使用 `PeriodicTimer` 或固定数量的后台任务:
|
|
||||||
```csharp
|
|
||||||
private readonly SemaphoreSlim _semaphore;
|
|
||||||
private readonly int _maxTokens;
|
|
||||||
|
|
||||||
public RateLimiter(int tokensPerSecond)
|
|
||||||
{
|
|
||||||
_maxTokens = tokensPerSecond;
|
|
||||||
_semaphore = new SemaphoreSlim(_maxTokens, _maxTokens);
|
|
||||||
// 单后台任务持续补充令牌
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
var interval = 1000 / _maxTokens;
|
|
||||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(interval));
|
|
||||||
while (await timer.WaitForNextTickAsync())
|
|
||||||
{
|
|
||||||
try { if (_semaphore.CurrentCount < _maxTokens) _semaphore.Release(); }
|
|
||||||
catch (ObjectDisposedException) { break; }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WaitAsync(CancellationToken ct = default)
|
|
||||||
=> await _semaphore.WaitAsync(ct);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Core/Models/AdapterCapabilities.cs — 1 项
|
|
||||||
|
|
||||||
### AC1 [🟡] `HasPtz` 与 `AcceptsControl` 字段冗余
|
|
||||||
|
|
||||||
**位置**: 第 10-13 行
|
|
||||||
```csharp
|
|
||||||
public bool HasPtz { get; set; }
|
|
||||||
public bool AcceptsControl { get; set; }
|
|
||||||
```
|
|
||||||
`HasPtz` 是 `HasStreams` 的子能力,`AcceptsControl` 是 `IAcceptsControl` 接口的声明。这两个字段在 `AdapterCapabilities` 中冗余——`IAcceptsControl` 接口本身已经声明了控制能力。
|
|
||||||
|
|
||||||
**修复** — 删除 `AcceptsControl` 字段(能力声明应直接检查接口实现):
|
|
||||||
```csharp
|
|
||||||
// 删除 AcceptsControl、HasPtz
|
|
||||||
// 网关 B 路由中改为: a is IAcceptsControl
|
|
||||||
```
|
|
||||||
|
|
||||||
**跨文件影响**: `gateway/src/IntegrationGateway.Host/Program.cs` — B10 路由已用 `FindByCode<IAcceptsControl>` 不再需要此字段。OwlAdapter/Mc4Adapter 的 Capabilities 声明中删除对应行。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Core/Models/StandardDevice.cs — 1 项
|
|
||||||
|
|
||||||
### SD1 [🟡] `DeviceId` 字段职责混淆
|
|
||||||
|
|
||||||
**位置**: 第 9 行
|
|
||||||
```csharp
|
|
||||||
public int DeviceId { get; set; }
|
|
||||||
```
|
|
||||||
`DeviceId` 是 VolPro 侧的主键,由 A3 同步后由 VolPro 回填。但网关在 `GetDevicesAsync` 返回时不设置此字段(始终为 0)。文档注释说"同步后由 VolPro 回填",但调用者可能在未同步时就读取此字段。
|
|
||||||
|
|
||||||
**修复** — 改为 `int?` 并初始化为 null:
|
|
||||||
```csharp
|
|
||||||
public int? DeviceId { get; set; }
|
|
||||||
// VolPro 回填后才有值
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Core/Abstractions/IHasRecordings.cs — 1 项
|
|
||||||
|
|
||||||
### IR1 [🟡] 方法参数过多
|
|
||||||
|
|
||||||
**位置**: 第 12-13 行
|
|
||||||
```csharp
|
|
||||||
Task<PagedResult<StandardRecording>> GetRecordingsAsync(
|
|
||||||
string channelId, DateTime start, DateTime end, int page, int size);
|
|
||||||
```
|
|
||||||
5 个位置参数,调用时易错序。
|
|
||||||
|
|
||||||
**修复** — 使用请求对象:
|
|
||||||
```csharp
|
|
||||||
public class RecordingQuery
|
|
||||||
{
|
|
||||||
public string ChannelId { get; set; } = "";
|
|
||||||
public DateTime Start { get; set; }
|
|
||||||
public DateTime End { get; set; }
|
|
||||||
public int Page { get; set; } = 1;
|
|
||||||
public int Size { get; set; } = 20;
|
|
||||||
}
|
|
||||||
Task<PagedResult<StandardRecording>> GetRecordingsAsync(RecordingQuery query);
|
|
||||||
```
|
|
||||||
**跨文件影响**: `OwlAdapter.cs` GetRecordingsAsync 实现 + `Program.cs` B 路由。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Core/Infrastructure/GatewayClientFactory.cs — 2 项
|
|
||||||
|
|
||||||
### GF1 [🟡] `JsonDocument?` 返回类型不透明
|
|
||||||
|
|
||||||
**位置**: 第 33-42 行
|
|
||||||
```csharp
|
|
||||||
public async Task<JsonDocument?> RegisterAsync(GatewayRegisterRequest req) { ... }
|
|
||||||
public async Task<JsonDocument?> SyncDevicesAsync(...) { ... }
|
|
||||||
```
|
|
||||||
返回 `JsonDocument?` 使调用者必须知道 VolPro 响应的 JSON 结构。
|
|
||||||
|
|
||||||
**修复** — 定义响应 DTO:
|
|
||||||
```csharp
|
|
||||||
public class RegisterResponse { public int NodeId { get; set; } public List<DeviceSummary> Devices { get; set; } = new(); }
|
|
||||||
public Task<RegisterResponse?> RegisterAsync(GatewayRegisterRequest req) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
### GF2 [🟡] `CreateClient()` 每次创建新 HttpClient
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
private HttpClient CreateClient() => _httpFactory.CreateClient("VolPro");
|
|
||||||
```
|
|
||||||
`IHttpClientFactory.CreateClient("VolPro")` 从连接池返回,每次调用都会创建新的 `HttpClient` 包装实例。虽然底层 SocketsHttpHandler 复用连接,但 `HttpClient` 上设置的 Timeout/Headers 不会被保留。
|
|
||||||
|
|
||||||
**修复** — 直接使用工厂客户端(已配置好 Timeout/Headers):
|
|
||||||
```csharp
|
|
||||||
// Program.cs 中注册时已设置 Timeout=30s + Accept: application/json
|
|
||||||
// 直接使用 factory 创建的客户端
|
|
||||||
private HttpClient GetClient() => _httpFactory.CreateClient("VolPro");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Adapters.Owl/OwlAdapter.cs — 5 项
|
|
||||||
|
|
||||||
### OW1 [🟠] `GetDevicesAsync` 忽略 `page`/`size` 参数
|
|
||||||
|
|
||||||
**位置**: 第 68 行
|
|
||||||
```csharp
|
|
||||||
var url = $"/devices/channels?page={page}&size=1000";
|
|
||||||
```
|
|
||||||
硬编码 `size=1000`,无论调用者传的 `size` 值是多少。前端分页完全失效。
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```csharp
|
|
||||||
var url = $"/devices/channels?page={page}&size={size}";
|
|
||||||
```
|
|
||||||
|
|
||||||
### OW2 [🟡] `GetPlaybackUrlAsync` 手工拼 URL 脆弱
|
|
||||||
|
|
||||||
**位置**: 第 149-153 行
|
|
||||||
```csharp
|
|
||||||
return new StreamUrls
|
|
||||||
{
|
|
||||||
Hls = $"{baseUrl}/recordings/channels/{channelId}/index.m3u8?start_ms={startMs}&end_ms={endMs}&token={token}"
|
|
||||||
};
|
|
||||||
```
|
|
||||||
直接拼接 URL,依赖 Owl 内部路径约定。Owl API 版本升级可能改变路径格式。
|
|
||||||
|
|
||||||
**修复** — 调 Owl API 获取回放地址(如果有对应接口)或至少将 base URL 路径前缀提取为常量:
|
|
||||||
```csharp
|
|
||||||
private const string PlaybackPathFormat = "/recordings/channels/{0}/index.m3u8?start_ms={1}&end_ms={2}&token={3}";
|
|
||||||
var hls = string.Format(PlaybackPathFormat, channelId, startMs, endMs, token);
|
|
||||||
```
|
|
||||||
|
|
||||||
### OW3 [🟡] `MapChannel` IsOnline 判断脆弱
|
|
||||||
|
|
||||||
**位置**: 第 116 行
|
|
||||||
```csharp
|
|
||||||
IsOnline = ch.IsOnline?.ToLower() == "true" || ch.IsOnline == "1",
|
|
||||||
```
|
|
||||||
Owl 的 `IsOnline` 字段在不同接口中返回类型不同:设备接口返回 `"0"/"1"`,通道接口可能返回 `true/false` 字符串。这种脆弱的兼容方式在 Owl 版本升级后可能失效。
|
|
||||||
|
|
||||||
**修复** — 统一为 bool 解析:
|
|
||||||
```csharp
|
|
||||||
private static bool ParseOwlOnline(string? val) =>
|
|
||||||
bool.TryParse(val, out var b) ? b : val == "1";
|
|
||||||
```
|
|
||||||
|
|
||||||
### OW4 [🟡] `MapDevice` + `MapChannel` 硬编码设备名
|
|
||||||
|
|
||||||
**位置**: 第 91, 113 行
|
|
||||||
```csharp
|
|
||||||
Category = "硬盘录像机", Group = "视频设备"
|
|
||||||
Category = "摄像机", Group = "视频设备"
|
|
||||||
```
|
|
||||||
**修复** — 提取为常量:
|
|
||||||
```csharp
|
|
||||||
private const string DEVICE_CATEGORY_NVR = "硬盘录像机";
|
|
||||||
private const string DEVICE_CATEGORY_CAMERA = "摄像机";
|
|
||||||
private const string DEVICE_GROUP_VIDEO = "视频设备";
|
|
||||||
```
|
|
||||||
|
|
||||||
### OW5 [⚪] 文件 308 行过长
|
|
||||||
|
|
||||||
**修复** — 拆分为:
|
|
||||||
- `OwlAdapter.cs` — 类声明 + 构造函数 + Capabilities (40 行)
|
|
||||||
- `OwlAdapter.FlatDevices.cs` — IHasFlatDevices 实现 (70 行)
|
|
||||||
- `OwlAdapter.Streams.cs` — IHasStreams 实现 (80 行)
|
|
||||||
- `OwlAdapter.Recordings.cs` — IHasRecordings (30 行)
|
|
||||||
- `OwlAdapter.Alarms.cs` — IHasAlarms (60 行)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Adapters.Owl/OwlAuthHelper.cs — 0 项
|
|
||||||
|
|
||||||
代码质量良好的 RSA 加密认证实现。Token 缓存策略合理(2.5 天/3 天)。无可优化项。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Adapters.Owl/OwlModels.cs — 1 项
|
|
||||||
|
|
||||||
### OM1 [🟡] `OwlDeviceChannel` 字段注释缺失
|
|
||||||
|
|
||||||
**位置**: 第 17-38 行
|
|
||||||
`Type`、`Did`、`Ptztype`、`App`、`StreamId` 等字段无注释说明其含义和取值范围。
|
|
||||||
|
|
||||||
**修复** — 添加 XML 注释:
|
|
||||||
```csharp
|
|
||||||
/// <summary>类型: "DEVICE"(NVR) | "CHANNEL"(摄像头)</summary>
|
|
||||||
public string? Type { get; set; }
|
|
||||||
/// <summary>设备 ID(通道记录指向其所属设备)</summary>
|
|
||||||
public string? Did { get; set; }
|
|
||||||
/// <summary>云台类型: 0=无, 1=方向, 2=预置位</summary>
|
|
||||||
public int? Ptztype { get; set; }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Adapters.MC4/Mc4Adapter.cs — 4 项
|
|
||||||
|
|
||||||
### MC1 [🟠] `GetAlarmsAsync` DateTime.MinValue 仍发送
|
|
||||||
|
|
||||||
**位置**: 第 145-146 行
|
|
||||||
```csharp
|
|
||||||
From = from.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
||||||
To = to.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
||||||
```
|
|
||||||
当 `from`/`to` 为 `DateTime.MinValue`(B8 路由默认值)时,发送 `"0001-01-01 00:00:00"` 给 MC4——MC4 将认为这是有效日期过滤。
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```csharp
|
|
||||||
From = from == DateTime.MinValue ? "" : from.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
||||||
To = to == DateTime.MinValue ? "" : to.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
||||||
```
|
|
||||||
|
|
||||||
### MC2 [🟡] MC4 模型应独立文件
|
|
||||||
|
|
||||||
**位置**: 第 266-350 行
|
|
||||||
`Mc4TreeNode`、`Mc4PointValue`、`Mc4AlarmQuery` 等 8 个类全部定义在 `Mc4Adapter.cs` 底部。
|
|
||||||
|
|
||||||
**修复** — 创建 `Mc4Models.cs`(参照 KMS/Owl 的模式),将 266 行之后的模型移出。
|
|
||||||
|
|
||||||
### MC3 [🟡] `GetMultiRealtimeValuesAsync` + `GetHisAlarmsAsync` 未暴露到 B 路由
|
|
||||||
|
|
||||||
**位置**: 第 211, 228 行
|
|
||||||
这两个方法是 MC4.0 原生批量接口,但 Program.cs 中没有对应的 B 路由暴露它们。B4-batch 路由直接用 `IHasPoints.GetRealtimeValuesAsync` 逐设备调用。
|
|
||||||
|
|
||||||
**修复** — B4-batch 路由已检查 `Mc4Adapter` 类型并优先调用 `GetMultiRealtimeValuesAsync`(已实现)。确认编译通过即可。
|
|
||||||
|
|
||||||
### MC4 [🟡] `ConfirmAlarmAsync` / `EndAlarmAsync` 不检查响应
|
|
||||||
|
|
||||||
**位置**: 第 175-192 行
|
|
||||||
```csharp
|
|
||||||
await client.PostAsync("/api/central/alarm/confirm", ...); // 无 resp.EnsureSuccessStatusCode()
|
|
||||||
```
|
|
||||||
MC4 返回非 200 时静默失败,调用者认为确认成功。
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```csharp
|
|
||||||
var resp = await client.PostAsync(...);
|
|
||||||
resp.EnsureSuccessStatusCode();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Adapters.MC4/Mc4AuthHelper.cs — 1 项
|
|
||||||
|
|
||||||
### MA1 [🟡] `_needMd5` 只获取一次
|
|
||||||
|
|
||||||
**位置**: 第 46-57 行
|
|
||||||
```csharp
|
|
||||||
if (!_needMd5.HasValue) { /* 仅首次调用时查 conf/get */ }
|
|
||||||
```
|
|
||||||
如果 MC4 服务重启并更改了加密配置(encrypt true↔false),适配器不会重新检测。但实际场景中 MC4 不会在运行时切换加密模式,风险极低。
|
|
||||||
|
|
||||||
**修复** — 加一个 Token 刷新计数器,每 N 次重新获取 conf/get(N=10 即可)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Adapters.Kms/KmsAdapter.cs — 1 项
|
|
||||||
|
|
||||||
### KM1 [⚪] `OpenerIds` 缩进不齐
|
|
||||||
|
|
||||||
**位置**: 第 296 行
|
|
||||||
```csharp
|
|
||||||
OpenerIds = parameters.TryGetValue(...)
|
|
||||||
```
|
|
||||||
前面的注释和代码使用 8 空格缩进,`OpenerIds` 使用 0 空格缩进。视觉效果不一致但不影响编译。
|
|
||||||
|
|
||||||
**修复** — 统一缩进为 8 空格。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Adapters.Kms/KmsAuthHelper.cs — 0 项
|
|
||||||
|
|
||||||
Token 缓存 25min(30-5)、Bearer 头、Invalidate 均实现正确。无可优化项。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Adapters.Kms/KmsModels.cs — 0 项
|
|
||||||
|
|
||||||
15 个 DTO 覆盖全部 KMS 接口,字段完整。标准接口 DTO 虽暂未使用,但注释说明 Phase 2 用途——保留合理。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 15. Host/Program.cs — 6 项
|
|
||||||
|
|
||||||
### PR1 [🟠] `SyncAllDevicesAsync` 和 `FlattenTree` 在两个层次定义
|
|
||||||
|
|
||||||
**位置**: 第 152-184 行
|
|
||||||
这两个函数在 `InitializeAllAsync` 回调的闭包作用域中定义为局部函数。如果未来需要在其他地方调用(如 A3 手动触发),将不可用。
|
|
||||||
|
|
||||||
**修复** — 提取为私有静态方法或迁移到 Core:
|
|
||||||
```csharp
|
|
||||||
// 新建 Core/Infrastructure/DeviceSyncHelper.cs
|
|
||||||
public static class DeviceSyncHelper
|
|
||||||
{
|
|
||||||
public static async Task SyncAllAsync(AdapterRegistry reg, GatewayClientFactory factory, string nodeCode, string token) { ... }
|
|
||||||
private static void FlattenTree(...) { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PR2 [🟡] Swagger 无 XML 注释
|
|
||||||
|
|
||||||
**位置**: 第 17-19 行
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddSwaggerGen(); // 无 XML 注释选项
|
|
||||||
```
|
|
||||||
所有 Minimal API 端点没有 Swagger 描述,调用者必须参考外部文档。
|
|
||||||
|
|
||||||
**修复**:
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddSwaggerGen(c =>
|
|
||||||
{
|
|
||||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
|
||||||
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFile));
|
|
||||||
});
|
|
||||||
```
|
|
||||||
并在 `.csproj` 中添加 `<GenerateDocumentationFile>true</GenerateDocumentationFile>`。
|
|
||||||
|
|
||||||
### PR3 [🟡] 路由注册代码可读性差
|
|
||||||
|
|
||||||
**位置**: 第 186-377 行
|
|
||||||
19 条 B 路由全部内联在 `Program.cs` 中,每条路由 5-10 行,总计 ~200 行路由注册代码。
|
|
||||||
|
|
||||||
**修复** — 按模块拆分为扩展方法:
|
|
||||||
```csharp
|
|
||||||
// Host/Routes/HealthRoutes.cs
|
|
||||||
public static class HealthRoutes
|
|
||||||
{
|
|
||||||
public static void MapHealthEndpoints(this WebApplication app, AdapterRegistry registry) { ... }
|
|
||||||
}
|
|
||||||
// Host/Routes/DeviceRoutes.cs — B2, B3, B3-sync
|
|
||||||
// Host/Routes/StreamRoutes.cs — B6a, B6b, B7, snapshot
|
|
||||||
// Host/Routes/RealtimeRoutes.cs — B4, B4-batch, B5
|
|
||||||
// Host/Routes/AlarmRoutes.cs — B8, B9-confirm, B9-end
|
|
||||||
// Host/Routes/ControlRoutes.cs — B10
|
|
||||||
// Host/Routes/LogRoutes.cs — B11
|
|
||||||
// Host/Routes/SyncRoutes.cs — B12, B13
|
|
||||||
```
|
|
||||||
Program.cs 改为:
|
|
||||||
```csharp
|
|
||||||
app.MapHealthEndpoints(registry);
|
|
||||||
app.MapDeviceEndpoints(registry);
|
|
||||||
// ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### PR4 [🟡] 配置缺少验证
|
|
||||||
|
|
||||||
**位置**: 第 53-83 行
|
|
||||||
`app.Configuration.GetSection("Owl").Get<List<OwlConfig>>()` 可能在配置格式错误时返回 null,直接 `foreach` 正常运作但无错误提示。
|
|
||||||
|
|
||||||
**修复** — 加空值检查和警告:
|
|
||||||
```csharp
|
|
||||||
var owlList = app.Configuration.GetSection("Owl").Get<List<OwlConfig>>();
|
|
||||||
if (owlList == null || !owlList.Any()) Console.WriteLine("[Gateway] WARNING: 未配置 Owl 适配器");
|
|
||||||
```
|
|
||||||
|
|
||||||
### PR5 [🟡] `appsettings.json` 凭证明文
|
|
||||||
|
|
||||||
**位置**: appsettings.json
|
|
||||||
```json
|
|
||||||
"NodeToken": "changeme",
|
|
||||||
"Password": "your_owl_password",
|
|
||||||
"ClientSecret": "your_client_secret"
|
|
||||||
```
|
|
||||||
|
|
||||||
**修复** — 生产环境注入:
|
|
||||||
```json
|
|
||||||
"NodeToken": null, // 生产环境由 SECMPS_GATEWAY_TOKEN 环境变量注入
|
|
||||||
"Password": null, // 生产环境由 OWL_PASSWORD 注入
|
|
||||||
"ClientSecret": null // 生产环境由 KMS_CLIENT_SECRET 注入
|
|
||||||
```
|
|
||||||
Program.cs:
|
|
||||||
```csharp
|
|
||||||
var nodeToken = Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"];
|
|
||||||
```
|
|
||||||
|
|
||||||
### PR6 [⚪] dotnet-tools.json 未使用
|
|
||||||
|
|
||||||
`gateway/src/IntegrationGateway.Host/dotnet-tools.json` 存在但可能无声明工具。确认是否被使用。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 16. 跨切面问题 — 4 项
|
|
||||||
|
|
||||||
### X1 [🟡] 无统一日志抽象
|
|
||||||
|
|
||||||
所有适配器使用 `Console.Error.WriteLine` 直接写控制台。无日志级别、无结构化输出、无法对接日志收集系统。
|
|
||||||
|
|
||||||
**修复** — 注入 `ILogger<T>`:
|
|
||||||
```csharp
|
|
||||||
public class OwlAdapter : ...
|
|
||||||
{
|
|
||||||
private readonly ILogger<OwlAdapter> _logger;
|
|
||||||
public OwlAdapter(..., ILogger<OwlAdapter> logger) { _logger = logger; }
|
|
||||||
|
|
||||||
public async Task<bool> HealthCheckAsync()
|
|
||||||
{
|
|
||||||
try { ... }
|
|
||||||
catch (Exception ex) { _logger.LogWarning(ex, "[{Code}] HealthCheck 失败", AdapterCode); return false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### X2 [🟡] `GetAuthenticatedClientAsync` 每次 new HttpClient
|
|
||||||
|
|
||||||
三个适配器(Owl/KMS/MC4)的 AuthHelper 都在 `GetAuthenticatedClientAsync` 中 `new HttpClient { BaseAddress = new Uri(_baseUrl) }`。每个请求创建一个新的 HttpClient 实例,无法复用连接池。
|
|
||||||
|
|
||||||
**修复** — 传入 `IHttpClientFactory`:
|
|
||||||
```csharp
|
|
||||||
public class KmsAuthHelper
|
|
||||||
{
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
|
||||||
public async Task<HttpClient> GetAuthenticatedClientAsync()
|
|
||||||
{
|
|
||||||
var token = await GetTokenAsync();
|
|
||||||
var client = _httpFactory.CreateClient(); // 连接池复用
|
|
||||||
client.BaseAddress = new Uri(_baseUrl);
|
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**跨文件影响**: 三个 AuthHelper 构造函数全部需加 `IHttpClientFactory` 参数。三个 Adapter 构造函数也需传递。
|
|
||||||
|
|
||||||
### X3 [🟡] 无 request/response DTO 版本策略
|
|
||||||
|
|
||||||
当前所有接口模型无版本号字段。未来接口升级时无法区分新旧格式。
|
|
||||||
|
|
||||||
**修复** — 在 B 路由响应中加 `version` 字段:
|
|
||||||
```csharp
|
|
||||||
return Results.Ok(new { version = "1.0", items = result.Items, total = result.Total });
|
|
||||||
```
|
|
||||||
|
|
||||||
### X4 [⚪] `csproj` 无 `<GenerateDocumentationFile>` 配置
|
|
||||||
|
|
||||||
三个适配器项目均无 XML 文档生成配置。
|
|
||||||
|
|
||||||
**修复** — 在各 `.csproj` 中添加 `<GenerateDocumentationFile>true</GenerateDocumentationFile>`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 统计
|
|
||||||
|
|
||||||
| 级别 | 数量 | 位置 |
|
|
||||||
|:--:|:--:|------|
|
|
||||||
| 🟠 严重 | 4 | RateLimiter(Owl.GetDevices size,MC4 MinValue,Program SyncAll) |
|
|
||||||
| 🟡 改善 | 21 | 命名/拆分/日志/HttpClient/配置 |
|
|
||||||
| ⚪ 低优 | 5 | 缩进/tools.json/注释 |
|
|
||||||
|
|
||||||
## 总评
|
|
||||||
|
|
||||||
网关架构设计优秀——适配器模式隔离清晰、限流/认证/错误处理一致、接口粒度过细(19 条 B 路由)而非不足。最大改进空间在于:HttpClient 连接池复用、日志抽象统一、Program.cs 路由拆分。
|
|
||||||
File diff suppressed because it is too large
Load Diff
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`)
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
# KMS 钥匙柜适配器详细设计文档
|
|
||||||
|
|
||||||
> **版本**: 1.0
|
|
||||||
> **日期**: 2025-05-19
|
|
||||||
> **基准**: `doc/整合方案/KMS钥匙柜整合方案_v2.0.md`
|
|
||||||
> **接口文档**: `doc/对接文档/钥匙管理系统软件接口.docx` (KMS API v1.0.4)
|
|
||||||
> **技术栈**: .NET 8 / ASP.NET Core / C#
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 接口覆盖设计
|
|
||||||
|
|
||||||
### 1.1 完整 KMS API 概览
|
|
||||||
|
|
||||||
KMS 系统共有 **38 个 REST 端点**,分 9 大类。按设计原则,适配器只代理 **第三方集成接口**(第 2.18 节),不代理 KMS 自身管理接口。
|
|
||||||
|
|
||||||
### 1.2 第三方集成接口(Phase 1 — 核心实现)
|
|
||||||
|
|
||||||
以下 8 个接口是 KMS 专为第三方对接设计的扁平化 API:
|
|
||||||
|
|
||||||
| # | 方法 | 路径 | 用途 | 适配器方法 |
|
|
||||||
|---|:---:|------|------|------|
|
|
||||||
| 2.18.1 | GET | `/prod-api/heartBeat` | 心跳检测 | `HealthCheckAsync()` |
|
|
||||||
| 2.18.2 | POST | `/prod-api/batchDeleteStaff` | 批量删除员工 | `BatchDeleteStaffAsync()` |
|
|
||||||
| 2.18.3 | POST | `/prod-api/batchSyncStaff` | 批量同步员工 | `BatchSyncStaffAsync()` |
|
|
||||||
| 2.18.4 | POST | `/prod-api/getOpenerList` | 查询柜体+钥匙信息 | `GetDevicesAsync()` |
|
|
||||||
| 2.18.5 | POST | `/prod-api/getPermissionList` | 查询授权记录 | `GetPermissionListAsync()` |
|
|
||||||
| 2.18.6 | POST | `/prod-api/getRecordList` | 查询借还记录 | `GetBorrowRecordsAsync()` |
|
|
||||||
| 2.18.7 | POST | `/prod-api/getWarningList` | 查询告警记录 | `GetAlarmsAsync()` |
|
|
||||||
| 2.18.8 | POST | `/thirdPlatlogin` | 第三方登录/事件 | `ThirdPlatLoginAsync()` |
|
|
||||||
|
|
||||||
加上认证接口 `POST /prod-api/getToken`,适配器共需对接 **9 个 KMS 接口**。
|
|
||||||
|
|
||||||
### 1.3 标准 KMS 管理接口(Phase 2 可选)
|
|
||||||
|
|
||||||
以下 29 个接口属于 KMS 自身管理功能(员工 CRUD、柜体 CRUD、钥匙 CRUD 等),KMS 自带 Web 管理端即可操作。Vol.Pro 如需代理这些接口,可在 Phase 2 扩展,但设计文档不列入必需实现。
|
|
||||||
|
|
||||||
| 模块 | 接口数 | 备注 |
|
|
||||||
|------|:---:|------|
|
|
||||||
| 交接记录 | 2 | GET `/kms/handover/*` |
|
|
||||||
| 授权管理 | 3 | GET `/kms/permission/*` + POST remote |
|
|
||||||
| 告警记录(标准) | 1 | GET `/kms/warning/list` |
|
|
||||||
| 员工可借钥匙 | 2 | POST/GET `/kms/staffopener/*` |
|
|
||||||
| 员工管理 | 5 | CRUD `/kms/staff/*` |
|
|
||||||
| 员工组管理 | 6 | CRUD `/kms/staffGroup/*` |
|
|
||||||
| 物品类别 | 6 | CRUD `/kms/openerType/*` |
|
|
||||||
| 柜体管理 | 5 | CRUD `/kms/locker/*` + statistics |
|
|
||||||
| 锁孔管理 | 4 | CRUD `/kms/lockhole/*` |
|
|
||||||
| 钥匙管理 | 7 | CRUD `/kms/opener/*` + selectCanBorrow |
|
|
||||||
| 钥匙组 | 7 | CRUD `/kms/openerGroup/*` |
|
|
||||||
| 部门 | 1 | GET `/system/dept/root/{userId}` |
|
|
||||||
| 授权详情 | 1 | GET `/kms/permissioninfo/getByPermissionId/{uuid}` |
|
|
||||||
| **合计** | **29** | Phase 2 按需实现 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
gateway/src/IntegrationGateway.Adapters.Kms/
|
|
||||||
├── IntegrationGateway.Adapters.Kms.csproj # 类库, net8.0
|
|
||||||
├── KmsAdapter.cs # 适配器主体 (IHasFlatDevices + IHasAlarms)
|
|
||||||
├── KmsAuthHelper.cs # Bearer Token 认证
|
|
||||||
└── KmsModels.cs # 请求/响应 DTO
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.1 依赖关系
|
|
||||||
|
|
||||||
```
|
|
||||||
Host → Adapters.Kms → Core
|
|
||||||
Host → Core
|
|
||||||
```
|
|
||||||
|
|
||||||
适配器只引用 Core,零外部 NuGet 依赖。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 数据模型设计
|
|
||||||
|
|
||||||
### 3.1 KMS 响应模型(全部 9 个第三方接口的 DTO)
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ── 认证 ──
|
|
||||||
public class KmsTokenResponse { public int Code { get; set; } public string Token { get; set; } = ""; public string? Msg { get; set; } }
|
|
||||||
|
|
||||||
// ── 2.18.4 柜体+钥匙 ──
|
|
||||||
public class KmsOpenerListResponse { public int Code { get; set; } public string? Msg { get; set; } public List<KmsLocker>? Rows { get; set; } }
|
|
||||||
public class KmsLocker { public int LockerId { get; set; } public string? LockerName { get; set; } public string? LockerCode { get; set; } public List<KmsLockhole>? LockholeList { get; set; } }
|
|
||||||
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; } }
|
|
||||||
|
|
||||||
// ── 2.18.7 告警 ──
|
|
||||||
public class KmsWarningListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List<KmsWarning>? Rows { get; set; } }
|
|
||||||
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; } }
|
|
||||||
|
|
||||||
// ── 2.18.6 借还记录 ──
|
|
||||||
public class KmsRecordListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List<KmsRecord>? Rows { get; set; } }
|
|
||||||
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.18.5 授权记录 ──
|
|
||||||
public class KmsPermissionListResponse { public int Code { get; set; } public string? Msg { get; set; } public int Total { get; set; } public List<KmsPermission>? Rows { get; set; } }
|
|
||||||
public class KmsPermission { 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? ApplyTime { get; set; } public string? BackTime { get; set; } }
|
|
||||||
|
|
||||||
// ── 2.18.3 员工同步 ──
|
|
||||||
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; } }
|
|
||||||
|
|
||||||
// ── 通用响应 ──
|
|
||||||
public class KmsApiResponse { public int Code { get; set; } public string? Msg { get; set; } }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 实体映射关系
|
|
||||||
|
|
||||||
```
|
|
||||||
KMS 物理拓扑 base_device 映射
|
|
||||||
─────── ──────────────────
|
|
||||||
智能钥匙柜A (lockerId=25) SourceId="locker_25", IsParent=是
|
|
||||||
├── 锁孔1 "仓库大门" SourceId="lockhole_25_1", ParentSourceId="locker_25"
|
|
||||||
├── 锁孔2 "机房钥匙" SourceId="lockhole_25_2", ParentSourceId="locker_25"
|
|
||||||
└── 锁孔N ...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. KmsAuthHelper 设计
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>
|
|
||||||
/// KMS Bearer Token 认证辅助。
|
|
||||||
/// Token 通过 POST /prod-api/getToken 获取,参数 clientId + clientSecret。
|
|
||||||
/// 缓存 25 分钟(KMS 有效期 30 分钟,留 5 分钟余量)。
|
|
||||||
/// </summary>
|
|
||||||
public class KmsAuthHelper
|
|
||||||
{
|
|
||||||
private readonly HttpClient _http;
|
|
||||||
private readonly string _baseUrl, _clientId, _clientSecret;
|
|
||||||
private string? _token;
|
|
||||||
private DateTime _tokenExpiry = DateTime.MinValue;
|
|
||||||
|
|
||||||
public KmsAuthHelper(HttpClient http, string baseUrl, string clientId, string clientSecret)
|
|
||||||
{
|
|
||||||
_http = http; _baseUrl = baseUrl.TrimEnd('/');
|
|
||||||
_clientId = clientId; _clientSecret = clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>获取或刷新 Token(自动缓存)</summary>
|
|
||||||
public async Task<string> GetTokenAsync()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_token) && DateTime.UtcNow < _tokenExpiry) return _token;
|
|
||||||
var resp = await _http.PostAsync(
|
|
||||||
$"{_baseUrl}/prod-api/getToken?clientId={Uri.EscapeDataString(_clientId)}&clientSecret={Uri.EscapeDataString(_clientSecret)}", 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</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Invalidate() => _token = null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. KmsAdapter 设计
|
|
||||||
|
|
||||||
### 5.1 类声明与核心属性
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>
|
|
||||||
/// KMS 智能钥匙柜适配器。
|
|
||||||
/// 实现 IHasFlatDevices + IHasAlarms。
|
|
||||||
/// 通过 8 个第三方接口(2.18.X)对接 KMS 子系统。
|
|
||||||
/// AdapterCode: "KMS:{InstanceName}",限流: 5 QPS。
|
|
||||||
/// </summary>
|
|
||||||
public class KmsAdapter : IHasFlatDevices, IHasAlarms
|
|
||||||
{
|
|
||||||
private readonly HttpClient _http;
|
|
||||||
private readonly KmsAuthHelper _auth;
|
|
||||||
private readonly RateLimiter _limiter = new(5);
|
|
||||||
|
|
||||||
public string AdapterCode { get; }
|
|
||||||
public string DisplayName => $"KMS ({AdapterCode})";
|
|
||||||
public AdapterCapabilities Capabilities => new() { HasFlatDevices = true, HasAlarms = true };
|
|
||||||
|
|
||||||
public KmsAdapter(string adapterCode, HttpClient http, string baseUrl, string clientId, string clientSecret)
|
|
||||||
{
|
|
||||||
AdapterCode = adapterCode; _http = http;
|
|
||||||
_auth = new KmsAuthHelper(http, baseUrl, clientId, clientSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeAsync() => await _auth.GetTokenAsync();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 IGatewayAdapter — 健康检查
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>2.18.1: 心跳检测 — GET /prod-api/heartBeat</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; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 IHasFlatDevices — 设备同步
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>2.18.4: 柜体+钥匙信息 — POST /prod-api/getOpenerList</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(new StandardDevice
|
|
||||||
{
|
|
||||||
SourceId = $"locker_{locker.LockerId}", Name = locker.LockerName ?? $"柜体{locker.LockerId}",
|
|
||||||
Category = "智能钥匙柜", Group = "门禁设备", IsParent = true, IsOnline = true,
|
|
||||||
Extra = new() { ["lockerCode"] = locker.LockerCode, ["lockholeCount"] = locker.LockholeList?.Count ?? 0 }
|
|
||||||
});
|
|
||||||
// 子设备: 锁孔(钥匙位)
|
|
||||||
foreach (var hole in locker.LockholeList ?? new())
|
|
||||||
{
|
|
||||||
devices.Add(new StandardDevice
|
|
||||||
{
|
|
||||||
SourceId = $"lockhole_{locker.LockerId}_{hole.LockholeSort}",
|
|
||||||
Name = hole.OpenerName ?? $"锁孔{hole.LockholeSort}",
|
|
||||||
Category = "钥匙位", Group = "门禁设备", IsParent = false,
|
|
||||||
IsOnline = hole.OpenerState == "在位", ParentSourceId = $"locker_{locker.LockerId}",
|
|
||||||
Extra = new() { ["openerId"] = hole.OpenerId, ["openerType"] = hole.OpenerType, ["openerState"] = hole.OpenerState }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new PagedResult<StandardDevice> { Items = devices, Total = devices.Count };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.4 IHasAlarms — 告警同步
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>2.18.7: 告警记录 — POST /prod-api/getWarningList</summary>
|
|
||||||
public async Task<PagedResult<StandardAlarm>> GetAlarmsAsync(...)
|
|
||||||
{
|
|
||||||
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 = "普通",
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.5 扩展方法(非接口方法,供 B 组路由直接调用)
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 2.18.6 借还记录
|
|
||||||
public async Task<PagedResult<KmsRecord>> GetBorrowRecordsAsync(DateTime? from = null, DateTime? to = null) { ... }
|
|
||||||
|
|
||||||
// 2.18.5 授权记录
|
|
||||||
public async Task<PagedResult<KmsPermission>> GetPermissionListAsync(DateTime? from = null, DateTime? to = null) { ... }
|
|
||||||
|
|
||||||
// 2.18.3 批量同步员工
|
|
||||||
public async Task BatchSyncStaffAsync(List<KmsStaff> staffList) { ... }
|
|
||||||
|
|
||||||
// 2.18.2 批量删除员工
|
|
||||||
public async Task BatchDeleteStaffAsync(List<string> staffUuids) { ... }
|
|
||||||
|
|
||||||
// 2.4.3 远程授权开门
|
|
||||||
public async Task RemoteAuthorizeAsync(KmsRemotePermissionRequest request) { ... }
|
|
||||||
|
|
||||||
// 2.18.8 第三方登录代理
|
|
||||||
public async Task<string?> ThirdPlatLoginAsync(string username) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 配置
|
|
||||||
|
|
||||||
### 6.1 KmsConfig POCO
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class KmsConfig
|
|
||||||
{
|
|
||||||
public string? InstanceName { get; set; }
|
|
||||||
public string BaseUrl { get; set; } = "";
|
|
||||||
public string ClientId { get; set; } = "";
|
|
||||||
public string ClientSecret { get; set; } = "";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 appsettings.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"KMS": [
|
|
||||||
{
|
|
||||||
"InstanceName": "main",
|
|
||||||
"BaseUrl": "http://192.168.1.50:8080",
|
|
||||||
"ClientId": "your_client_id",
|
|
||||||
"ClientSecret": "your_client_secret"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Program.cs 注册
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var kmsList = app.Configuration.GetSection("KMS").Get<List<KmsConfig>>() ?? new();
|
|
||||||
foreach (var k in kmsList)
|
|
||||||
{
|
|
||||||
var code = $"KMS:{k.InstanceName ?? "default"}";
|
|
||||||
var a = new KmsAdapter(code,
|
|
||||||
app.Services.GetRequiredService<IHttpClientFactory>().CreateClient("VolPro"),
|
|
||||||
k.BaseUrl, k.ClientId, k.ClientSecret);
|
|
||||||
registry.Register(a);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Vol.Pro 端配套
|
|
||||||
|
|
||||||
| 项 | 改动 | 说明 |
|
|
||||||
|------|:---:|------|
|
|
||||||
| 数据库 | 无 | base_device / iot_alarm 已兼容 |
|
|
||||||
| 后端 | 无 | A1-A4 同步逻辑通用 |
|
|
||||||
| 字典 | 新增 2 项 | "智能钥匙柜" / "钥匙位" |
|
|
||||||
| 前端列表 | 无 | 自动显示 KMS 设备 |
|
|
||||||
| 前端操作 | Phase 2 | KeyDeviceActions.vue(开门/授权按钮)|
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **接口覆盖**: 9 个第三方接口(2.18.X + Token)100% 设计覆盖;29 个标准管理接口留 Phase 2 按需扩展。
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
# KMS 钥匙柜适配器 — 任务清单
|
|
||||||
|
|
||||||
> **基准文档**: `doc/设计文档/KMS钥匙柜适配器详细设计文档.md`
|
|
||||||
> **分支**: 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 编译验证
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
> **K1 提交点**: `PhaseK1_models — KmsModels.cs 完整定义全部响应 DTO`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase K2: KmsAuthHelper — 认证(预计 30min)
|
|
||||||
|
|
||||||
### K2.1 创建 KmsAuthHelper.cs
|
|
||||||
- [ ] 构造函数:接收 `HttpClient`, `baseUrl`, `clientId`, `clientSecret`
|
|
||||||
- [ ] 属性:`_token` (string?), `_tokenExpiry` (DateTime)
|
|
||||||
|
|
||||||
### K2.2 GetTokenAsync
|
|
||||||
- [ ] POST `/prod-api/getToken?clientId=xx&clientSecret=yy`
|
|
||||||
- [ ] 校验 `Code == 200`
|
|
||||||
- [ ] 缓存 Token,过期时间 = `UtcNow.AddMinutes(25)`(30 分钟效期,5 分钟余量)
|
|
||||||
|
|
||||||
### K2.3 GetAuthenticatedClientAsync
|
|
||||||
- [ ] 创建 `HttpClient`,设置 `Authorization: Bearer {token}`
|
|
||||||
- [ ] Invalidate() → `_token = null`
|
|
||||||
|
|
||||||
### K2.4 编译验证
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
> **K2 提交点**: `PhaseK2_auth — Bearer Token 认证就绪`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase K3: KmsAdapter 核心方法(预计 1.5h)
|
|
||||||
|
|
||||||
### K3.1 类定义
|
|
||||||
- [ ] `public class KmsAdapter : IHasFlatDevices, IHasAlarms`
|
|
||||||
- [ ] 属性:`AdapterCode`, `DisplayName`, `Capabilities`
|
|
||||||
|
|
||||||
### K3.2 HealthCheckAsync(2.18.1)
|
|
||||||
- [ ] GET `/prod-api/heartBeat`
|
|
||||||
- [ ] 异常捕获返回 false + Console.Error 打日志
|
|
||||||
|
|
||||||
### K3.3 GetDevicesAsync(2.18.4)
|
|
||||||
- [ ] POST `/prod-api/getOpenerList` (body `{}`)
|
|
||||||
- [ ] 遍历柜体/锁孔 → 映射为 StandardDevice
|
|
||||||
- [ ] 父设备 `IsParent=是`, 子设备 `ParentSourceId=locker_{id}`
|
|
||||||
|
|
||||||
### K3.4 GetAlarmsAsync(2.18.7)
|
|
||||||
- [ ] POST `/prod-api/getWarningList`
|
|
||||||
- [ ] 映射 KmsWarning → StandardAlarm
|
|
||||||
- [ ] AlarmId=uuid, Status=Type==1?"未确认":"已结束"
|
|
||||||
|
|
||||||
### K3.5 ConfirmAlarmAsync / EndAlarmAsync
|
|
||||||
- [ ] Confirm 调标准接口;End 留空实现
|
|
||||||
|
|
||||||
### K3.6 编译验证
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
> **K3 提交点**: `PhaseK3_adapter_core — 核心4方法就绪`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase K4: 扩展方法(预计 1h)
|
|
||||||
|
|
||||||
### K4.1 借还/授权/员工/登录
|
|
||||||
- [ ] GetBorrowRecordsAsync(2.18.6)
|
|
||||||
- [ ] GetPermissionListAsync(2.18.5)
|
|
||||||
- [ ] BatchSyncStaffAsync(2.18.3)
|
|
||||||
- [ ] BatchDeleteStaffAsync(2.18.2)
|
|
||||||
- [ ] RemoteAuthorizeAsync(2.4.3)
|
|
||||||
- [ ] ThirdPlatLoginAsync(2.18.8)
|
|
||||||
|
|
||||||
### K4.2 编译验证
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
> **K4 提交点**: `PhaseK4_adapter_ext — 6个扩展方法就绪`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase K5: 配置与注册(预计 15min)
|
|
||||||
|
|
||||||
### K5.1 KmsConfig POCO
|
|
||||||
- [ ] 在 Program.cs 同级加 class,属性:`InstanceName, BaseUrl, ClientId, ClientSecret`
|
|
||||||
|
|
||||||
### K5.2 appsettings.json
|
|
||||||
- [ ] 新增 KMS 数组配置段
|
|
||||||
|
|
||||||
### K5.3 Program.cs 注册
|
|
||||||
- [ ] `var kmsList = app.Configuration.GetSection("KMS").Get<List<KmsConfig>>() ?? new();`
|
|
||||||
- [ ] foreach 注册 `KmsAdapter("KMS:{InstanceName}", ...)`
|
|
||||||
|
|
||||||
> **K5 提交点**: `PhaseK5_config — 配置+注册就绪`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase K6: 编译与自测(预计 15min)
|
|
||||||
|
|
||||||
### K6.1 编译验证
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
> **K6 提交点**: `PhaseK6_build — 全量编译通过`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase K7: Vol.Pro 端配套(预计 1h)
|
|
||||||
|
|
||||||
### K7.1 字典
|
|
||||||
- [ ] 管理端设备种类字典 ← "智能钥匙柜" + "钥匙位"
|
|
||||||
|
|
||||||
### K7.2 前端按钮
|
|
||||||
- [ ] `base_device.vue` 操作列:门禁设备 → [开门] [授权] 按钮
|
|
||||||
|
|
||||||
> **K7 提交点**: `PhaseK7_volpro — 字典+前端就绪`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase K8: 联调验证(预计 3h,需 KMS 环境)
|
|
||||||
|
|
||||||
### K8.1 认证
|
|
||||||
- [ ] 网关启动 → KmsAdapter.InitializeAsync 成功
|
|
||||||
|
|
||||||
### K8.2 设备/告警/记录
|
|
||||||
- [ ] /api/gateway/devices?adapter=KMS:main → 返回柜体+锁孔
|
|
||||||
- [ ] /api/gateway/alarms/KMS:main → 返回告警列表
|
|
||||||
- [ ] /api/gateway/control/KMS:main → 远程开门
|
|
||||||
|
|
||||||
### K9: 联调文档记录
|
|
||||||
- [ ] 记录异常接口到 KMS_联调笔记.txt
|
|
||||||
|
|
||||||
> **K8 提交点**: `PhaseK8_integration — 全链路联调通过`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
| Phase | 内容 | 文件 | 预计 |
|
|
||||||
|:---:|------|:---:|:---:|
|
|
||||||
| K0 | 项目骨架 | 2 | 15min |
|
|
||||||
| K1 | 全部 DTO | 1 | 1h |
|
|
||||||
| K2 | AuthHelper | 1 | 30min |
|
|
||||||
| K3 | 核心方法 | 1 | 1.5h |
|
|
||||||
| K4 | 扩展方法 | 1 | 1h |
|
|
||||||
| K5 | 配置注册 | 3 | 15min |
|
|
||||||
| K6 | 编译 | — | 15min |
|
|
||||||
| K7 | VolPro配套 | 2 | 1h |
|
|
||||||
| K8 | 联调 | — | 3h |
|
|
||||||
| **合计** | — | **11** | **~9h** |
|
|
||||||
@@ -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,56 +0,0 @@
|
|||||||
# SecMPS 统一问题清单 2026-06-03
|
|
||||||
|
|
||||||
> **版本**: 1.0
|
|
||||||
> **日期**: 2026-06-03
|
|
||||||
> **范围**: gateway / VolPro (api_sqlsugar) / web.vite / warehouse / owl_zlmediakit
|
|
||||||
> **来源**: 项目深度审计 + 规则引擎方案审查
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P0 — 阻塞性(影响功能完整性,必须修复)
|
|
||||||
|
|
||||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
|
||||||
|:---:|:---:|------|------|------|
|
|
||||||
| P0-1 | 规则引擎 | RealtimePollJob 空壳 — IoT 实时值从未持久化,规则引擎无历史数据源 | 规则无法追溯历史趋势 | 在此 Job 实现轮询→写入 iot_devicedata,或合并到 RuleEngineJob |
|
|
||||||
| P0-2 | 网关 | A1 自注册未调用 — `GatewayClientFactory.RegisterAsync` 已定义但 Program.cs 从未执行 | 网关启动后不向 Vol.Pro 注册 | `InitializeAllAsync()` 后遍历适配器调 A1 |
|
|
||||||
| P0-3 | 安全 | B 组路由零认证 — 14+ 条路由无任何认证 | 内网未授权客户端可操控设备、查视频流 | 生产环境绑定 `127.0.0.1`,或加 `X-Gateway-Key` 中间件 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P1 — 重要(影响性能、安全、可靠性)
|
|
||||||
|
|
||||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
|
||||||
|:---:|:---:|------|------|------|
|
|
||||||
| P1-1 | 性能 | 逐设备 B4 调用 — 规则引擎按设备逐个调 B4 | 规则引擎 90% 时间耗在网络往返 | 新增 `POST /realtime/{adapter}/batch` 批量接口 |
|
|
||||||
| P1-2 | 性能 | 级联离线标记逐条 UPDATE — HeartbeatMonitorJob 对每台设备单独更新 | 设备多时慢且无事务 | 一条 SQL: `UPDATE base_device SET IsOnline='离线' WHERE GatewayNodeId=@id` |
|
|
||||||
| P1-3 | 安全 | Token/密码明文存储 — appsettings.json 明文且被复制到 bin/ | 源码泄露 = 凭据泄露 | 环境变量覆盖 + `.gitignore bin` |
|
|
||||||
| P1-4 | 可维护 | 前端硬编码网关地址 — `const GW = 'http://localhost:5100'` | 部署时需逐文件修改 | 统一用 `window.apiConfig.gatewayUrl` |
|
|
||||||
| P1-5 | 规则引擎 | DeviceId→(AdapterCode, SourceId) 解析缺失 | 规则引擎无法直接调网关 B4 | 批量查 base_device 建映射表 |
|
|
||||||
| P1-6 | 规则引擎 | ValueId 语义模糊 — 字典绑定但无对应实体表 | "变量"选的是什么不明确 | 新建 `warehouse_variable` 表 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P2 — 改善(影响排错效率、维护成本)
|
|
||||||
|
|
||||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
|
||||||
|:---:|:---:|------|------|------|
|
|
||||||
| P2-1 | 代码质量 | 静默异常吞噬 — 适配器 `catch { return false; }` | 离线不知道原因 | `catch(Exception ex)` + STDERR 输出 |
|
|
||||||
| P2-2 | 规则引擎 | 阈值抖动 — 温度反复跳变时规则频繁触发→恢复 | 空调反复开关,告警洪水 | hysteresis 滞后窗 |
|
|
||||||
| P2-3 | 规则引擎 | 冷却期粒度 — Cooldown 在规则级,OR 组合不该整体冷却 | 冷却期过宽 | 冷却期下沉到条件表或基于"上次触发值"去重 |
|
|
||||||
| P2-4 | 可维护 | warehouse 端 console.log 残留 — 30+ 处开发日志 | 生产环境噪声 | vite.config.ts 移除非生产日志 |
|
|
||||||
| P2-5 | 可维护 | 双端 gateway API 重复封装 | 维护两份 | 统一到 web.vite/src/api/gateway.js |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## P3 — 优化(影响开发体验、仓库整洁)
|
|
||||||
|
|
||||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
|
||||||
|:---:|:---:|------|------|------|
|
|
||||||
| P3-1 | 规则引擎 | 动作执行阻塞 — `ExecuteActionsAsync` 串行等待 B5 响应 | 一条规则卡住全部阻塞 | `Task.WhenAll` + 5s 超时 |
|
|
||||||
| P3-2 | 文档 | bin 目录残留配置 | 仓库体积 + 凭据泄露 | `.gitignore` 加 `**/bin/` |
|
|
||||||
| P3-3 | 开发 | 网关无 Swagger | 调试需手动 curl | `AddEndpointsApiExplorer` + `MapSwagger` |
|
|
||||||
| P3-4 | 文档 | 设计文档与代码路由数不一致 | 架构文档过时 | 每次 Phase 同步更新 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **总计**: 18 项 — P0: 3 / P1: 6 / P2: 5 / P3: 4
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
# SecMPS 统一问题清单 2026-06-03 修复方案
|
|
||||||
|
|
||||||
> **版本**: 1.0
|
|
||||||
> **日期**: 2026-06-03
|
|
||||||
> **基准**: `SecMPS统一问题清单20260603.md`
|
|
||||||
> **原则**: 按优先级逐项修复,每项修复后编译验证;涉及网关/Vol.Pro 改动的放一组批量提交
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 修复总览
|
|
||||||
|
|
||||||
| 阶段 | 优先级 | 涉及项目 | 文件数 | 预计 |
|
|
||||||
|:---:|:---:|------|:---:|:---:|
|
|
||||||
| F1 | P0-1 ~ P0-3 | gateway + Vol.Pro | 5 | 2h |
|
|
||||||
| F2 | P1-1 ~ P1-6 | gateway + Vol.Pro + 库表 + 前端 | 8 | 4h |
|
|
||||||
| F3 | P2-1 ~ P2-5 | gateway + warehouse | 8 | 2h |
|
|
||||||
| F4 | P3-1 ~ P3-4 | gateway + 文档 | 4 | 1h |
|
|
||||||
| **合计** | — | 4 项目 | **25** | **~9h** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 阶段 F1: P0 阻塞项修复(预计 2h)
|
|
||||||
|
|
||||||
#### F1.1 [P0-1] RealtimePollJob 填充实现
|
|
||||||
|
|
||||||
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/RealtimePollJob.cs`
|
|
||||||
- [ ] 注入 `GatewayClient` + `Ibase_deviceRepository`
|
|
||||||
- [ ] `Execute()` 中:
|
|
||||||
1. 查询在线 MC4 网关 (`gateway_nodes WHERE IsOnline=在线 AND AdapterTypes LIKE '%MC4%'`)
|
|
||||||
2. 查对应设备列表 (`base_device WHERE DeviceGroup='IoT设备' AND IsOnline=在线`)
|
|
||||||
3. 对每个设备调 `GatewayClient.GetRealtimeAsync(gwBaseUrl, adapterCode, sourceId)`
|
|
||||||
4. 结果写入 `iot_devicedata` 表(INSERT 新记录)
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
#### F1.2 [P0-2] 网关 A1 自注册
|
|
||||||
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
|
||||||
- [ ] 在 `registry.InitializeAllAsync()` 后加入:
|
|
||||||
```csharp
|
|
||||||
var nodeCode = gwCfg["NodeCode"] ?? "gw-default";
|
|
||||||
var nodeToken = gwCfg["NodeToken"] ?? "";
|
|
||||||
var adapterTypes = string.Join(",", registry.All.Select(a => a.AdapterCode));
|
|
||||||
await clientFactory.RegisterAsync(nodeCode, nodeToken, adapterTypes, volProUrl);
|
|
||||||
```
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
#### F1.3 [P0-3] B 组路由认证
|
|
||||||
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
|
||||||
- [ ] 在 `app.UseCors()` 之后添加中间件:
|
|
||||||
```csharp
|
|
||||||
var gatewayKey = gwCfg["GatewayKey"];
|
|
||||||
if (!string.IsNullOrEmpty(gatewayKey))
|
|
||||||
{
|
|
||||||
app.Use(async (context, next) => {
|
|
||||||
var key = context.Request.Headers["X-Gateway-Key"].FirstOrDefault();
|
|
||||||
if (key == gatewayKey || context.Request.Path == "/") { await next(); }
|
|
||||||
else { context.Response.StatusCode = 401; }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- [ ] appsettings.json Gateway 段新增 `"GatewayKey": null`
|
|
||||||
- [ ] Vol.Pro 端 `GatewayClient` 所有 HTTP 请求头自动附加 `X-Gateway-Key`
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
> **F1 提交点**: `Fix-P0: RealtimePollJob+A1自注册+B组认证`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 阶段 F2: P1 重要项修复(预计 4h)
|
|
||||||
|
|
||||||
#### F2.1 [P1-1] 网关新增批量实时值接口
|
|
||||||
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
|
||||||
- [ ] 新增 B4-batch 路由:
|
|
||||||
```csharp
|
|
||||||
app.MapPost("/api/gateway/realtime/{adapter}/batch", async (string adapter, BatchRealtimeRequest req) =>
|
|
||||||
{
|
|
||||||
var a = registry.FindByCode<IHasPoints>(adapter);
|
|
||||||
if (a == null) return Results.NotFound();
|
|
||||||
var results = new Dictionary<string, List<PointValue>>();
|
|
||||||
foreach (var deviceId in req.DeviceIds ?? new())
|
|
||||||
results[deviceId] = await a.GetRealtimeValuesAsync(deviceId);
|
|
||||||
return Results.Ok(results);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
- [ ] 新增 `record BatchRealtimeRequest(List<string>? DeviceIds);`
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
#### F2.2 [P1-2] 批量级联离线标记
|
|
||||||
|
|
||||||
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/HeartbeatMonitorJob.cs`
|
|
||||||
- [ ] 替换逐条 `UpdateAsync` 为:
|
|
||||||
```csharp
|
|
||||||
context.Repository.DbContext.Db.Ado.ExecuteCommand(
|
|
||||||
"UPDATE base_device SET IsOnline='离线' WHERE GatewayNodeId=@id AND IsOnline='在线'",
|
|
||||||
new { id = node.GatewayNodeId });
|
|
||||||
```
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
#### F2.3 [P1-3] 凭据安全化
|
|
||||||
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/appsettings.json`
|
|
||||||
- `NodeToken` → `null`, 加注释 "生产环境由 SECMPS_GATEWAY_TOKEN 环境变量注入"
|
|
||||||
- Owl `Password` → `""`, 加注释
|
|
||||||
- KMS `ClientSecret` → `""`, 加注释
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
|
||||||
- `gwCfg["NodeToken"]` → `Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"]`
|
|
||||||
- [ ] 编辑 `.gitignore` → 加 `**/bin/`、`**/obj/`
|
|
||||||
- [ ] `git rm -r --cached gateway/src/IntegrationGateway.Host/bin/`
|
|
||||||
|
|
||||||
#### F2.4 [P1-4] 前端网关地址统一化
|
|
||||||
|
|
||||||
- [ ] 编辑 `web.vite/public/index.html` 的 `window.apiConfig` → 加 `gatewayUrl: 'http://localhost:5100'`
|
|
||||||
- [ ] 编辑 `web.vite/src/views/warehouse/device_manager/base_device.vue`
|
|
||||||
- `const GW = 'http://localhost:5100'` → `const GW = window.apiConfig.gatewayUrl || 'http://localhost:5100'`
|
|
||||||
- [ ] 编辑 `warehouse/src/api/gateway.ts`
|
|
||||||
- `const GW_BASE = 'http://localhost:5100'` → 读取 `window.apiConfig.gatewayUrl`
|
|
||||||
- [ ] 编辑 `warehouse/index.html` 的 `window.apiConfig` → 加 `gatewayUrl`
|
|
||||||
|
|
||||||
#### F2.5 [P1-5] 规则引擎增加 DeviceId 映射
|
|
||||||
|
|
||||||
- [ ] 在规则引擎实现方案中增加 `BuildDeviceMappingAsync` 方法:
|
|
||||||
```csharp
|
|
||||||
var deviceIds = rules.SelectMany(r => r.Conditions).Select(c => c.DeviceId).Distinct();
|
|
||||||
var devices = await _deviceRepo.FindAsync(d => deviceIds.Contains(d.DeviceId));
|
|
||||||
var map = devices.ToDictionary(d => d.DeviceId, d => (d.AdapterCode, d.SourceId));
|
|
||||||
```
|
|
||||||
- [ ] 后续调网关时用 `map[cond.DeviceId]` 拼装 URL
|
|
||||||
|
|
||||||
#### F2.6 [P1-6] 新建 warehouse_variable 表
|
|
||||||
|
|
||||||
- [ ] 执行 SQL:
|
|
||||||
```sql
|
|
||||||
CREATE TABLE warehouse_variable (
|
|
||||||
VariableId INT IDENTITY PRIMARY KEY,
|
|
||||||
DeviceId INT NOT NULL,
|
|
||||||
VariableName NVARCHAR(255), -- 温度/湿度/人数
|
|
||||||
PointIndex INT DEFAULT 0, -- MC4 pointIndex
|
|
||||||
Unit NVARCHAR(50), -- ℃/%/人
|
|
||||||
SortOrder INT DEFAULT 0
|
|
||||||
);
|
|
||||||
```
|
|
||||||
- [ ] 在 Vol.Pro 代码生成器选择 `warehouse_variable`,生成全套 CRUD 代码
|
|
||||||
- [ ] 管理端字典 "变量列表" 绑定到 `warehouse_variable.VariableName`
|
|
||||||
- [ ] 规则条件/动作的 `ValueId` 下拉框改为从 `warehouse_variable` 查询(JOIN `base_device.DeviceId`)
|
|
||||||
|
|
||||||
> **F2 提交点**: `Fix-P1: B4-batch+批量离线+凭据安全+前端地址+DeviceId映射+变量表`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 阶段 F3: P2 改善项修复(预计 2h)
|
|
||||||
|
|
||||||
#### F3.1 [P2-1] 适配器异常日志
|
|
||||||
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs`
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs`
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.Kms/KmsAdapter.cs`
|
|
||||||
- [ ] 所有 `catch { return false; }` → `catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck: {ex.Message}"); return false; }`
|
|
||||||
- [ ] `dotnet build` → 0 错误
|
|
||||||
|
|
||||||
#### F3.2 [P2-2] 规则引擎滞后窗
|
|
||||||
|
|
||||||
- [ ] 在 `warehouse_rulecondition` 表新增字段:
|
|
||||||
```sql
|
|
||||||
ALTER TABLE warehouse_rulecondition ADD
|
|
||||||
RecoveryThreshold_Numeric DECIMAL(18,2) NULL, -- 恢复阈值(下界)
|
|
||||||
RecoveryThreshold_Switch NVARCHAR(50) NULL; -- 恢复开关状态
|
|
||||||
```
|
|
||||||
- [ ] `RuleEngineService.EvaluateCondition` 中加逻辑:
|
|
||||||
```csharp
|
|
||||||
bool wasTriggered = cond.LastTriggered.HasValue;
|
|
||||||
if (wasTriggered)
|
|
||||||
return Compare(actualValue, "大于等于", cond.RecoveryThreshold_Numeric);
|
|
||||||
else
|
|
||||||
return Compare(actualValue, cond.CompareOperator, cond.TargetValue_Number);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### F3.3 [P2-3] 条件级冷却
|
|
||||||
|
|
||||||
- [ ] `warehouse_rulecondition` 表新增 `LastTriggered DATETIME NULL`、`LastTriggerValue DECIMAL(18,2) NULL`
|
|
||||||
- [ ] `RuleEngineService.EvaluateCondition` 中:
|
|
||||||
- 如果 `DateTime.Now - cond.LastTriggered < rule.CooldownSec` → 跳过此条件
|
|
||||||
- 触发时更新 `LastTriggered` 和 `LastTriggerValue`
|
|
||||||
|
|
||||||
#### F3.4 [P2-4] 生产环境移除 console.log
|
|
||||||
|
|
||||||
- [ ] 编辑 `warehouse/vite.config.ts`
|
|
||||||
```typescript
|
|
||||||
build: {
|
|
||||||
terserOptions: { compress: { drop_console: true } }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- [ ] 开发环境保留 `console.log`(仅 build 时移除)
|
|
||||||
- [ ] `npm run build` → 确认无 console.log 残留
|
|
||||||
|
|
||||||
#### F3.5 [P2-5] 统一 gateway API 封装
|
|
||||||
|
|
||||||
- [ ] 复制 `warehouse/src/api/gateway.ts` → `web.vite/src/api/gateway.js`
|
|
||||||
- 修改 `GW_BASE` 为 `window.apiConfig.gatewayUrl || 'http://localhost:5100'`
|
|
||||||
- [ ] `web.vite/src/views/warehouse/device_manager/base_device.vue`
|
|
||||||
- 删除内联 `const GW =` + `fetch()` → 改为 `import { gwGet, gwPost } from '@/api/gateway.js'`
|
|
||||||
- 所有 `fetch(\`\${GW}/api/gateway/...\`)` → `gwGet(...)` / `gwPost(...)`
|
|
||||||
|
|
||||||
> **F3 提交点**: `Fix-P2: 异常日志+滞后窗+条件冷却+console清理+API统一`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 阶段 F4: P3 优化项(预计 1h)
|
|
||||||
|
|
||||||
#### F4.1 [P3-1] 规则引擎并发动作执行
|
|
||||||
|
|
||||||
- [ ] 在规则引擎实现方案的 `ExecuteActionsAsync` 中:
|
|
||||||
```csharp
|
|
||||||
var tasks = actions.Select(a => ExecuteSingleActionAsync(a, rule));
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
|
|
||||||
async Task ExecuteSingleActionAsync(Action a, Rule r) {
|
|
||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
try { await DoAction(a, r, cts.Token); }
|
|
||||||
catch (OperationCanceledException) { Log($"[RuleEngine] 动作超时: {a.id}"); }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### F4.2 [P3-2] 清理 bin/obj + .gitignore
|
|
||||||
|
|
||||||
- [ ] `.gitignore` 追加规则(如未在 F2.3 中完成):
|
|
||||||
```
|
|
||||||
**/bin/
|
|
||||||
**/obj/
|
|
||||||
gateway/src/IntegrationGateway.Host/bin/
|
|
||||||
api_sqlsugar/**/bin/
|
|
||||||
```
|
|
||||||
- [ ] `git rm -r --cached` 所有 bin/obj 目录
|
|
||||||
|
|
||||||
#### F4.3 [P3-3] 网关 Swagger
|
|
||||||
|
|
||||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`:
|
|
||||||
```csharp
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen();
|
|
||||||
// ...
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
```
|
|
||||||
- [ ] 浏览器访问 `http://localhost:5100/swagger` 验证
|
|
||||||
|
|
||||||
#### F4.4 [P3-4] 同步设计文档路由数
|
|
||||||
|
|
||||||
- [ ] 编辑 `doc/设计文档/对接网关设计文档.md` → 路由表从 14 条更新为当前实际数
|
|
||||||
- [ ] 确认以下设计文档一致: 对接网关设计文档、规则引擎方案、KMS 设计文档
|
|
||||||
|
|
||||||
> **F4 提交点**: `Fix-P3: 并发动作+清理bin+Swagger+文档同步`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 任务总览
|
|
||||||
|
|
||||||
| 编号 | 问题 | 涉及文件 | 预计 |
|
|
||||||
|:---:|------|------|:---:|
|
|
||||||
| P0-1 | RealtimePollJob 空壳 | RealtimePollJob.cs | 1h |
|
|
||||||
| P0-2 | A1 自注册 | Program.cs | 30min |
|
|
||||||
| P0-3 | B 组认证 | Program.cs + appsettings | 30min |
|
|
||||||
| P1-1 | B4-batch | Program.cs | 30min |
|
|
||||||
| P1-2 | 批量离线 | HeartbeatMonitorJob.cs | 20min |
|
|
||||||
| P1-3 | 凭据安全 | appsettings + .gitignore + bin | 20min |
|
|
||||||
| P1-4 | 前端地址 | base_device.vue + gateway.ts | 20min |
|
|
||||||
| P1-5 | DeviceId 映射 | RuleEngineService | 30min |
|
|
||||||
| P1-6 | 变量表 | SQL + 代码生成 + 前端 | 1h |
|
|
||||||
| P2-1 | 异常日志 | OwlAdapter + MC4Adapter + KmsAdapter | 20min |
|
|
||||||
| P2-2 | 滞后窗 | SQL + RuleEngineService | 30min |
|
|
||||||
| P2-3 | 条件冷却 | SQL + RuleEngineService | 20min |
|
|
||||||
| P2-4 | console 清理 | vite.config.ts | 10min |
|
|
||||||
| P2-5 | API 统一 | gateway.js + base_device.vue | 30min |
|
|
||||||
| P3-1 | 并发动作 | RuleEngineService | 15min |
|
|
||||||
| P3-2 | bin 清理 | .gitignore + git rm | 5min |
|
|
||||||
| P3-3 | Swagger | Program.cs | 10min |
|
|
||||||
| P3-4 | 文档同步 | 设计文档 | 15min |
|
|
||||||
|
|
||||||
> **总计**: 18 项 / 25 文件 / ~9h
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
# VolPro.WebApi 网关相关接口文档
|
|
||||||
|
|
||||||
> **版本**: 1.0
|
|
||||||
> **日期**: 2026-06-04
|
|
||||||
> **基址**: `http://{host}:{port}`(默认 `http://localhost:9100`)
|
|
||||||
> **内容类型**: `application/json`
|
|
||||||
> **接口来源**: `api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
1. [A 组 — 网关注册通信](#1-a-组--网关注册通信)
|
|
||||||
- A1: 网关注册 `POST /api/gateway/register`
|
|
||||||
- A2: 心跳上报 `POST /api/gateway/heartbeat`
|
|
||||||
- A3: 设备同步 `POST /api/gateway/sync/devices`
|
|
||||||
- A4: 告警同步 `POST /api/gateway/sync/alarms`
|
|
||||||
2. [设备管理](#2-设备管理)
|
|
||||||
- 区域树 `GET /api/DeviceManager/GetRegionTree`
|
|
||||||
- 点位设备 `GET /api/DeviceManager/GetDevicesByPoint`
|
|
||||||
3. [定时任务](#3-定时任务)
|
|
||||||
- 设备同步任务 `POST /api/task/syncDevices`
|
|
||||||
- 心跳监控任务 `POST /api/task/heartbeatMonitor`
|
|
||||||
- 实时轮询任务 `POST /api/task/realtimePoll`
|
|
||||||
- 规则引擎任务 `POST /api/task/ruleEngine`
|
|
||||||
4. [错误代码](#4-错误代码)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. A 组 — 网关注册通信
|
|
||||||
|
|
||||||
> A 组接口是 VolPro 向网关暴露的管理端点,由网关主动调用。所有 A 组使用 `[AllowAnonymous]` + `NodeToken` 二次认证,不走 VolPro JWT 体系。
|
|
||||||
>
|
|
||||||
> **实现文件**: `Controllers/Warehouse/Partial/gateway_nodesController.cs`
|
|
||||||
|
|
||||||
### A1: 网关注册(Upsert)
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/register
|
|
||||||
```
|
|
||||||
|
|
||||||
网关启动时调用,注册自身节点信息。NodeCode 已存在则更新,不存在则插入。返回当前网关的已有设备列表供网关对比差异。
|
|
||||||
|
|
||||||
**请求头**: `Content-Type: application/json`
|
|
||||||
|
|
||||||
**请求体 (GatewayRegisterRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `NodeCode` | string | ✅ | 网关节点编码,如 `gw-31ku` |
|
|
||||||
| `Token` | string | ✅ | 认证令牌(由环境变量 `SECMPS_GATEWAY_TOKEN` 注入) |
|
|
||||||
| `AdapterTypes` | string | ✅ | 适配器类型列表(逗号分隔),如 `Owl:main,MC4:31ku` |
|
|
||||||
| `BaseUrl` | string | ✅ | 网关自身地址,如 `http://192.168.1.10:5100` |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `nodeId` | int | 网关节点 ID(base_device.NodeId 外键) |
|
|
||||||
| `devices` | array | 该网关已有的设备列表 |
|
|
||||||
|
|
||||||
**devices[] 条目**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `deviceId` | int | 设备自增 ID |
|
|
||||||
| `deviceName` | string | 设备名称 |
|
|
||||||
| `adapterCode` | string | 适配器编码 |
|
|
||||||
| `sourceId` | string | 子系统设备原始 ID |
|
|
||||||
| `deviceCategory` | string | 设备种类 |
|
|
||||||
| `deviceGroup` | string | 设备分组 |
|
|
||||||
| `isParent` | string | 是否父设备("是"/"否") |
|
|
||||||
| `isOnline` | string | 是否在线("在线"/"离线") |
|
|
||||||
| `extraData` | string? | 扩展数据 JSON |
|
|
||||||
|
|
||||||
**返回示例**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"nodeId": 1,
|
|
||||||
"devices": [
|
|
||||||
{ "deviceId": 10, "deviceName": "NVR-1", "adapterCode": "Owl:main", "sourceId": "nvr_001", "deviceCategory": "硬盘录像机", "deviceGroup": "视频设备", "isParent": "是", "isOnline": "在线" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**错误响应**:
|
|
||||||
|
|
||||||
| HTTP | 说明 |
|
|
||||||
|:---:|------|
|
|
||||||
| 400 | `NodeCode` 或 `Token` 为空 |
|
|
||||||
| 401 | `NodeToken` 不匹配(已有节点 Token 变更) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### A2: 心跳上报
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/heartbeat
|
|
||||||
```
|
|
||||||
|
|
||||||
网关每 15s 调用一次,更新 `LastHeartbeat` 字段。连续失败 ≥3 次(45s)后网关自动触发 A1+A3 重注册。
|
|
||||||
|
|
||||||
**请求体 (GatewayHeartbeatRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `NodeCode` | string | ✅ | 网关节点编码 |
|
|
||||||
| `Token` | string | ✅ | 认证令牌 |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `status` | string | 固定 `"ok"` |
|
|
||||||
| `serverTime` | string | 服务器时间 (`yyyy-MM-dd HH:mm:ss`) |
|
|
||||||
|
|
||||||
**错误响应**:
|
|
||||||
|
|
||||||
| HTTP | 说明 |
|
|
||||||
|:---:|------|
|
|
||||||
| 400 | `NodeCode` 或 `Token` 为空 |
|
|
||||||
| 401 | NodeCode+Token 组合不匹配 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### A3: 设备数据同步
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/sync/devices
|
|
||||||
```
|
|
||||||
|
|
||||||
网关每次设备变更后调用,将全量设备列表推送到 VolPro。采用字段分治策略:首次入库写全量,后续只更新网关字段。
|
|
||||||
|
|
||||||
**请求体 (SyncDevicesRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `NodeCode` | string | ✅ | 网关节点编码 |
|
|
||||||
| `Token` | string | ✅ | 认证令牌 |
|
|
||||||
| `Devices` | array | ✅ | 设备列表 |
|
|
||||||
|
|
||||||
**Devices[].SyncDeviceItemDto**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `AdapterCode` | string | ✅ | 适配器编码 |
|
|
||||||
| `SourceId` | string | ✅ | 子系统设备原始 ID |
|
|
||||||
| `Name` | string? | ❌ | 设备名称 |
|
|
||||||
| `Category` | string? | ❌ | 设备种类 |
|
|
||||||
| `Group` | string? | ❌ | 设备分组 |
|
|
||||||
| `IsParent` | bool | ❌ | 是否父设备 |
|
|
||||||
| `ParentSourceId` | string? | ❌ | 父设备 SourceId(用于解析 ParentDeviceId) |
|
|
||||||
| `IsOnline` | bool | ❌ | 是否在线 |
|
|
||||||
| `IpAddress` | string? | ❌ | IP 地址 |
|
|
||||||
| `Port` | int? | ❌ | 端口号 |
|
|
||||||
| `ExtraDataJson` | string? | ❌ | 扩展数据 JSON |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `added` | int | 新增设备数 |
|
|
||||||
| `updated` | int | 更新设备数 |
|
|
||||||
| `removed` | int | 固定 `0`(当前版本不移除下线设备) |
|
|
||||||
|
|
||||||
**错误响应**:
|
|
||||||
|
|
||||||
| HTTP | 说明 |
|
|
||||||
|:---:|------|
|
|
||||||
| 400 | `NodeCode` 或 `Token` 为空 |
|
|
||||||
| 401 | NodeCode+Token 认证失败 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### A4: 告警数据同步
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/sync/alarms
|
|
||||||
```
|
|
||||||
|
|
||||||
网关检测到新告警后调用,推送告警列表到 VolPro。通过 `SourceAlarmId` 去重(同一告警不重复入库)。
|
|
||||||
|
|
||||||
**请求体 (SyncAlarmsRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `NodeCode` | string | ✅ | 网关节点编码 |
|
|
||||||
| `Token` | string | ✅ | 认证令牌 |
|
|
||||||
| `Alarms` | array | ✅ | 告警列表 |
|
|
||||||
|
|
||||||
**Alarms[].SyncAlarmItemDto**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `SourceAlarmId` | string | ✅ | 子系统告警唯一 ID(用于去重) |
|
|
||||||
| `DeviceSourceId` | string | ✅ | 关联设备 SourceId(用于映射 DeviceId) |
|
|
||||||
| `AdapterCode` | string | ✅ | 适配器编码 |
|
|
||||||
| `Level` | string | ✅ | 告警等级:`提示`/`普通`/`重要`/`紧急` |
|
|
||||||
| `Desc` | string | ✅ | 告警描述 |
|
|
||||||
| `Value` | double? | ❌ | 告警实际值 |
|
|
||||||
| `StartTime` | string | ✅ | 告警发生时间 |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `added` | int | 新增告警数 |
|
|
||||||
|
|
||||||
**错误响应**:
|
|
||||||
|
|
||||||
| HTTP | 说明 |
|
|
||||||
|:---:|------|
|
|
||||||
| 400 | `NodeCode` 或 `Token` 为空 |
|
|
||||||
| 401 | NodeCode+Token 认证失败 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 设备管理
|
|
||||||
|
|
||||||
> **实现文件**: `Controllers/Warehouse/Partial/base_deviceController.cs`
|
|
||||||
|
|
||||||
### 区域树
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/DeviceManager/GetRegionTree
|
|
||||||
```
|
|
||||||
|
|
||||||
返回 区域→点位 的层级结构,供管理端左侧树形控件使用。
|
|
||||||
|
|
||||||
**请求参数**: 无
|
|
||||||
|
|
||||||
**返回参数**: `TreeNode[]`
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `id` | string | 节点 ID(`r_{regionId}` 或 `p_{pointId}`) |
|
|
||||||
| `label` | string | 节点显示名称 |
|
|
||||||
| `type` | string | 节点类型:`region`(区域) 或 `point`(点位) |
|
|
||||||
| `deviceCount` | int | 该节点下的设备数量 |
|
|
||||||
| `children` | array? | 子节点列表(仅 region 节点有) |
|
|
||||||
|
|
||||||
**返回示例**:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "r_1", "label": "库房A区", "type": "region", "deviceCount": 3,
|
|
||||||
"children": [
|
|
||||||
{ "id": "p_10", "label": "温湿度监测点1", "type": "point", "deviceCount": 5 },
|
|
||||||
{ "id": "p_11", "label": "门禁点1", "type": "point", "deviceCount": 2 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 点位设备列表
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/DeviceManager/GetDevicesByPoint?pointId={pointId}&page={page}&size={size}
|
|
||||||
```
|
|
||||||
|
|
||||||
获取指定点位下的设备列表(含子设备),支持分页。
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
||||||
|------|------|:--:|------|------|
|
|
||||||
| `pointId` | int | ✅ | — | 点位 ID |
|
|
||||||
| `page` | int | ❌ | 1 | 页码 |
|
|
||||||
| `size` | int | ❌ | 20 | 每页条数 |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `items` | array | 设备列表 |
|
|
||||||
| `total` | int | 总设备数 |
|
|
||||||
|
|
||||||
**items[] 条目**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `deviceId` | int | 设备自增 ID |
|
|
||||||
| `deviceName` | string | 设备名称 |
|
|
||||||
| `adapterCode` | string | 适配器编码 |
|
|
||||||
| `sourceId` | string | 子系统设备原始 ID |
|
|
||||||
| `deviceCategory` | string | 设备种类 |
|
|
||||||
| `deviceGroup` | string | 设备分组(`视频设备`/`IoT设备`/`门禁设备`) |
|
|
||||||
| `isParent` | string | 是否父设备("是"/"否") |
|
|
||||||
| `parentDeviceId` | int? | 父设备 ID |
|
|
||||||
| `isOnline` | string | 是否在线("在线"/"离线") |
|
|
||||||
| `ipAddress` | string? | IP 地址 |
|
|
||||||
| `port` | int? | 端口号 |
|
|
||||||
| `location` | string? | 位置描述 |
|
|
||||||
| `extraData` | string? | 扩展数据 JSON |
|
|
||||||
| `lastSyncTime` | DateTime? | 最后同步时间 |
|
|
||||||
| `mapModelId` | string? | 3D 地图模型 ID |
|
|
||||||
| `mapModelScale` | decimal? | 模型缩放比例 |
|
|
||||||
| `mapModelRotation` | string? | 模型旋转参数 JSON |
|
|
||||||
| `enable` | string | 启用状态("启用"/"停用") |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 定时任务
|
|
||||||
|
|
||||||
> VolPro 框架通过 `Sys_QuartzOptions` 表配置 URL+Cron 定时调用。每个端点加 `[ApiTask]` 属性以允许框架匿名调用。
|
|
||||||
>
|
|
||||||
> **实现文件**: `Controllers/Warehouse/TaskController.cs`
|
|
||||||
|
|
||||||
### 设备同步任务
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/task/syncDevices
|
|
||||||
```
|
|
||||||
|
|
||||||
遍历所有在线网关,触发全量设备同步至 VolPro。
|
|
||||||
|
|
||||||
**Cron**: `0 */5 * * * ?`(每 5 分钟)
|
|
||||||
|
|
||||||
**请求参数**: 无
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `time` | DateTime | 执行时间 |
|
|
||||||
| `status` | string | 固定 `"ok"` |
|
|
||||||
|
|
||||||
**错误响应**:
|
|
||||||
|
|
||||||
| HTTP | 说明 |
|
|
||||||
|:---:|------|
|
|
||||||
| 500 | `gateway_nodesService` 未注册 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 心跳监控任务
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/task/heartbeatMonitor
|
|
||||||
```
|
|
||||||
|
|
||||||
扫描心跳超时 ≥30s 的网关节点,标记离线并级联标记该节点下所有设备离线。
|
|
||||||
|
|
||||||
**Cron**: `0/15 * * * * ?`(每 15 秒)
|
|
||||||
|
|
||||||
**请求参数**: 无
|
|
||||||
|
|
||||||
**返回**: 同设备同步任务
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 实时轮询任务
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/task/realtimePoll
|
|
||||||
```
|
|
||||||
|
|
||||||
轮询所有在线 MC4 IoT 设备的实时值,写入 `iot_devicedata` 表。
|
|
||||||
|
|
||||||
**Cron**: `0/10 * * * * ?`(每 10 秒)
|
|
||||||
|
|
||||||
**请求参数**: 无
|
|
||||||
|
|
||||||
**返回**: 同设备同步任务
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 规则引擎任务
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/task/ruleEngine
|
|
||||||
```
|
|
||||||
|
|
||||||
加载启用规则 → 从网关批量获取实时值 → 逐规则评估条件 → 触发动作(控制/告警/通知)。
|
|
||||||
|
|
||||||
**Cron**: `0/10 * * * * ?`(每 10 秒)
|
|
||||||
|
|
||||||
**请求参数**: 无
|
|
||||||
|
|
||||||
**返回**: 同设备同步任务
|
|
||||||
|
|
||||||
**当前状态**: 桩实现。`RuleEngineService.EvaluateAllAsync()` 抛出 `NotImplementedException`,需先执行 SQL ALTER TABLE + 代码生成器。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 错误代码
|
|
||||||
|
|
||||||
### HTTP 状态码
|
|
||||||
|
|
||||||
| 状态码 | 含义 | 触发条件 |
|
|
||||||
|:---:|------|------|
|
|
||||||
| 200 | OK | 请求成功 |
|
|
||||||
| 400 | Bad Request | 必填参数缺失(`NodeCode`/`Token`/`pointId`) |
|
|
||||||
| 401 | Unauthorized | A 组接口 NodeToken 认证失败 |
|
|
||||||
| 500 | Internal Server Error | 服务未注册或内部异常 |
|
|
||||||
|
|
||||||
### A 组认证错误
|
|
||||||
|
|
||||||
所有 A 组接口在认证失败时返回:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "message": "认证失败:Token 无效" }
|
|
||||||
```
|
|
||||||
|
|
||||||
或
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "message": "认证失败" }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 定时任务错误
|
|
||||||
|
|
||||||
定时任务在服务未注册时返回:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "error": "服务未注册: gateway_nodesService" }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **接口总数**: 10 个端点(A 组 4 + 设备管理 2 + 定时任务 4)
|
|
||||||
> **实现位置**: `api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/`
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
# 定时任务 API 化整改方案 v1.0
|
|
||||||
|
|
||||||
> **版本**: 1.0
|
|
||||||
> **日期**: 2026-06-04
|
|
||||||
> **背景**: VolPro 框架的 Quartz 机制基于 `[ApiTask]` + URL 调用,不支持 `IJob` 接口
|
|
||||||
> **现状**: 4 个 IJob 实现(SyncDevices/HeartbeatMonitor/RealtimePoll/RuleEngineJob)需迁移为 API 端点
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 影响范围
|
|
||||||
|
|
||||||
| 任务 | 当前文件 | 需改为 | 调度间隔 |
|
|
||||||
|------|------|------|:---:|
|
|
||||||
| 设备同步 | `SyncDevicesJob.cs` (IJob) | Controller + `[ApiTask]` | 每5分钟 |
|
|
||||||
| 心跳监控 | `HeartbeatMonitorJob.cs` (IJob) | Controller + `[ApiTask]` | 每15秒 |
|
|
||||||
| 实时轮询 | `RealtimePollJob.cs` (IJob) | Controller + `[ApiTask]` | 每10秒 |
|
|
||||||
| 规则引擎 | `RuleEngineJob.cs` (IJob) | Controller + `[ApiTask]` | 每10秒 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 整改步骤
|
|
||||||
|
|
||||||
### 步骤 T1: 创建任务调度 Controller(预计 30min)
|
|
||||||
|
|
||||||
**新建文件**: `api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/TaskController.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using VolPro.Core.Filters;
|
|
||||||
using Warehouse.Services;
|
|
||||||
|
|
||||||
namespace Warehouse.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定时任务 API 端点。
|
|
||||||
/// VolPro 框架通过 Sys_QuartzOptions 配置 URL+Cron 定时调用。
|
|
||||||
/// 每个方法加 [ApiTask] 属性以允许框架匿名调用。
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/task")]
|
|
||||||
public class TaskController : Controller
|
|
||||||
{
|
|
||||||
/// <summary>T1: 设备同步 — 遍历在线网关触发全量设备同步</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("syncDevices")]
|
|
||||||
public async Task<IActionResult> SyncDevices()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
var engine = sp.GetService<SyncDevicesJob>();
|
|
||||||
if (engine != null) await engine.Execute(null!);
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>T2: 心跳监控 — 扫描超时网关标记离线</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("heartbeatMonitor")]
|
|
||||||
public async Task<IActionResult> HeartbeatMonitor()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
var engine = sp.GetService<HeartbeatMonitorJob>();
|
|
||||||
if (engine != null) await engine.Execute(null!);
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>T3: 实时轮询 — 拉取 MC4 IoT 实时值</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("realtimePoll")]
|
|
||||||
public async Task<IActionResult> RealtimePoll()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
var engine = sp.GetService<RealtimePollJob>();
|
|
||||||
if (engine != null) await engine.Execute(null!);
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>T4: 规则引擎 — 评估规则+执行动作</summary>
|
|
||||||
[ApiTask]
|
|
||||||
[HttpGet, HttpPost, Route("ruleEngine")]
|
|
||||||
public async Task<IActionResult> RuleEngine()
|
|
||||||
{
|
|
||||||
var sp = HttpContext.RequestServices;
|
|
||||||
var engine = sp.GetService<RuleEngineService>();
|
|
||||||
if (engine != null) await engine.EvaluateAllAsync();
|
|
||||||
return Ok(new { time = DateTime.Now, status = "ok" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤 T2: 注册 DI(预计 10min)
|
|
||||||
|
|
||||||
**编辑文件**: `api_sqlsugar/VolPro.Core/Extensions/AutofacManager/AutofacContainerModuleExtension.cs`
|
|
||||||
|
|
||||||
或在 Warehouse 项目的 Startup/Module 中注册:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 在 Autofac 注册块中添加
|
|
||||||
builder.RegisterType<SyncDevicesJob>().AsSelf().InstancePerLifetimeScope();
|
|
||||||
builder.RegisterType<HeartbeatMonitorJob>().AsSelf().InstancePerLifetimeScope();
|
|
||||||
builder.RegisterType<RealtimePollJob>().AsSelf().InstancePerLifetimeScope();
|
|
||||||
builder.RegisterType<RuleEngineService>().AsSelf().InstancePerLifetimeScope();
|
|
||||||
```
|
|
||||||
|
|
||||||
如果已由 VolPro 框架自动扫描 Services 目录,则跳过此步骤。
|
|
||||||
|
|
||||||
### 步骤 T3: 管理端配置任务(预计 15min)
|
|
||||||
|
|
||||||
在 Vol.Pro 管理端 → Quartz 管理 → 新建 4 个任务:
|
|
||||||
|
|
||||||
| TaskName | ApiUrl | Cron | Method |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 设备同步 | `/api/task/syncDevices` | `0 */5 * * * ?` | POST |
|
|
||||||
| 心跳监控 | `/api/task/heartbeatMonitor` | `0/15 * * * * ?` | POST |
|
|
||||||
| 实时轮询 | `/api/task/realtimePoll` | `0/10 * * * * ?` | POST |
|
|
||||||
| 规则引擎 | `/api/task/ruleEngine` | `0/10 * * * * ?` | POST |
|
|
||||||
|
|
||||||
### 步骤 T4: 保留或删除 IJob 文件(预计 5min)
|
|
||||||
|
|
||||||
**保留** IJob 实现类(`SyncDevicesJob.cs` 等)不删除——Controller 通过 DI 获取它们并调用 `Execute()`。
|
|
||||||
|
|
||||||
只需将 IJob 实现类用 `IServiceProvider` 获取(而非 Quartz 的 `JobDataMap`),因为 Controller 不传 `IJobExecutionContext`。修改 `Execute` 方法签名:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 旧: 依赖 IJobExecutionContext.JobDataMap
|
|
||||||
public async Task Execute(IJobExecutionContext context)
|
|
||||||
{
|
|
||||||
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新: 注入 IServiceProvider 为构造函数参数
|
|
||||||
public class HeartbeatMonitorJob : IJob
|
|
||||||
{
|
|
||||||
private readonly IServiceProvider _sp;
|
|
||||||
public HeartbeatMonitorJob(IServiceProvider sp) { _sp = sp; }
|
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext? context)
|
|
||||||
{
|
|
||||||
var gwSvc = _sp.GetService<Igateway_nodesService>();
|
|
||||||
var devRepo = _sp.GetService<Ibase_deviceRepository>();
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤 T5: 编译验证(预计 10min)
|
|
||||||
|
|
||||||
- [ ] `dotnet build api_sqlsugar/VolPro.WebApi` → 0 错误
|
|
||||||
- [ ] 确认 `[ApiTask]` 不与其他权限 Filter 冲突
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 改动文件汇总
|
|
||||||
|
|
||||||
| 步骤 | 文件 | 改动 |
|
|
||||||
|:---:|------|------|
|
|
||||||
| T1 | `VolPro.WebApi/Controllers/Warehouse/TaskController.cs` | 新建,4 个 `[ApiTask]` 端点 |
|
|
||||||
| T2 | DI 注册 | 可能不需改动(VolPro 自动扫描) |
|
|
||||||
| T3 | 管理端 Sys_QuartzOptions | 新建 4 条任务记录 |
|
|
||||||
| T4 | 4 个 IJob 实现 | 构造函数改用 IServiceProvider 注入 |
|
|
||||||
| T5 | 全量编译 | 0 错误 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 原 IJob 文件处理方案
|
|
||||||
|
|
||||||
| 文件 | 处理 |
|
|
||||||
|------|------|
|
|
||||||
| `SyncDevicesJob.cs` | 构造函数注入 IServiceProvider,Execute 参数改为 nullable |
|
|
||||||
| `HeartbeatMonitorJob.cs` | 同上 |
|
|
||||||
| `RealtimePollJob.cs` | 同上 |
|
|
||||||
| `RuleEngineJob.cs` | 删除(RuleEngineService 本身就是普通类,不继承 IJob) |
|
|
||||||
|
|
||||||
> `RuleEngineJob.cs` 可直接删除——`RuleEngineService` 是普通类,已被 TaskController 直接调用。
|
|
||||||
@@ -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 需要验证
|
|
||||||
@@ -1,550 +0,0 @@
|
|||||||
# IntegrationGateway B 组接口文档
|
|
||||||
|
|
||||||
> **版本**: 1.0
|
|
||||||
> **日期**: 2026-06-04
|
|
||||||
> **基址**: `http://{host}:{port}`(默认 `http://localhost:5100`)
|
|
||||||
> **内容类型**: `application/json`(除标注外)
|
|
||||||
> **认证**: 可选 `X-Gateway-Key` 请求头(与 Gateway 段配置一致时生效)
|
|
||||||
> **通用错误码**: 见 §5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
1. [健康检查](#1-健康检查) — B1
|
|
||||||
2. [设备管理](#2-设备管理) — B2, B3, B3-sync
|
|
||||||
3. [视频与流媒体](#3-视频与流媒体) — B6a, B6b, 截图, B7
|
|
||||||
4. [IoT 实时数据](#4-iot-实时数据) — B4, B4-batch, B5
|
|
||||||
5. [告警管理](#5-告警管理) — B8, B9-confirm, B9-end
|
|
||||||
6. [录像查询](#6-录像查询)
|
|
||||||
7. [设备控制 (通用)](#7-设备控制-通用) — B10
|
|
||||||
8. [业务记录查询](#8-业务记录查询) — B11
|
|
||||||
9. [数据同步](#9-数据同步) — B12, B13
|
|
||||||
10. [错误代码](#10-错误代码)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 健康检查
|
|
||||||
|
|
||||||
### B1: 查询所有适配器健康状态
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/health
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**: 无
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `[].adapterCode` | string | 适配器编码,如 `Owl:main`、`MC4:31ku`、`KMS:main` |
|
|
||||||
| `[].displayName` | string | 人类可读的适配器名称 |
|
|
||||||
| `[].healthy` | bool | `true` = 适配器在线,`false` = 离线或不可达 |
|
|
||||||
| `[].capabilities` | object | 适配器能力声明 |
|
|
||||||
|
|
||||||
**capabilities 字段**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `hasFlatDevices` | bool | 是否支持扁平设备列表 |
|
|
||||||
| `hasOwnDeviceTree` | bool | 是否支持层级对象树 |
|
|
||||||
| `hasStreams` | bool | 是否支持视频取流 |
|
|
||||||
| `hasPoints` | bool | 是否支持 IoT 实时点位 |
|
|
||||||
| `hasAlarms` | bool | 是否支持告警查询 |
|
|
||||||
| `hasRecordings` | bool | 是否支持录像查询 |
|
|
||||||
|
|
||||||
**返回示例**:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"adapterCode": "Owl:main",
|
|
||||||
"displayName": "Owl (Owl:main)",
|
|
||||||
"healthy": true,
|
|
||||||
"capabilities": { "hasFlatDevices": true, "hasStreams": true, "hasAlarms": true, "hasRecordings": true }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 设备管理
|
|
||||||
|
|
||||||
### B2: 分页获取扁平设备列表
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/devices?adapter={adapterCode}&page={page}&size={size}&keyword={keyword}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
||||||
|------|------|:--:|------|------|
|
|
||||||
| `adapter` | string | ✅ | — | 适配器编码,如 `Owl:main` |
|
|
||||||
| `page` | int | ❌ | 1 | 页码(从 1 开始) |
|
|
||||||
| `size` | int | ❌ | 20 | 每页条数 |
|
|
||||||
| `keyword` | string | ❌ | null | 设备名称模糊搜索 |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `items` | array | 设备列表 |
|
|
||||||
| `total` | int | 总设备数 |
|
|
||||||
|
|
||||||
**items[].StandardDevice**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `sourceId` | string | 子系统设备原始 ID |
|
|
||||||
| `name` | string | 设备名称 |
|
|
||||||
| `category` | string | 设备种类(如 `硬盘录像机`、`摄像头`、`智能钥匙柜`、`钥匙位`) |
|
|
||||||
| `group` | string | 设备分组(`视频设备`/`IoT设备`/`门禁设备`) |
|
|
||||||
| `isParent` | bool | 是否父设备(含下级子设备) |
|
|
||||||
| `parentSourceId` | string? | 上级设备 SourceId |
|
|
||||||
| `isOnline` | bool | 是否在线 |
|
|
||||||
| `ipAddress` | string? | IP 地址 |
|
|
||||||
| `port` | int? | 端口号 |
|
|
||||||
| `extra` | object? | 子系统特有扩展属性 |
|
|
||||||
|
|
||||||
**返回示例**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{ "sourceId": "locker_25", "name": "10位智能公共钥匙柜", "category": "智能钥匙柜", "group": "门禁设备", "isParent": true, "isOnline": true },
|
|
||||||
{ "sourceId": "lockhole_25_1", "name": "仓库大门钥匙", "category": "钥匙位", "group": "门禁设备", "isParent": false, "isOnline": true, "parentSourceId": "locker_25" }
|
|
||||||
],
|
|
||||||
"total": 11
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### B3: 获取层级对象树
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/tree?adapter={adapterCode}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码,如 `MC4:31ku` |
|
|
||||||
|
|
||||||
**返回参数**: `DeviceTreeNode[]`
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `sourceId` | string | 节点 ID |
|
|
||||||
| `name` | string | 节点名称 |
|
|
||||||
| `tag` | string | 节点标签(如 `区域`/`设备组`/`IoT设备`) |
|
|
||||||
| `type` | int | 节点类型:1=父节点, 0=叶子节点 |
|
|
||||||
| `children` | array | 子节点列表(递归) |
|
|
||||||
| `option` | object? | 扩展配置 |
|
|
||||||
|
|
||||||
### B3-sync: 手动触发设备同步
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/devices/sync?adapter={adapterCode}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `deviceCount` | int | 同步设备数(扁平设备) |
|
|
||||||
| `nodeCount` | int | 同步节点数(对象树) |
|
|
||||||
| `message` | string | 同步结果描述 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 视频与流媒体
|
|
||||||
|
|
||||||
### B6a: 获取实时流地址
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/streams/{adapter}/{deviceId}/live
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码,如 `Owl:main` |
|
|
||||||
| `deviceId` | string | ✅ | 通道 SourceId |
|
|
||||||
|
|
||||||
**返回参数 (StreamUrls)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `wsFlv` | string? | WebSocket-FLV 地址(推荐,低延迟) |
|
|
||||||
| `httpFlv` | string? | HTTP-FLV 地址 |
|
|
||||||
| `hls` | string? | HLS (m3u8) 地址 |
|
|
||||||
| `webrtc` | string? | WebRTC 地址 |
|
|
||||||
| `rtmp` | string? | RTMP 地址 |
|
|
||||||
|
|
||||||
### B6b: 获取录像回放地址
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/streams/{adapter}/{deviceId}/playback?start={start}&end={end}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
| `deviceId` | string | ✅ | 通道 SourceId |
|
|
||||||
| `start` | DateTime | ✅ | 回放起始时间 (ISO 8601) |
|
|
||||||
| `end` | DateTime | ✅ | 回放结束时间 (ISO 8601) |
|
|
||||||
|
|
||||||
**返回**: 同 `StreamUrls`
|
|
||||||
|
|
||||||
### 截图: 获取通道实时截图
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/streams/{adapter}/{deviceId}/snapshot
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
| `deviceId` | string | ✅ | 通道 SourceId |
|
|
||||||
|
|
||||||
**返回**: JPEG 图片 Base64 或 URL
|
|
||||||
|
|
||||||
### B7: 云台控制
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/streams/{adapter}/{deviceId}/ptz
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
| `deviceId` | string | ✅ | 通道 SourceId |
|
|
||||||
|
|
||||||
**请求体 (PtzRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `direction` | string | ❌ | 方向:`up`/`down`/`left`/`right`/`zoom_in`/`zoom_out` |
|
|
||||||
| `action` | string | ✅ | 动作:`continuous`(持续)/`stop`(停止)/`preset`/`patrol` |
|
|
||||||
| `speed` | float | ❌ | 速度 (0.1~1.0),默认 0.5 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. IoT 实时数据
|
|
||||||
|
|
||||||
### B4: 获取设备实时点位值
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/realtime/{adapter}/{deviceId}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码,如 `MC4:31ku` |
|
|
||||||
| `deviceId` | string | ✅ | 设备 SourceId |
|
|
||||||
|
|
||||||
**返回参数**: `PointValue[]`
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `pointIndex` | int | 点位索引 |
|
|
||||||
| `pointName` | string | 点位名称 |
|
|
||||||
| `value` | decimal | 当前值 |
|
|
||||||
| `unit` | string? | 单位(如 `℃`、`%`) |
|
|
||||||
| `updateTime` | DateTime | 更新时间 |
|
|
||||||
| `interval` | int | 采集间隔(秒) |
|
|
||||||
|
|
||||||
### B4-batch: 批量获取实时点位值
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/realtime/{adapter}/batch
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
|
|
||||||
**请求体 (BatchRealtimeRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `deviceIds` | string[] | ✅ | 设备 SourceId 列表 |
|
|
||||||
|
|
||||||
**返回**: `Dictionary<string, PointValue[]>` — 以 deviceId 为键的实时值字典
|
|
||||||
|
|
||||||
### B5: 设备反向控制
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/realtime/{adapter}/control
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
|
|
||||||
**请求体 (ControlRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `deviceId` | string | ✅ | 设备 SourceId |
|
|
||||||
| `pointIndex` | int | ✅ | 目标点位索引 |
|
|
||||||
| `value` | double | ✅ | 目标值(如开关 0/1,温度设定值等) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 告警管理
|
|
||||||
|
|
||||||
### B8: 分页查询告警列表
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/alarms/{adapter}?page={page}&size={size}&from={from}&to={to}&level={level}&state={state}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
||||||
|------|------|:--:|------|------|
|
|
||||||
| `adapter` | string | ✅ | — | 适配器编码 |
|
|
||||||
| `page` | int | ❌ | 1 | 页码 |
|
|
||||||
| `size` | int | ❌ | 20 | 每页条数 |
|
|
||||||
| `from` | DateTime | ❌ | MinValue | 告警起始时间 |
|
|
||||||
| `to` | DateTime | ❌ | MinValue | 告警结束时间 |
|
|
||||||
| `level` | string | ❌ | null | 告警等级过滤 |
|
|
||||||
| `state` | string | ❌ | null | 告警状态过滤:`未确认`/`已确认`/`已结束` |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `items` | array | 告警列表 |
|
|
||||||
| `total` | int | 总告警数 |
|
|
||||||
|
|
||||||
**items[].StandardAlarm**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `alarmId` | string | 告警 ID |
|
|
||||||
| `adapterCode` | string | 适配器编码 |
|
|
||||||
| `deviceId` | string? | 关联设备 ID |
|
|
||||||
| `level` | string | 告警等级:`提示`/`普通`/`重要`/`紧急` |
|
|
||||||
| `title` | string | 告警标题 |
|
|
||||||
| `content` | string? | 告警详细内容 |
|
|
||||||
| `occurTime` | DateTime | 发生时间 |
|
|
||||||
| `status` | string | 状态:`未确认`/`已确认`/`已结束` |
|
|
||||||
| `actualValue` | string? | 实际值(超标告警) |
|
|
||||||
| `thresholdValue` | string? | 阈值(超标告警) |
|
|
||||||
|
|
||||||
### B9-confirm: 确认告警
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/alarms/{adapter}/{alarmId}/confirm
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
| `alarmId` | string | ✅ | 告警 ID(子系统告警源 ID) |
|
|
||||||
|
|
||||||
### B9-end: 结束告警
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/alarms/{adapter}/{alarmId}/end
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**: 同 B9-confirm
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 录像查询
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/recordings/{adapter}/{deviceId}?start={start}&end={end}&page={page}&size={size}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
| `deviceId` | string | ✅ | 通道 SourceId |
|
|
||||||
| `start` | DateTime | ✅ | 录像起始时间 |
|
|
||||||
| `end` | DateTime | ✅ | 录像结束时间 |
|
|
||||||
| `page` | int | ❌ | 页码,默认 1 |
|
|
||||||
| `size` | int | ❌ | 每页条数,默认 20 |
|
|
||||||
|
|
||||||
**返回**: 录像文件列表(含文件名、起止时间、时长、大小)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 设备控制 (通用)
|
|
||||||
|
|
||||||
### B10: 下发控制指令
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/control/{adapter}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码,如 `KMS:main` |
|
|
||||||
|
|
||||||
**请求体 (GatewayControlRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `deviceId` | string | ✅ | 设备 SourceId |
|
|
||||||
| `command` | string | ✅ | 指令名:`open`(开门)/`close`(关门)/`authorize`(授权) |
|
|
||||||
| `parameters` | object | ❌ | 指令参数,如 `{"staffIds": [1,2], "lockholeSort": 3}` |
|
|
||||||
|
|
||||||
**返回 (ControlResult)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `success` | bool | 操作是否成功 |
|
|
||||||
| `message` | string? | 失败时的错误信息 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 业务记录查询
|
|
||||||
|
|
||||||
### B11: 查询子系统业务记录
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/gateway/logs/{adapter}?logType={logType}&from={from}&to={to}&page={page}&size={size}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
| `logType` | string | ✅ | 记录类型:`borrow`(借还)/`handover`(交接)/`permission`(授权) |
|
|
||||||
| `from` | DateTime | ❌ | 起始时间 |
|
|
||||||
| `to` | DateTime | ❌ | 结束时间 |
|
|
||||||
| `page` | int | ❌ | 页码,默认 1 |
|
|
||||||
| `size` | int | ❌ | 每页条数,默认 20 |
|
|
||||||
|
|
||||||
**返回参数**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `items` | array | 业务记录列表 |
|
|
||||||
| `total` | int | 总记录数 |
|
|
||||||
|
|
||||||
**items[].BusinessLogEntry**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `logId` | string | 记录唯一 ID |
|
|
||||||
| `logType` | string | 记录类型 |
|
|
||||||
| `deviceSourceId` | string? | 关联设备 SourceId |
|
|
||||||
| `staffName` | string? | 关联员工姓名 |
|
|
||||||
| `description` | string? | 记录描述 |
|
|
||||||
| `createdAt` | DateTime? | 记录时间 |
|
|
||||||
| `extra` | object? | 扩展属性 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 数据同步
|
|
||||||
|
|
||||||
### B12: 向子系统写入数据
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /api/gateway/sync/{adapter}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**:
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `adapter` | string | ✅ | 适配器编码 |
|
|
||||||
|
|
||||||
**请求体 (SyncRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `dataType` | string | ✅ | 数据类型,当前支持 `staff`(员工) |
|
|
||||||
| `items` | object[] | ✅ | 待同步数据列表 |
|
|
||||||
|
|
||||||
**返回 (SyncResult)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `successCount` | int | 成功数量 |
|
|
||||||
| `failCount` | int | 失败数量 |
|
|
||||||
| `message` | string? | 错误信息 |
|
|
||||||
|
|
||||||
### B13: 从子系统删除数据
|
|
||||||
|
|
||||||
```
|
|
||||||
DELETE /api/gateway/sync/{adapter}
|
|
||||||
```
|
|
||||||
|
|
||||||
**请求参数**: 同 B12
|
|
||||||
|
|
||||||
**请求体 (SyncDeleteRequest)**:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|:--:|------|
|
|
||||||
| `dataType` | string | ✅ | 数据类型,当前支持 `staff` |
|
|
||||||
| `ids` | string[] | ✅ | 待删除 ID 列表 |
|
|
||||||
|
|
||||||
**返回**: 同 `SyncResult`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 错误代码
|
|
||||||
|
|
||||||
### 通用 HTTP 状态码
|
|
||||||
|
|
||||||
| 状态码 | 含义 | 触发条件 |
|
|
||||||
|:---:|------|------|
|
|
||||||
| 200 | OK | 请求成功 |
|
|
||||||
| 400 | Bad Request | 请求参数格式错误 |
|
|
||||||
| 401 | Unauthorized | `X-Gateway-Key` 缺失或不匹配 |
|
|
||||||
| 404 | Not Found | 适配器不存在或不支持该能力 |
|
|
||||||
| 500 | Internal Server Error | 适配器内部异常 |
|
|
||||||
| 502 | Bad Gateway | 子系统返回错误或不可达 |
|
|
||||||
|
|
||||||
### 业务错误码
|
|
||||||
|
|
||||||
所有非 200 响应包含 JSON body:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `error` | string | 错误码 |
|
|
||||||
| `message` | string? | 人类可读的错误详情 |
|
|
||||||
|
|
||||||
| error 值 | HTTP | 说明 |
|
|
||||||
|------|:---:|------|
|
|
||||||
| `ADAPTER_NOT_FOUND` | 404 | 指定适配器编码不存在 |
|
|
||||||
| `CAPABILITY_NOT_SUPPORTED` | 404 | 适配器不支持该接口能力 |
|
|
||||||
| — (control 接口) | 502 | `ControlResult.Success=false` 时返回 `ControlResult.Message` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **接口总数**: 19 个 REST 端点
|
|
||||||
> **适配器**: Owl / MC4 / KMS(通过 `adapter` 参数路由)
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
# 网关 KMS 模块检查报告 2026-06-04
|
|
||||||
|
|
||||||
> **基准文档**: `doc/对接文档/钥匙管理系统软件接口.docx` (KMS API v1.0.4)
|
|
||||||
> **检查范围**: `gateway/src/IntegrationGateway.Adapters.Kms/` (KmsAdapter.cs / KmsAuthHelper.cs / KmsModels.cs) + `Program.cs` B10-B13 路由
|
|
||||||
> **方法**: 逐接口比对文档 → 代码 → 路由
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 覆盖率总览
|
|
||||||
|
|
||||||
| 模块 | KMS 文档端点数 | Gateway 覆盖 | 覆盖率 |
|
|
||||||
|------|:---:|:---:|:---:|
|
|
||||||
| 2.9 Token 获取 | 1 | 1 | 100% |
|
|
||||||
| 2.18 开放接口 | 8 | 8 | 100% |
|
|
||||||
| **总计 (Phase 1)** | **9** | **9** | **100%** |
|
|
||||||
| 2.3-2.17 标准接口 | 38 | 1 (确认告警) | 3% |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 逐接口检查
|
|
||||||
|
|
||||||
### 2.18.1 心跳 — `GET /prod-api/heartBeat`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | `client.GetAsync(...)` | GET | ✅ |
|
|
||||||
| 请求路径 | `/prod-api/heartBeat` | `/prod-api/heartBeat` | ✅ |
|
|
||||||
| 请求体 | 无 | 无 | ✅ |
|
|
||||||
| 错误处理 | `catch (Exception ex) { Console.Error.WriteLine; return false; }` | — | ✅ |
|
|
||||||
|
|
||||||
### 2.18.2 批量删除员工 — `POST /prod-api/batchDeleteStaff`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | `PostAsJsonAsync(...)` | POST | ✅ |
|
|
||||||
| 请求路径 | `/prod-api/batchDeleteStaff` | `/prod-api/batchDeleteStaff` | ✅ |
|
|
||||||
| 请求体 | `List<string>` (staffUuid 数组) | `["uuid1","uuid2",...]` | ✅ |
|
|
||||||
| 参数类型 | 数组 | 数组 (v1.0.2 修正) | ✅ |
|
|
||||||
|
|
||||||
### 2.18.3 批量同步员工 — `POST /prod-api/batchSyncStaff`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | `PostAsJsonAsync(...)` | POST | ✅ |
|
|
||||||
| 请求路径 | `/prod-api/batchSyncStaff` | `/prod-api/batchSyncStaff` | ✅ |
|
|
||||||
| 请求体 | `new { staff = staffList }` | staff 数组 | ⚠️ |
|
|
||||||
| account 字段 | 模型中有 Account? | v1.0.4 新增 account | ⚠️ 待验证 |
|
|
||||||
|
|
||||||
**风险**: 文档 v1.0.4 新增了 `account` (登录账号) 字段。`KmsStaff` 模型需确认包含此字段。网关包装为 `{ staff: [...] }` 可能与 KMS 期望的裸数组不一致。
|
|
||||||
|
|
||||||
### 2.18.4 查询柜体钥匙 — `POST /prod-api/getOpenerList`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | POST | POST | ✅ |
|
|
||||||
| 请求路径 | `/prod-api/getOpenerList` | `/prod-api/getOpenerList` | ✅ |
|
|
||||||
| 请求体 | `"{}"` | 无明确要求 / 空对象 | ✅ |
|
|
||||||
| 响应 → StandardDevice | 柜体→父设备, 锁孔→子设备 | 树状结构 | ✅ |
|
|
||||||
|
|
||||||
### 2.18.5 查询授权记录 — `POST /prod-api/getPermissionList`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | POST | POST | ✅ |
|
|
||||||
| 请求路径 | `/prod-api/getPermissionList` | `/prod-api/getPermissionList` | ✅ |
|
|
||||||
| 请求体 | `"{}"` | 授权记录业务对象 (含 lockerName, lendStaffName 等 20+ 字段) | 🔴 |
|
|
||||||
| 时间范围 | `DateTime? from, DateTime? to` 参数**未传入请求体** | `beginApplyTime`/`endApplyTime` | 🔴 |
|
|
||||||
| 分页 | `page`/`size` 参数**未传入请求体** | `pageNum`/`pageSize` | 🔴 |
|
|
||||||
|
|
||||||
**致命问题**: 网关注入 `from`/`to`/`page`/`size` 参数但**从未传入 KMS 请求体**。代码注释 `// 联调时加入时间范围` 确认这是已知缺口。当前实现等价于无过滤全量查询,无法按时间范围分页。
|
|
||||||
|
|
||||||
### 2.18.6 查询借还记录 — `POST /prod-api/getRecordList`
|
|
||||||
|
|
||||||
**与 2.18.5 完全相同的致命问题**: `from`/`to`/`page`/`size` 参数未传入 KMS 请求体。此外文档标记 `lockerName`、`lockholeSort`、`openerCnName` 为必填字段,但网关传 `"{}"` 无这些字段。
|
|
||||||
|
|
||||||
### 2.18.7 查询告警记录 — `POST /prod-api/getWarningList`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | POST | POST | ✅ |
|
|
||||||
| 请求路径 | `/prod-api/getWarningList` | `/prod-api/getWarningList` | ✅ |
|
|
||||||
| 请求体 | `"{}"` | 告警业务对象 (含 type, beginWarningTime 等) | 🔴 |
|
|
||||||
| 时间范围 | 未传 | `beginWarningTime`/`endWarningTime` | 🔴 |
|
|
||||||
| 告警类型 | 未传 (type=1当前/2历史) | 文档支持过滤 | 🔴 |
|
|
||||||
| 响应映射 | `Type==1 ? "未确认" : "已结束"` | `type` 1=当前告警, 2=历史告警 | 🔴 |
|
|
||||||
|
|
||||||
**状态映射错误**: `type` 字段在告警接口中表示 1=当前告警 / 2=历史告警,**不是** 1=未确认 / 2=已结束。代码将 type=1 映射为 Status="未确认"、type=2 映射为 Status="已结束",语义错误。正确的映射应该是 type=1 → "活跃", type=2 → "历史"。
|
|
||||||
|
|
||||||
### 2.18.8 单点登录 — `POST /thirdPlatlogin`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | POST | POST | ✅ |
|
|
||||||
| 请求路径 | `/thirdPlatlogin?username={x}` | `/thirdPlatlogin?username={x}` | ✅ |
|
|
||||||
| 重定向处理 | 捕获 302, 返回 Location header | 文档说明"调用成功后直接重定向" | ✅ |
|
|
||||||
| 超时 | 无显式设置 | — | 🟡 |
|
|
||||||
|
|
||||||
### 2.9 Token 获取 — `POST /prod-api/getToken`
|
|
||||||
|
|
||||||
| 检查项 | Gateway 实现 | 文档规范 | 状态 |
|
|
||||||
|------|------|------|:--:|
|
|
||||||
| 请求方法 | `http.PostAsync(url, null)` | POST | ✅ |
|
|
||||||
| 参数位置 | query string: `?clientId=&clientSecret=` | query string | ✅ |
|
|
||||||
| 响应校验 | `Code != 200` → 抛异常 | `code: 200` = 成功 | ✅ |
|
|
||||||
| 缓存策略 | 25分钟 (30分钟效期-5分钟余量) | 30分钟效期 | ✅ |
|
|
||||||
|
|
||||||
> **注意**: 文档 2.9.1 显示 `POST /prod-api/getToken` 参数在 body 中 (`{ clientId, clientSecret }`),但 2.9 节概述描述为 query 参数。两种方式 KMS 可能都支持。当前实现用 query string,联调时需确认兼容。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 模型映射核对
|
|
||||||
|
|
||||||
### 3.1 KMS 柜体 → StandardDevice
|
|
||||||
|
|
||||||
| 文档字段 | 代码映射 | 准确性 |
|
|
||||||
|------|------|:--:|
|
|
||||||
| lockerId | `SourceId = $"locker_{lockerId}"` | ✅ |
|
|
||||||
| lockerName | `Name = lockerName` | ✅ |
|
|
||||||
| lockerCode | `Extra["lockerCode"]` | ✅ |
|
|
||||||
| lockholeList | 遍历展开为子设备 | ✅ |
|
|
||||||
| IsParent | `true` | ✅ |
|
|
||||||
|
|
||||||
### 3.2 KMS 锁孔 → StandardDevice
|
|
||||||
|
|
||||||
| 文档字段 | 代码映射 | 准确性 |
|
|
||||||
|------|------|:--:|
|
|
||||||
| lockholeSort | `SourceId = $"lockhole_{lockerId}_{lockholeSort}"` | ✅ |
|
|
||||||
| openerName | `Name = openerName` | ✅ |
|
|
||||||
| openerType | `Extra["openerType"]` (1/2/3 数值) | ✅ |
|
|
||||||
| openerState | `Extra["openerState"]` + `IsOnline = (openerState=="在位")` | 🔴 |
|
|
||||||
| ParentSourceId | `$"locker_{lockerId}"` | ✅ |
|
|
||||||
|
|
||||||
**openerState 映射错误**: 根据文档数据字典(§4),`openerState` 是数值编码:
|
|
||||||
- 1 = 在柜
|
|
||||||
- 2 = 借出
|
|
||||||
- 3 = 录入
|
|
||||||
- 10 = 丢失
|
|
||||||
|
|
||||||
代码用 `openerState == "在位"` 做字符串比较,**永远不成立**。需改为 `openerState == "1"` 或解析为 int 后判断。
|
|
||||||
|
|
||||||
### 3.3 KMS 借还记录 → BusinessLogEntry
|
|
||||||
|
|
||||||
| 文档字段 | 代码映射 | 准确性 |
|
|
||||||
|------|------|:--:|
|
|
||||||
| uuid | `LogId` | ✅ |
|
|
||||||
| lockerName | 拼入 `DeviceSourceId` | ✅ |
|
|
||||||
| staffName | `StaffName` | ✅ |
|
|
||||||
| borrowTime | `CreatedAt` | ✅ |
|
|
||||||
| openerName | `Description` (不充分) | 🟡 |
|
|
||||||
|
|
||||||
### 3.4 KMS 告警 → StandardAlarm
|
|
||||||
|
|
||||||
| 文档字段 | 代码映射 | 准确性 |
|
|
||||||
|------|------|:--:|
|
|
||||||
| uuid | `AlarmId` | ✅ |
|
|
||||||
| warningTime | `OccurTime` | ✅ |
|
|
||||||
| remark | `Content` | ✅ |
|
|
||||||
| type (1/2) | `Status = Type==1 ? "未确认":"已结束"` | 🔴 |
|
|
||||||
| level | 固定 `"普通"` | 🟡 |
|
|
||||||
|
|
||||||
**type 语义错误**: 见 2.18.7 说明。文档明确 `type` 表示告警分类(1=当前,2=历史),而非确认状态。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. B 路由链路检查
|
|
||||||
|
|
||||||
| B 路由 | 对应 KMS 能力 | 适配器方法 | 参数传递 | 状态 |
|
|
||||||
|------|------|------|:--:|:--:|
|
|
||||||
| B1 `/health` | 心跳 2.18.1 | `HealthCheckAsync` | ✅ | ✅ |
|
|
||||||
| B2 `/devices` | 柜体钥匙 2.18.4 | `GetDevicesAsync` | ✅ | ✅ |
|
|
||||||
| B8 `/alarms` | 告警 2.18.7 | `GetAlarmsAsync` | ✅ | ✅ (映射有误) |
|
|
||||||
| B9 `/alarms/{id}/confirm` | 确认告警 | `ConfirmAlarmAsync` | ✅ | ⚠️ 端点未确认 |
|
|
||||||
| B10 `/control` | 远程控制 | `SendControlAsync` | ✅ | ⚠️ |
|
|
||||||
| B11 `/logs` | 业务记录 | `GetBusinessLogsAsync` | ✅ | ⚠️ |
|
|
||||||
| B12 `/sync` (POST) | 员工同步 2.18.3 | `SyncDataAsync` | ✅ | ⚠️ |
|
|
||||||
| B13 `/sync` (DELETE) | 删除员工 2.18.2 | `DeleteDataAsync` | ✅ | ✅ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 问题汇总
|
|
||||||
|
|
||||||
### 🔴 致命问题 (需联调前修复)
|
|
||||||
|
|
||||||
| # | 问题 | 影响 |
|
|
||||||
|:--:|------|------|
|
|
||||||
| **R1** | 2.18.5/2.18.6/2.18.7 请求体只传 `"{}"`,忽略 `from`/`to`/`page`/`size` 参数 | 无法按时间分页查询,联调时大概率返回全量数据或报错 |
|
|
||||||
| **R2** | 2.18.7 type 字段语义错误 (1=当前告警被映射为"未确认") | 告警状态全部错误 |
|
|
||||||
| **R3** | openerState 字符串比较 vs 文档数值编码 | 所有锁孔 IsOnline 永远为 false |
|
|
||||||
|
|
||||||
### 🟠 严重问题
|
|
||||||
|
|
||||||
| # | 问题 | 影响 |
|
|
||||||
|:--:|------|------|
|
|
||||||
| **S1** | 2.18.6 必填字段 (`lockerName`, `lockholeSort`, `openerCnName`) 未传 | 借还记录查询可能被 KMS 拒绝 |
|
|
||||||
| **S2** | `ConfirmAlarmAsync` 端点 (`/prod-api/kms/warning/confirm/{id}`) 未在文档中确认存在 | 告警确认功能不可用 |
|
|
||||||
| **S3** | 2.18.5 请求体结构未知 (文档未给完整示例) | 授权记录查询格式需联调验证 |
|
|
||||||
|
|
||||||
### 🟡 改善项
|
|
||||||
|
|
||||||
| # | 问题 | 建议 |
|
|
||||||
|:--:|------|------|
|
|
||||||
| **M1** | KmsModels.cs 包含大量 Phase 2 DTO 但未使用 | 保留,Phase 2 可用 |
|
|
||||||
| **M2** | B10 控制指令 `command == "open"` 和 `command == "authorize"` 都调同一方法 | 区分"开门"和"授权"两种指令 |
|
|
||||||
| **M3** | ThirdPlatLoginAsync 无超时设置 | 加 15s 超时 |
|
|
||||||
| **M4** | SyncDevicesJob 中 `gwRepo.Update(node)` vs `gwSvc.UpdateAsync(node)` 不一致 | 统一风格 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 需联调验证项
|
|
||||||
|
|
||||||
| # | 验证项 | 说明 |
|
|
||||||
|:--:|------|------|
|
|
||||||
| V1 | Token 获取用 query string vs body | 文档 2.9.1 标记为 body 参数,2.9 节概述为 query |
|
|
||||||
| V2 | `batchSyncStaff` body 格式 | `{ staff: [...] }` vs `[...]` |
|
|
||||||
| V3 | `KmsStaff` 是否需 account 字段 | v1.0.4 新增 |
|
|
||||||
| V4 | `getOpenerList` 返回的 openerState 是数值还是中文 | 决定映射逻辑 |
|
|
||||||
| V5 | `getRecordList` 必填字段是否真的必填 | 决定请求体最小字段集 |
|
|
||||||
| V6 | `getPermissionList` 请求体完整格式 | 文档示例不完整 |
|
|
||||||
| V7 | `warning/confirm` 端点存在性 | 调标准管理接口 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **结论**: 9 个 Phase 1 接口全部实现覆盖(100%),但 3 个致命问题(R1-R3)需在联调前修复——核心是请求体格式、type 语义映射、openerState 编码映射。其余 4 个严重问题需联调验证后确认。
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user