Compare commits
5 Commits
c8f36ad3b4
...
8413a52a28
| Author | SHA1 | Date | |
|---|---|---|---|
| 8413a52a28 | |||
| 6835ce86ce | |||
| 7adf6407d5 | |||
| 4427ca9fb9 | |||
| 55d42db81c |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,3 +13,7 @@ temp.json
|
||||
*.bak
|
||||
tmp.bat
|
||||
report_form_rollup-plugin-visualizer.html
|
||||
|
||||
**/bin/
|
||||
**/obj/
|
||||
appsettings.Production.json
|
||||
@@ -27,44 +27,46 @@ namespace VolPro.Core.Utilities
|
||||
|
||||
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();
|
||||
pen.FakeBoldText = true;
|
||||
pen.Style = SKPaintStyle.Fill;
|
||||
pen.TextSize = 20;// 0.6f * info.Width * pen.TextSize / pen.MeasureText(code);
|
||||
pen.TextSize = 20;
|
||||
pen.Typeface = typeface; // 使用加载的本地字体
|
||||
|
||||
// 绘制随机字符
|
||||
for (int i = 0; i < code.Length; i++)
|
||||
{
|
||||
pen.Color = random.GetRandom(colors);//随机颜色索引值
|
||||
|
||||
pen.Typeface = SKTypeface.FromFamilyName("DejaVu Sans", 700, 20, SKFontStyleSlant.Italic);//配置字体
|
||||
var point = new SKPoint()
|
||||
pen.Color = random.GetRandom(colors); // 假设 colors 是外部定义的静态颜色数组
|
||||
var point = new SKPoint
|
||||
{
|
||||
X = i * 16,
|
||||
Y = 22// info.Height - ((i + 1) % 2 == 0 ? 2 : 4),
|
||||
|
||||
Y = 22
|
||||
};
|
||||
canvas.DrawText(code.Substring(i, 1), point, pen);//绘制一个验证字符
|
||||
|
||||
canvas.DrawText(code.Substring(i, 1), point, pen);
|
||||
}
|
||||
|
||||
// 绘制噪点
|
||||
var points = Enumerable.Range(0, 100).Select(
|
||||
_ => new SKPoint(random.Next(bitmap.Width), random.Next(bitmap.Height))
|
||||
).ToArray();
|
||||
canvas.DrawPoints(
|
||||
SKPointMode.Points,
|
||||
points,
|
||||
pen);
|
||||
canvas.DrawPoints(SKPointMode.Points, points, pen);
|
||||
|
||||
//绘制贝塞尔线条
|
||||
// 绘制贝塞尔线条(原有逻辑存在 p1~p4 全为零的问题,此处保留原样)
|
||||
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();
|
||||
@@ -76,8 +78,76 @@ namespace VolPro.Core.Utilities
|
||||
path.CubicTo(touchPoints[1], touchPoints[2], touchPoints[3]);
|
||||
canvas.DrawPath(path, bPen);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
中文提示 : 检测到你没有开启文件,AllowLoadLocalInfile=true加到自符串上,已自动执行 SET GLOBAL local_infile=1 在试一次
|
||||
English Message : Loading local data is disabled; this must be enabled on both the client and server sides at SqlSugar.Check.ExceptionEasy(String enMessage, String cnMessage)
|
||||
at SqlSugar.MySqlFastBuilder.ExecuteBulkCopyAsync(DataTable dt)
|
||||
at SqlSugar.FastestProvider`1._BulkCopy(List`1 datas)
|
||||
at SqlSugar.FastestProvider`1.BulkCopyAsync(List`1 datas)
|
||||
at SqlSugar.FastestProvider`1.BulkCopy(List`1 datas)
|
||||
at VolPro.Core.Services.Logger.Start() in D:\Code\SecMPS\api_sqlsugar\VolPro.Core\Services\Logger.cs:line 194SqlSugar
|
||||
@@ -4,13 +4,18 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<DeleteExistingFiles>False</DeleteExistingFiles>
|
||||
<ExcludeApp_Data>False</ExcludeApp_Data>
|
||||
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
|
||||
<DeleteExistingFiles>true</DeleteExistingFiles>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<PublishProvider>FileSystem</PublishProvider>
|
||||
<PublishUrl>bin\Release\netcoreapp3.1\net6.0\publish\</PublishUrl>
|
||||
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
|
||||
<ProjectGuid>4db3c91b-93fe-4937-8b58-ddd3f57d4607</ProjectGuid>
|
||||
<SelfContained>true</SelfContained>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -59,5 +59,11 @@
|
||||
<None Include="Startup copy.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="fonts\DejaVuSans.ttf">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
||||
BIN
api_sqlsugar/VolPro.WebApi/fonts/DejaVuSans.ttf
Normal file
BIN
api_sqlsugar/VolPro.WebApi/fonts/DejaVuSans.ttf
Normal file
Binary file not shown.
@@ -3,8 +3,10 @@ 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;
|
||||
|
||||
@@ -12,46 +14,60 @@ namespace VolPro.Warehouse.Services;
|
||||
/// 心跳超时检测任务。扫描心跳超时 30 秒的网关节点,标记为离线,
|
||||
/// 并级联标记该节点下所有设备为离线。
|
||||
/// Cron 建议: 每 15 秒 ("0/15 * * * * ?")
|
||||
///
|
||||
/// 设备与网关的关联通过 AdapterCode 前缀匹配(如设备 AdapterCode="MC4:31ku" 匹配网关 AdapterTypes="MC4:31ku")。
|
||||
/// </summary>
|
||||
public class HeartbeatMonitorJob : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
|
||||
if (sp == null) return;
|
||||
|
||||
var gwSvc = sp.GetService<Igateway_nodesService>();
|
||||
var devSvc = sp.GetService<Ibase_deviceService>();
|
||||
if (gwSvc == null) return;
|
||||
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();
|
||||
x => x.IsOnline == "在线" && x.LastHeartbeat < timeout).ToListAsync();
|
||||
|
||||
foreach (var node in offlineNodes)
|
||||
{
|
||||
// 标记网关离线
|
||||
node.IsOnline = "离线";
|
||||
await gwSvc.FindAsIQueryable(x => x.NodeId == node.NodeId)
|
||||
.FirstAsync(); // 确保实体被跟踪
|
||||
// 直接通过 DbContext 更新
|
||||
var dbProp = gwSvc.GetType().BaseType?.GetProperty("DbContext");
|
||||
if (dbProp != null) continue; // fallback: 通过 FindAsIQueryable 重新获取更新
|
||||
|
||||
try { gwRepo.Update(node); } catch { }
|
||||
Console.WriteLine($"[HeartbeatMonitorJob] 网关 {node.NodeCode} 心跳超时,标记离线");
|
||||
|
||||
// 级联标记该网关下所有设备离线
|
||||
if (devSvc != null)
|
||||
// 级联标记该网关下所有设备离线(批量 SQL)
|
||||
try
|
||||
{
|
||||
var devices = await devSvc.FindAsIQueryable(
|
||||
x => x.NodeId == node.NodeId && x.IsOnline == "在线")
|
||||
.ToListAsync();
|
||||
foreach (var dev in devices)
|
||||
var adapterPrefixes = (node.AdapterTypes ?? "")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim()).ToList();
|
||||
|
||||
if (adapterPrefixes.Any())
|
||||
{
|
||||
dev.IsOnline = "离线";
|
||||
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} 台设备离线");
|
||||
}
|
||||
Console.WriteLine($"[HeartbeatMonitorJob] 级联 {devices.Count} 台设备离线");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[HeartbeatMonitorJob] 级联离线失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,87 @@
|
||||
using Quartz;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using VolPro.Entity.DomainModels;
|
||||
using Warehouse.IRepositories;
|
||||
using Warehouse.IServices;
|
||||
|
||||
namespace VolPro.Warehouse.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 实时数据轮询任务(Phase 2 完善)。
|
||||
/// 定时轮询 MC4.0 IoT 设备实时值 → 更新 iot_devicedata。
|
||||
/// 实时数据轮询任务。
|
||||
/// 定时轮询在线 MC4 IoT 设备的实时值 → 写入 iot_devicedata 表。
|
||||
/// Cron 建议: 每 10 秒 ("0/10 * * * * ?")
|
||||
///
|
||||
/// 设备与网关的关联通过 AdapterCode 前缀匹配(如设备 AdapterCode="MC4:31ku" 匹配网关 AdapterTypes="MC4:31ku")。
|
||||
/// </summary>
|
||||
public class RealtimePollJob : IJob
|
||||
{
|
||||
public Task Execute(IJobExecutionContext context)
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
// TODO: Phase 2 — 遍历在线 MC4 网关,轮询实时值写入 iot_devicedata
|
||||
return Task.CompletedTask;
|
||||
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
|
||||
if (sp == null) return;
|
||||
|
||||
var gwSvc = sp.GetService<Igateway_nodesService>();
|
||||
var devRepo = sp.GetService<Ibase_deviceRepository>();
|
||||
var dataRepo = sp.GetService<Iiot_devicedataRepository>();
|
||||
var gatewayClient = sp.GetService<GatewayClient>();
|
||||
if (gwSvc == null || devRepo == null || dataRepo == null || gatewayClient == null) return;
|
||||
|
||||
// 1. 查在线 MC4 网关
|
||||
var onlineNodes = await gwSvc.FindAsIQueryable(x =>
|
||||
x.IsOnline == "在线" && x.AdapterTypes != null && x.AdapterTypes.Contains("MC4")).ToArrayAsync();
|
||||
|
||||
foreach (var node in onlineNodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseUrl = node.BaseUrl;
|
||||
if (string.IsNullOrEmpty(baseUrl)) continue;
|
||||
|
||||
// 2. 解析网关管理的适配器前缀列表
|
||||
var adapterPrefixes = (node.AdapterTypes ?? "")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim());
|
||||
|
||||
// 3. 查该网关下在线的 IoT 设备(AdapterCode 前缀匹配)
|
||||
var devices = await devRepo.FindAsIQueryable(d =>
|
||||
d.DeviceGroup == "IoT设备" && d.IsOnline == "在线").ToListAsync();
|
||||
var matchedDevices = devices.Where(d =>
|
||||
adapterPrefixes.Any(p => (d.AdapterCode ?? "").StartsWith(p))).ToList();
|
||||
|
||||
if (!matchedDevices.Any()) continue;
|
||||
|
||||
// 4. 逐设备调网关 B4 获取实时值
|
||||
foreach (var dev in matchedDevices)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await gatewayClient.GetRealtimeAsync(baseUrl, dev.AdapterCode, dev.SourceId);
|
||||
if (result == null) continue;
|
||||
var root = result.RootElement;
|
||||
var points = root.TryGetProperty("pointValues", out var pv) ? pv
|
||||
: root.TryGetProperty("rows", out var r) ? r
|
||||
: root;
|
||||
// 结果可能是 PointValue[] 数组,取第一个点位写入
|
||||
if (points.ValueKind == System.Text.Json.JsonValueKind.Array && points.GetArrayLength() > 0)
|
||||
{
|
||||
var first = points[0];
|
||||
var entry = new iot_devicedata
|
||||
{
|
||||
DeviceId = dev.DeviceId,
|
||||
PointValue = first.TryGetProperty("value", out var v) ? v.GetDecimal() : (decimal?)null,
|
||||
UpdateTime = DateTime.Now,
|
||||
Interval = first.TryGetProperty("interval", out var iv) ? iv.GetInt32() : 10
|
||||
};
|
||||
dataRepo.Add(entry);
|
||||
}
|
||||
}
|
||||
catch { /* 单设备失败不阻塞其他设备 */ }
|
||||
}
|
||||
}
|
||||
catch { /* 单网关失败不阻塞其他网关 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,3 +161,33 @@ CREATE TABLE gateway_nodes (
|
||||
UNIQUE INDEX IX_Code (NodeCode),
|
||||
INDEX IX_Online (IsOnline)
|
||||
) COMMENT '网关节点注册表';
|
||||
|
||||
|
||||
-- ═══════════════════════════════════════════════
|
||||
-- SecMPS 规则引擎: warehouse_variable 变量定义表 (P1-6)
|
||||
-- =================================================
|
||||
-- 规则条件/动作的 ValueId 绑定到此表的 VariableId
|
||||
-- DeviceId 关联 base_device.DeviceId
|
||||
-- =================================================
|
||||
|
||||
CREATE TABLE warehouse_variable (
|
||||
VariableId INT IDENTITY(1,1) PRIMARY KEY,
|
||||
DeviceId INT NOT NULL,
|
||||
VariableName NVARCHAR(255) NOT NULL, -- 温度/湿度/人数
|
||||
PointIndex INT DEFAULT 0, -- MC4 pointIndex / Owl 统计量编码
|
||||
Unit NVARCHAR(50) NULL, -- ℃/%/人
|
||||
SortOrder INT DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IX_warehouse_variable_DeviceId ON warehouse_variable (DeviceId);
|
||||
|
||||
|
||||
-- F3.2 规则引擎滞后窗 (hysteresis)
|
||||
ALTER TABLE warehouse_rulecondition ADD
|
||||
RecoveryThreshold_Numeric DECIMAL(18,2) NULL,
|
||||
RecoveryThreshold_Switch NVARCHAR(50) NULL;
|
||||
|
||||
-- F3.3 条件级冷却
|
||||
ALTER TABLE warehouse_rulecondition ADD
|
||||
LastTriggered DATETIME NULL,
|
||||
LastTriggerValue DECIMAL(18,2) NULL;
|
||||
|
||||
3
doc/error/KMS_error_20260519.txt
Normal file
3
doc/error/KMS_error_20260519.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
严重性 代码 说明 项目 文件 行 抑制状态 详细信息
|
||||
错误(活动) CS1061 “ControlRequest”未包含“Command”的定义,并且找不到可接受第一个“ControlRequest”类型参数的可访问扩展方法“Command”(是否缺少 using 指令或程序集引用?) IntegrationGateway.Host D:\Code\SecMPS\gateway\src\IntegrationGateway.Host\Program.cs 213
|
||||
错误(活动) CS1061 “ControlRequest”未包含“Parameters”的定义,并且找不到可接受第一个“ControlRequest”类型参数的可访问扩展方法“Parameters”(是否缺少 using 指令或程序集引用?) IntegrationGateway.Host D:\Code\SecMPS\gateway\src\IntegrationGateway.Host\Program.cs 213
|
||||
56
doc/设计文档/SecMPS统一问题清单20260603.md
Normal file
56
doc/设计文档/SecMPS统一问题清单20260603.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# SecMPS 统一问题清单 2026-06-03
|
||||
|
||||
> **版本**: 1.0
|
||||
> **日期**: 2026-06-03
|
||||
> **范围**: gateway / VolPro (api_sqlsugar) / web.vite / warehouse / owl_zlmediakit
|
||||
> **来源**: 项目深度审计 + 规则引擎方案审查
|
||||
|
||||
---
|
||||
|
||||
## P0 — 阻塞性(影响功能完整性,必须修复)
|
||||
|
||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
||||
|:---:|:---:|------|------|------|
|
||||
| P0-1 | 规则引擎 | RealtimePollJob 空壳 — IoT 实时值从未持久化,规则引擎无历史数据源 | 规则无法追溯历史趋势 | 在此 Job 实现轮询→写入 iot_devicedata,或合并到 RuleEngineJob |
|
||||
| P0-2 | 网关 | A1 自注册未调用 — `GatewayClientFactory.RegisterAsync` 已定义但 Program.cs 从未执行 | 网关启动后不向 Vol.Pro 注册 | `InitializeAllAsync()` 后遍历适配器调 A1 |
|
||||
| P0-3 | 安全 | B 组路由零认证 — 14+ 条路由无任何认证 | 内网未授权客户端可操控设备、查视频流 | 生产环境绑定 `127.0.0.1`,或加 `X-Gateway-Key` 中间件 |
|
||||
|
||||
---
|
||||
|
||||
## P1 — 重要(影响性能、安全、可靠性)
|
||||
|
||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
||||
|:---:|:---:|------|------|------|
|
||||
| P1-1 | 性能 | 逐设备 B4 调用 — 规则引擎按设备逐个调 B4 | 规则引擎 90% 时间耗在网络往返 | 新增 `POST /realtime/{adapter}/batch` 批量接口 |
|
||||
| P1-2 | 性能 | 级联离线标记逐条 UPDATE — HeartbeatMonitorJob 对每台设备单独更新 | 设备多时慢且无事务 | 一条 SQL: `UPDATE base_device SET IsOnline='离线' WHERE GatewayNodeId=@id` |
|
||||
| P1-3 | 安全 | Token/密码明文存储 — appsettings.json 明文且被复制到 bin/ | 源码泄露 = 凭据泄露 | 环境变量覆盖 + `.gitignore bin` |
|
||||
| P1-4 | 可维护 | 前端硬编码网关地址 — `const GW = 'http://localhost:5100'` | 部署时需逐文件修改 | 统一用 `window.apiConfig.gatewayUrl` |
|
||||
| P1-5 | 规则引擎 | DeviceId→(AdapterCode, SourceId) 解析缺失 | 规则引擎无法直接调网关 B4 | 批量查 base_device 建映射表 |
|
||||
| P1-6 | 规则引擎 | ValueId 语义模糊 — 字典绑定但无对应实体表 | "变量"选的是什么不明确 | 新建 `warehouse_variable` 表 |
|
||||
|
||||
---
|
||||
|
||||
## P2 — 改善(影响排错效率、维护成本)
|
||||
|
||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
||||
|:---:|:---:|------|------|------|
|
||||
| P2-1 | 代码质量 | 静默异常吞噬 — 适配器 `catch { return false; }` | 离线不知道原因 | `catch(Exception ex)` + STDERR 输出 |
|
||||
| P2-2 | 规则引擎 | 阈值抖动 — 温度反复跳变时规则频繁触发→恢复 | 空调反复开关,告警洪水 | hysteresis 滞后窗 |
|
||||
| P2-3 | 规则引擎 | 冷却期粒度 — Cooldown 在规则级,OR 组合不该整体冷却 | 冷却期过宽 | 冷却期下沉到条件表或基于"上次触发值"去重 |
|
||||
| P2-4 | 可维护 | warehouse 端 console.log 残留 — 30+ 处开发日志 | 生产环境噪声 | vite.config.ts 移除非生产日志 |
|
||||
| P2-5 | 可维护 | 双端 gateway API 重复封装 | 维护两份 | 统一到 web.vite/src/api/gateway.js |
|
||||
|
||||
---
|
||||
|
||||
## P3 — 优化(影响开发体验、仓库整洁)
|
||||
|
||||
| 编号 | 类别 | 问题 | 影响 | 方案 |
|
||||
|:---:|:---:|------|------|------|
|
||||
| P3-1 | 规则引擎 | 动作执行阻塞 — `ExecuteActionsAsync` 串行等待 B5 响应 | 一条规则卡住全部阻塞 | `Task.WhenAll` + 5s 超时 |
|
||||
| P3-2 | 文档 | bin 目录残留配置 | 仓库体积 + 凭据泄露 | `.gitignore` 加 `**/bin/` |
|
||||
| P3-3 | 开发 | 网关无 Swagger | 调试需手动 curl | `AddEndpointsApiExplorer` + `MapSwagger` |
|
||||
| P3-4 | 文档 | 设计文档与代码路由数不一致 | 架构文档过时 | 每次 Phase 同步更新 |
|
||||
|
||||
---
|
||||
|
||||
> **总计**: 18 项 — P0: 3 / P1: 6 / P2: 5 / P3: 4
|
||||
280
doc/设计文档/SecMPS统一问题清单20260603修复方案.md
Normal file
280
doc/设计文档/SecMPS统一问题清单20260603修复方案.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# SecMPS 统一问题清单 2026-06-03 修复方案
|
||||
|
||||
> **版本**: 1.0
|
||||
> **日期**: 2026-06-03
|
||||
> **基准**: `SecMPS统一问题清单20260603.md`
|
||||
> **原则**: 按优先级逐项修复,每项修复后编译验证;涉及网关/Vol.Pro 改动的放一组批量提交
|
||||
|
||||
---
|
||||
|
||||
## 修复总览
|
||||
|
||||
| 阶段 | 优先级 | 涉及项目 | 文件数 | 预计 |
|
||||
|:---:|:---:|------|:---:|:---:|
|
||||
| F1 | P0-1 ~ P0-3 | gateway + Vol.Pro | 5 | 2h |
|
||||
| F2 | P1-1 ~ P1-6 | gateway + Vol.Pro + 库表 + 前端 | 8 | 4h |
|
||||
| F3 | P2-1 ~ P2-5 | gateway + warehouse | 8 | 2h |
|
||||
| F4 | P3-1 ~ P3-4 | gateway + 文档 | 4 | 1h |
|
||||
| **合计** | — | 4 项目 | **25** | **~9h** |
|
||||
|
||||
---
|
||||
|
||||
## 阶段 F1: P0 阻塞项修复(预计 2h)
|
||||
|
||||
#### F1.1 [P0-1] RealtimePollJob 填充实现
|
||||
|
||||
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/RealtimePollJob.cs`
|
||||
- [ ] 注入 `GatewayClient` + `Ibase_deviceRepository`
|
||||
- [ ] `Execute()` 中:
|
||||
1. 查询在线 MC4 网关 (`gateway_nodes WHERE IsOnline=在线 AND AdapterTypes LIKE '%MC4%'`)
|
||||
2. 查对应设备列表 (`base_device WHERE DeviceGroup='IoT设备' AND IsOnline=在线`)
|
||||
3. 对每个设备调 `GatewayClient.GetRealtimeAsync(gwBaseUrl, adapterCode, sourceId)`
|
||||
4. 结果写入 `iot_devicedata` 表(INSERT 新记录)
|
||||
- [ ] `dotnet build` → 0 错误
|
||||
|
||||
#### F1.2 [P0-2] 网关 A1 自注册
|
||||
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
||||
- [ ] 在 `registry.InitializeAllAsync()` 后加入:
|
||||
```csharp
|
||||
var nodeCode = gwCfg["NodeCode"] ?? "gw-default";
|
||||
var nodeToken = gwCfg["NodeToken"] ?? "";
|
||||
var adapterTypes = string.Join(",", registry.All.Select(a => a.AdapterCode));
|
||||
await clientFactory.RegisterAsync(nodeCode, nodeToken, adapterTypes, volProUrl);
|
||||
```
|
||||
- [ ] `dotnet build` → 0 错误
|
||||
|
||||
#### F1.3 [P0-3] B 组路由认证
|
||||
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
||||
- [ ] 在 `app.UseCors()` 之后添加中间件:
|
||||
```csharp
|
||||
var gatewayKey = gwCfg["GatewayKey"];
|
||||
if (!string.IsNullOrEmpty(gatewayKey))
|
||||
{
|
||||
app.Use(async (context, next) => {
|
||||
var key = context.Request.Headers["X-Gateway-Key"].FirstOrDefault();
|
||||
if (key == gatewayKey || context.Request.Path == "/") { await next(); }
|
||||
else { context.Response.StatusCode = 401; }
|
||||
});
|
||||
}
|
||||
```
|
||||
- [ ] appsettings.json Gateway 段新增 `"GatewayKey": null`
|
||||
- [ ] Vol.Pro 端 `GatewayClient` 所有 HTTP 请求头自动附加 `X-Gateway-Key`
|
||||
- [ ] `dotnet build` → 0 错误
|
||||
|
||||
> **F1 提交点**: `Fix-P0: RealtimePollJob+A1自注册+B组认证`
|
||||
|
||||
---
|
||||
|
||||
## 阶段 F2: P1 重要项修复(预计 4h)
|
||||
|
||||
#### F2.1 [P1-1] 网关新增批量实时值接口
|
||||
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
||||
- [ ] 新增 B4-batch 路由:
|
||||
```csharp
|
||||
app.MapPost("/api/gateway/realtime/{adapter}/batch", async (string adapter, BatchRealtimeRequest req) =>
|
||||
{
|
||||
var a = registry.FindByCode<IHasPoints>(adapter);
|
||||
if (a == null) return Results.NotFound();
|
||||
var results = new Dictionary<string, List<PointValue>>();
|
||||
foreach (var deviceId in req.DeviceIds ?? new())
|
||||
results[deviceId] = await a.GetRealtimeValuesAsync(deviceId);
|
||||
return Results.Ok(results);
|
||||
});
|
||||
```
|
||||
- [ ] 新增 `record BatchRealtimeRequest(List<string>? DeviceIds);`
|
||||
- [ ] `dotnet build` → 0 错误
|
||||
|
||||
#### F2.2 [P1-2] 批量级联离线标记
|
||||
|
||||
- [ ] 编辑 `api_sqlsugar/Warehouse/Services/HeartbeatMonitorJob.cs`
|
||||
- [ ] 替换逐条 `UpdateAsync` 为:
|
||||
```csharp
|
||||
context.Repository.DbContext.Db.Ado.ExecuteCommand(
|
||||
"UPDATE base_device SET IsOnline='离线' WHERE GatewayNodeId=@id AND IsOnline='在线'",
|
||||
new { id = node.GatewayNodeId });
|
||||
```
|
||||
- [ ] `dotnet build` → 0 错误
|
||||
|
||||
#### F2.3 [P1-3] 凭据安全化
|
||||
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/appsettings.json`
|
||||
- `NodeToken` → `null`, 加注释 "生产环境由 SECMPS_GATEWAY_TOKEN 环境变量注入"
|
||||
- Owl `Password` → `""`, 加注释
|
||||
- KMS `ClientSecret` → `""`, 加注释
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`
|
||||
- `gwCfg["NodeToken"]` → `Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"]`
|
||||
- [ ] 编辑 `.gitignore` → 加 `**/bin/`、`**/obj/`
|
||||
- [ ] `git rm -r --cached gateway/src/IntegrationGateway.Host/bin/`
|
||||
|
||||
#### F2.4 [P1-4] 前端网关地址统一化
|
||||
|
||||
- [ ] 编辑 `web.vite/public/index.html` 的 `window.apiConfig` → 加 `gatewayUrl: 'http://localhost:5100'`
|
||||
- [ ] 编辑 `web.vite/src/views/warehouse/device_manager/base_device.vue`
|
||||
- `const GW = 'http://localhost:5100'` → `const GW = window.apiConfig.gatewayUrl || 'http://localhost:5100'`
|
||||
- [ ] 编辑 `warehouse/src/api/gateway.ts`
|
||||
- `const GW_BASE = 'http://localhost:5100'` → 读取 `window.apiConfig.gatewayUrl`
|
||||
- [ ] 编辑 `warehouse/index.html` 的 `window.apiConfig` → 加 `gatewayUrl`
|
||||
|
||||
#### F2.5 [P1-5] 规则引擎增加 DeviceId 映射
|
||||
|
||||
- [ ] 在规则引擎实现方案中增加 `BuildDeviceMappingAsync` 方法:
|
||||
```csharp
|
||||
var deviceIds = rules.SelectMany(r => r.Conditions).Select(c => c.DeviceId).Distinct();
|
||||
var devices = await _deviceRepo.FindAsync(d => deviceIds.Contains(d.DeviceId));
|
||||
var map = devices.ToDictionary(d => d.DeviceId, d => (d.AdapterCode, d.SourceId));
|
||||
```
|
||||
- [ ] 后续调网关时用 `map[cond.DeviceId]` 拼装 URL
|
||||
|
||||
#### F2.6 [P1-6] 新建 warehouse_variable 表
|
||||
|
||||
- [ ] 执行 SQL:
|
||||
```sql
|
||||
CREATE TABLE warehouse_variable (
|
||||
VariableId INT IDENTITY PRIMARY KEY,
|
||||
DeviceId INT NOT NULL,
|
||||
VariableName NVARCHAR(255), -- 温度/湿度/人数
|
||||
PointIndex INT DEFAULT 0, -- MC4 pointIndex
|
||||
Unit NVARCHAR(50), -- ℃/%/人
|
||||
SortOrder INT DEFAULT 0
|
||||
);
|
||||
```
|
||||
- [ ] 在 Vol.Pro 代码生成器选择 `warehouse_variable`,生成全套 CRUD 代码
|
||||
- [ ] 管理端字典 "变量列表" 绑定到 `warehouse_variable.VariableName`
|
||||
- [ ] 规则条件/动作的 `ValueId` 下拉框改为从 `warehouse_variable` 查询(JOIN `base_device.DeviceId`)
|
||||
|
||||
> **F2 提交点**: `Fix-P1: B4-batch+批量离线+凭据安全+前端地址+DeviceId映射+变量表`
|
||||
|
||||
---
|
||||
|
||||
## 阶段 F3: P2 改善项修复(预计 2h)
|
||||
|
||||
#### F3.1 [P2-1] 适配器异常日志
|
||||
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.Owl/OwlAdapter.cs`
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs`
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Adapters.Kms/KmsAdapter.cs`
|
||||
- [ ] 所有 `catch { return false; }` → `catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck: {ex.Message}"); return false; }`
|
||||
- [ ] `dotnet build` → 0 错误
|
||||
|
||||
#### F3.2 [P2-2] 规则引擎滞后窗
|
||||
|
||||
- [ ] 在 `warehouse_rulecondition` 表新增字段:
|
||||
```sql
|
||||
ALTER TABLE warehouse_rulecondition ADD
|
||||
RecoveryThreshold_Numeric DECIMAL(18,2) NULL, -- 恢复阈值(下界)
|
||||
RecoveryThreshold_Switch NVARCHAR(50) NULL; -- 恢复开关状态
|
||||
```
|
||||
- [ ] `RuleEngineService.EvaluateCondition` 中加逻辑:
|
||||
```csharp
|
||||
bool wasTriggered = cond.LastTriggered.HasValue;
|
||||
if (wasTriggered)
|
||||
return Compare(actualValue, "大于等于", cond.RecoveryThreshold_Numeric);
|
||||
else
|
||||
return Compare(actualValue, cond.CompareOperator, cond.TargetValue_Number);
|
||||
```
|
||||
|
||||
#### F3.3 [P2-3] 条件级冷却
|
||||
|
||||
- [ ] `warehouse_rulecondition` 表新增 `LastTriggered DATETIME NULL`、`LastTriggerValue DECIMAL(18,2) NULL`
|
||||
- [ ] `RuleEngineService.EvaluateCondition` 中:
|
||||
- 如果 `DateTime.Now - cond.LastTriggered < rule.CooldownSec` → 跳过此条件
|
||||
- 触发时更新 `LastTriggered` 和 `LastTriggerValue`
|
||||
|
||||
#### F3.4 [P2-4] 生产环境移除 console.log
|
||||
|
||||
- [ ] 编辑 `warehouse/vite.config.ts`
|
||||
```typescript
|
||||
build: {
|
||||
terserOptions: { compress: { drop_console: true } }
|
||||
}
|
||||
```
|
||||
- [ ] 开发环境保留 `console.log`(仅 build 时移除)
|
||||
- [ ] `npm run build` → 确认无 console.log 残留
|
||||
|
||||
#### F3.5 [P2-5] 统一 gateway API 封装
|
||||
|
||||
- [ ] 复制 `warehouse/src/api/gateway.ts` → `web.vite/src/api/gateway.js`
|
||||
- 修改 `GW_BASE` 为 `window.apiConfig.gatewayUrl || 'http://localhost:5100'`
|
||||
- [ ] `web.vite/src/views/warehouse/device_manager/base_device.vue`
|
||||
- 删除内联 `const GW =` + `fetch()` → 改为 `import { gwGet, gwPost } from '@/api/gateway.js'`
|
||||
- 所有 `fetch(\`\${GW}/api/gateway/...\`)` → `gwGet(...)` / `gwPost(...)`
|
||||
|
||||
> **F3 提交点**: `Fix-P2: 异常日志+滞后窗+条件冷却+console清理+API统一`
|
||||
|
||||
---
|
||||
|
||||
## 阶段 F4: P3 优化项(预计 1h)
|
||||
|
||||
#### F4.1 [P3-1] 规则引擎并发动作执行
|
||||
|
||||
- [ ] 在规则引擎实现方案的 `ExecuteActionsAsync` 中:
|
||||
```csharp
|
||||
var tasks = actions.Select(a => ExecuteSingleActionAsync(a, rule));
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
async Task ExecuteSingleActionAsync(Action a, Rule r) {
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
try { await DoAction(a, r, cts.Token); }
|
||||
catch (OperationCanceledException) { Log($"[RuleEngine] 动作超时: {a.id}"); }
|
||||
}
|
||||
```
|
||||
|
||||
#### F4.2 [P3-2] 清理 bin/obj + .gitignore
|
||||
|
||||
- [ ] `.gitignore` 追加规则(如未在 F2.3 中完成):
|
||||
```
|
||||
**/bin/
|
||||
**/obj/
|
||||
gateway/src/IntegrationGateway.Host/bin/
|
||||
api_sqlsugar/**/bin/
|
||||
```
|
||||
- [ ] `git rm -r --cached` 所有 bin/obj 目录
|
||||
|
||||
#### F4.3 [P3-3] 网关 Swagger
|
||||
|
||||
- [ ] 编辑 `gateway/src/IntegrationGateway.Host/Program.cs`:
|
||||
```csharp
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
// ...
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
```
|
||||
- [ ] 浏览器访问 `http://localhost:5100/swagger` 验证
|
||||
|
||||
#### F4.4 [P3-4] 同步设计文档路由数
|
||||
|
||||
- [ ] 编辑 `doc/设计文档/对接网关设计文档.md` → 路由表从 14 条更新为当前实际数
|
||||
- [ ] 确认以下设计文档一致: 对接网关设计文档、规则引擎方案、KMS 设计文档
|
||||
|
||||
> **F4 提交点**: `Fix-P3: 并发动作+清理bin+Swagger+文档同步`
|
||||
|
||||
---
|
||||
|
||||
## 任务总览
|
||||
|
||||
| 编号 | 问题 | 涉及文件 | 预计 |
|
||||
|:---:|------|------|:---:|
|
||||
| P0-1 | RealtimePollJob 空壳 | RealtimePollJob.cs | 1h |
|
||||
| P0-2 | A1 自注册 | Program.cs | 30min |
|
||||
| P0-3 | B 组认证 | Program.cs + appsettings | 30min |
|
||||
| P1-1 | B4-batch | Program.cs | 30min |
|
||||
| P1-2 | 批量离线 | HeartbeatMonitorJob.cs | 20min |
|
||||
| P1-3 | 凭据安全 | appsettings + .gitignore + bin | 20min |
|
||||
| P1-4 | 前端地址 | base_device.vue + gateway.ts | 20min |
|
||||
| P1-5 | DeviceId 映射 | RuleEngineService | 30min |
|
||||
| P1-6 | 变量表 | SQL + 代码生成 + 前端 | 1h |
|
||||
| P2-1 | 异常日志 | OwlAdapter + MC4Adapter + KmsAdapter | 20min |
|
||||
| P2-2 | 滞后窗 | SQL + RuleEngineService | 30min |
|
||||
| P2-3 | 条件冷却 | SQL + RuleEngineService | 20min |
|
||||
| P2-4 | console 清理 | vite.config.ts | 10min |
|
||||
| P2-5 | API 统一 | gateway.js + base_device.vue | 30min |
|
||||
| P3-1 | 并发动作 | RuleEngineService | 15min |
|
||||
| P3-2 | bin 清理 | .gitignore + git rm | 5min |
|
||||
| P3-3 | Swagger | Program.cs | 10min |
|
||||
| P3-4 | 文档同步 | 设计文档 | 15min |
|
||||
|
||||
> **总计**: 18 项 / 25 文件 / ~9h
|
||||
333
doc/设计文档/规则引擎实现方案_v1.0.md
Normal file
333
doc/设计文档/规则引擎实现方案_v1.0.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# 规则引擎实现方案 v1.0
|
||||
|
||||
> **版本**: 1.0
|
||||
> **日期**: 2025-05-24
|
||||
> **基准**: 现有 warehouse_rule / warehouse_rulecondition / warehouse_ruleaction 表
|
||||
> **目标**: 实现规则驱动的实时监测 → 条件比对 → 自动执行动作的完整闭环
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构决策
|
||||
|
||||
### 1.1 部署位置:集成在 Vol.Pro 框架中 ✓
|
||||
|
||||
| 维度 | Vol.Pro 集成 | 网关集成 |
|
||||
|------|:--:|:--:|
|
||||
| 规则存储 | ✅ 规则表在本库 | ❌ 需从 Vol.Pro 下发 |
|
||||
| 规则管理 UI | ✅ 已有完整 CRUD | ❌ 无管理界面 |
|
||||
| 定时调度 | ✅ 已集成 Quartz | ❌ 无调度框架 |
|
||||
| 前端推送 | ✅ SignalR Hub 已就绪 | ❌ 无推送能力 |
|
||||
| 适配器数据源 | ⚠️ 需通过 B4 轮询 | ✅ 直接调用 |
|
||||
| 状态维护 | ✅ 数据库持久化 | ❌ 网关无状态 |
|
||||
| 实现复杂度 | ⭐⭐ | ⭐⭐⭐⭐ |
|
||||
|
||||
**结论**:规则引擎部署在 Vol.Pro 端,通过网关 B 组接口获取实时数据。与网关的 5~10 秒轮询延迟对于仓储环境规则(温湿度超标、人数越限)完全可接受。
|
||||
|
||||
### 1.2 核心设计思想
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Vol.Pro 规则引擎 │
|
||||
│ │
|
||||
│ Quartz RuleEngineJob (每10秒) │
|
||||
│ │ │
|
||||
│ ├─ 1. 加载启用的规则 (warehouse_rule WHERE Enable=1) │
|
||||
│ ├─ 2. 按 AdapterCode 分组去重设备列表 │
|
||||
│ ├─ 3. 调网关 B4 批量获取实时值 │
|
||||
│ ├─ 4. 逐规则评估条件 (AND/OR 组合) │
|
||||
│ └─ 5. 条件匹配 → 执行动作链 │
|
||||
│ ├── 控制设备 → 网关 B5/B10 │
|
||||
│ ├── 生成告警 → iot_alarm 表 │
|
||||
│ └── 推送前端 → SignalR Hub │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库改动
|
||||
|
||||
### 2.1 warehouse_rule 表新增字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE warehouse_rule ADD
|
||||
Enable NVARCHAR(50) DEFAULT '启用', -- 启用/停用
|
||||
Priority INT DEFAULT 0, -- 优先级(数字越大越优先)
|
||||
LastEvaluated DATETIME NULL, -- 上次评估时间
|
||||
LastTriggered DATETIME NULL, -- 上次触发时间
|
||||
CooldownSec INT DEFAULT 60; -- 冷却时间(秒,防止重复触发)
|
||||
```
|
||||
|
||||
### 2.2 warehouse_ruleaction 表确认字段
|
||||
|
||||
当前已含 `Alert` (生成告警/是/否) 和 `AlertMessage` (告警内容),需补充:
|
||||
|
||||
```sql
|
||||
ALTER TABLE warehouse_ruleaction ADD
|
||||
ActionType NVARCHAR(255) DEFAULT '控制', -- 动作类型: 控制/告警/通知
|
||||
ExtraJson NVARCHAR(MAX) NULL; -- 扩展JSON(如控制指令参数)
|
||||
```
|
||||
|
||||
### 2.3 新增规则执行日志表
|
||||
|
||||
```sql
|
||||
CREATE TABLE warehouse_rulelog (
|
||||
LogID INT IDENTITY PRIMARY KEY,
|
||||
RuleID INT NOT NULL, -- 关联 warehouse_rule
|
||||
ConditionMet NVARCHAR(50), -- 条件是否满足(满足/不满足)
|
||||
ActionResult NVARCHAR(MAX), -- 动作执行结果JSON
|
||||
EvaluatedAt DATETIME DEFAULT GETDATE(), -- 评估时间
|
||||
Detail NVARCHAR(MAX) NULL -- 执行详情
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 规则引擎核心设计
|
||||
|
||||
### 3.1 规则评估流程
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 规则引擎核心服务。由 Quartz RuleEngineJob 每 10s 调用一次。
|
||||
/// 顺序:
|
||||
/// 1. 加载所有启用规则(含条件和动作)
|
||||
/// 2. 从 gateway 批量获取实时值
|
||||
/// 3. 逐规则评估条件 → 触发动作 → 写日志
|
||||
/// </summary>
|
||||
public class RuleEngineService
|
||||
{
|
||||
// 注入
|
||||
private readonly IHttpClientFactory _httpClient;
|
||||
private readonly ISignalRHub _hub; // 前端推送
|
||||
private readonly Iwarehouse_ruleRepository _ruleRepo;
|
||||
|
||||
public async Task EvaluateAllAsync()
|
||||
{
|
||||
var rules = await LoadEnabledRulesAsync(); // 1. 加载规则
|
||||
var adapters = rules.SelectMany(r => r.AdapterCodes).Distinct();
|
||||
var realtimeData = await BatchFetchRealtimeAsync(adapters); // 2. 批量取实时值
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (await EvaluateRuleAsync(rule, realtimeData)) // 3. 评估条件
|
||||
{
|
||||
await ExecuteActionsAsync(rule); // 4. 执行动作
|
||||
}
|
||||
await LogEvaluationAsync(rule); // 5. 写日志
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 条件评估模型
|
||||
|
||||
```csharp
|
||||
/// <summary>评估单条规则的所有条件</summary>
|
||||
async Task<bool> EvaluateRuleAsync(Rule rule, Dictionary<string, List<PointValue>> realtimeData)
|
||||
{
|
||||
// 从 realtimeData 中查找每个条件的实际值
|
||||
var results = new List<bool>();
|
||||
foreach (var cond in rule.Conditions)
|
||||
{
|
||||
var actualValue = FindValue(realtimeData, cond.DeviceId, cond.ValueId);
|
||||
bool met = Compare(actualValue, cond.CompareOperator, cond.TargetValue);
|
||||
results.Add(met);
|
||||
}
|
||||
|
||||
// 按 JudgmentMode 组合条件结果
|
||||
return rule.JudgmentMode == "AND"
|
||||
? results.All(r => r)
|
||||
: results.Any(r => r);
|
||||
}
|
||||
|
||||
double? FindValue(Dictionary<...> data, int deviceId, int valueId)
|
||||
{
|
||||
// 从批量取回的数据中定位对应设备+变量的实时值
|
||||
// 映射: valueId → 适配器点位索引
|
||||
}
|
||||
|
||||
bool Compare(double? actual, string op, double target) => op switch
|
||||
{
|
||||
"大于" => (actual ?? double.MinValue) > target,
|
||||
"小于" => (actual ?? double.MaxValue) < target,
|
||||
"等于" => actual == target,
|
||||
"大于等于" => (actual ?? double.MinValue) >= target,
|
||||
"小于等于" => (actual ?? double.MaxValue) <= target,
|
||||
"不等于" => actual != target,
|
||||
_ => false
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 动作执行模型
|
||||
|
||||
```csharp
|
||||
/// <summary>按优先级执行规则的所有动作</summary>
|
||||
async Task ExecuteActionsAsync(Rule rule)
|
||||
{
|
||||
// 冷却检查:防止重复触发
|
||||
if (rule.LastTriggered.HasValue &&
|
||||
(DateTime.Now - rule.LastTriggered.Value).TotalSeconds < rule.CooldownSec)
|
||||
return;
|
||||
|
||||
foreach (var action in rule.Actions.OrderByDescending(a => a.Priority))
|
||||
{
|
||||
switch (action.Type)
|
||||
{
|
||||
case "控制":
|
||||
// 调网关 B5 或 B10 向目标设备发控制指令
|
||||
// 例如: POST /api/gateway/realtime/MC4:31ku/control { deviceId, pointIndex, value }
|
||||
await ControlDeviceAsync(action);
|
||||
break;
|
||||
|
||||
case "告警":
|
||||
if (action.Alert == "是")
|
||||
{
|
||||
// 写入 iot_alarm 表
|
||||
await CreateAlarmAsync(rule, action);
|
||||
}
|
||||
break;
|
||||
|
||||
case "通知":
|
||||
// 通过 SignalR 推送前端弹窗
|
||||
await _hub.SendAsync("RuleTriggered", new { rule.Title, action.AlertMessage });
|
||||
break;
|
||||
}
|
||||
}
|
||||
rule.LastTriggered = DateTime.Now;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 定时调度
|
||||
|
||||
### 4.1 新增 RuleEngineJob
|
||||
|
||||
```csharp
|
||||
/// <summary>规则引擎定时任务。挂载到 Vol.Pro Quartz 调度。</summary>
|
||||
public class RuleEngineJob : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var engine = ServiceProvider.GetService<RuleEngineService>();
|
||||
await engine.EvaluateAllAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Quartz 配置
|
||||
|
||||
在 Vol.Pro 管理端 → Quartz 管理 → 新建 Job:
|
||||
|
||||
```
|
||||
JobName: RuleEngineJob
|
||||
Cron: 0/10 * * * * ? (每 10 秒)
|
||||
ClassName: Warehouse.Services.RuleEngineJob
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据字典补充
|
||||
|
||||
| 字典键 | 字典值 | 用途 |
|
||||
|------|------|------|
|
||||
| 条件判断方式 | AND / OR | warehouse_rule.JudgmentMode |
|
||||
| 比较运算 | 大于 / 小于 / 等于 / 大于等于 / 小于等于 / 不等于 | warehouse_rulecondition.CompareOperator |
|
||||
| 比对类型 | 数值 / 开关状态 / 字符串 | warehouse_rulecondition.Type |
|
||||
| 开关状态 | 开 / 关 | TargetValue_Switch |
|
||||
| 动作类型 | 控制 / 告警 / 通知 | warehouse_ruleaction.ActionType |
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端改动
|
||||
|
||||
### 6.1 规则管理页增强
|
||||
|
||||
基于现有 `warehouse_rule.vue`(已在管理端运行),无需重建页面。仅优化表单绑定:
|
||||
- 条件表格中"设备"列绑定到 `allDevices` 动态字典
|
||||
- "变量"列绑定到对应变量的动态字典
|
||||
- 动作表格中"生成告警"列改为 select(是/否)
|
||||
- 新增 `ExtraJson` 字段(高级模式)提供 JSON 编辑器
|
||||
|
||||
### 6.2 前端告警接收
|
||||
|
||||
```javascript
|
||||
// warehouse 大屏端 - SignalR 订阅规则推送
|
||||
connection.on("RuleTriggered", (data) => {
|
||||
showMessage({
|
||||
title: data.title,
|
||||
type: "alarm",
|
||||
content: data.alertMessage
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Gateway 端配套
|
||||
|
||||
### 7.1 现有接口即用
|
||||
|
||||
| 规则需求 | 网关接口 | 状态 |
|
||||
|------|------|:--:|
|
||||
| 获取 MC4 IoT 实时值 | `GET /api/gateway/realtime/{adapter}/{deviceId}` (B4) | ✅ 已实现 |
|
||||
| 向 MC4 设备发控制指令 | `POST /api/gateway/realtime/{adapter}/control` (B5) | ✅ 已实现 |
|
||||
| 获取 Owl 人数统计 | `GET /api/gateway/devices?adapter=Owl:main` (B2) | ✅ 已实现 |
|
||||
| 远程开门/道闸 | `POST /api/gateway/control/{adapter}` (B10) | ✅ 已实现 |
|
||||
|
||||
### 7.2 新增:批量实时值查询(可选优化)
|
||||
|
||||
当前 B4 需要逐设备调,M 条规则 × N 个条件会导致过多 HTTP 调用。建议新增批量接口:
|
||||
|
||||
```
|
||||
POST /api/gateway/realtime/{adapter}/batch
|
||||
Body: { "deviceIds": ["sid1", "sid2", ...] }
|
||||
Return: { "sid1": [{pointIndex, value}], "sid2": [{...}] }
|
||||
```
|
||||
|
||||
此接口为**可选优化**。初期可直接逐设备调 B4(每设备 ~100ms,10 设备 = 1s,可接受)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施计划
|
||||
|
||||
| 阶段 | 内容 | 涉及文件 | 预计 |
|
||||
|:---:|------|------|:---:|
|
||||
| R1 | 补充数据库表字段 + 新增 warehouse_rulelog 表 + 建字典 | SQL + 管理端 | 1h |
|
||||
| R2 | 实现 RuleEngineService (规则评估 + 动作执行) | 1 个新文件 | 3h |
|
||||
| R3 | 实现 RuleEngineJob + Quartz 注册 | 1 文件 + 管理端配置 | 30min |
|
||||
| R4 | 网关批量实时值接口 (B4-batch) | Program.cs | 30min |
|
||||
| R5 | 规则管理页 UI 增强(动态字典绑定) | warehouse_rule.vue | 1h |
|
||||
| R6 | warehouse 大屏端 SignalR 规则推送接收 | warehouse DataView.vue | 30min |
|
||||
| R7 | 联调验证 | — | 2h |
|
||||
| **合计** | — | **8 文件** | **~8.5h** |
|
||||
|
||||
---
|
||||
|
||||
## 9. 规则示例
|
||||
|
||||
**示例 1:温湿度超标自动开空调**
|
||||
|
||||
```
|
||||
规则标题: 库房31温度过高自动开空调
|
||||
判断方式: AND
|
||||
条件:
|
||||
- 设备: MC4:31ku/温湿度变送器1 | 变量: 温度 | 比较: 大于 | 目标值: 28
|
||||
动作:
|
||||
- 设备: MC4:31ku/空调控制器1 | 动作类型: 控制 | 目标值开关: 开
|
||||
```
|
||||
|
||||
**示例 2:摄像机人数越限告警**
|
||||
|
||||
```
|
||||
规则标题: 仓库A区人数超限告警
|
||||
判断方式: AND
|
||||
条件:
|
||||
- 设备: Owl:main/摄像头1 | 变量: 人数统计 | 比较: 大于等于 | 目标值: 50
|
||||
动作:
|
||||
- 动作类型: 告警 | 生成告警: 是 | 告警内容: "仓库A区人数已达{value}人,请立即检查"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **结论**: 规则引擎作为 Vol.Pro 内部 Quartz Job 运行,每 10s 拉网关数据评估,零网关状态改造。8 个文件 ~8.5h 可完成闭环。
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="nuget.org_v2" value="https://www.nuget.org/api/v2/" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
||||
@@ -61,7 +61,7 @@ public class KmsAdapter : IHasFlatDevices, IHasAlarms, IAcceptsControl, IHasBusi
|
||||
var resp = await client.GetAsync("/prod-api/heartBeat");
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
@@ -57,7 +57,7 @@ public class Mc4Adapter : IHasOwnDeviceTree, IHasPoints, IHasAlarms
|
||||
var resp = await client.PostAsync("/api/central/auth/conf/get", null);
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
@@ -60,7 +60,7 @@ public class OwlAdapter : IHasFlatDevices, IHasStreams, IHasRecordings, IAccepts
|
||||
var resp = await client.GetAsync("/health");
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[{AdapterCode}] HealthCheck 失败: {ex.Message}"); return false; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
<ProjectReference Include="..\IntegrationGateway.Adapters.Kms\IntegrationGateway.Adapters.Kms.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -14,6 +14,9 @@ using IntegrationGateway.Core.Models;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// ── 注册 HttpClient 工厂 ──
|
||||
// 命名客户端 "VolPro":用于调用 Vol.Pro A 组接口和适配器内部 HTTP 请求
|
||||
// 连接池:最多 10 个并发连接,5 分钟生命周期
|
||||
@@ -30,6 +33,9 @@ builder.Services.AddHttpClient("VolPro", c =>
|
||||
builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
// ── 读取配置 ──
|
||||
@@ -83,6 +89,22 @@ var adapterTypes = string.Join(",", registry.All.Select(a => a.AdapterCode));
|
||||
await registry.InitializeAllAsync();
|
||||
Console.WriteLine($"[Gateway] {registry.All.Count} 个适配器已注册: {adapterTypes}");
|
||||
|
||||
// ── A1: 向 Vol.Pro 注册当前网关节点 ──
|
||||
var nodeCode = gwCfg["NodeCode"] ?? "gw-default";
|
||||
var nodeToken = Environment.GetEnvironmentVariable("SECMPS_GATEWAY_TOKEN") ?? gwCfg["NodeToken"] ?? "";
|
||||
try
|
||||
{
|
||||
var registerReq = new GatewayRegisterRequest
|
||||
{
|
||||
NodeCode = nodeCode, Token = nodeToken,
|
||||
AdapterTypes = adapterTypes,
|
||||
BaseUrl = $"http://localhost:{app.Urls.FirstOrDefault()?.Split(':').LastOrDefault() ?? "5100"}"
|
||||
};
|
||||
var registerResult = await clientFactory.RegisterAsync(registerReq);
|
||||
Console.WriteLine($"[Gateway] A1 注册完成: nodeCode={nodeCode}, adapters={adapterTypes}");
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine($"[Gateway] A1 注册失败: {ex.Message}"); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// B 组路由(管理端 / Vol.Pro → 网关)
|
||||
// 所有路由通过适配器编码查找对应适配器,按能力接口分发请求
|
||||
@@ -162,6 +184,17 @@ app.MapGet("/api/gateway/realtime/{adapter}/{deviceId}", async (string adapter,
|
||||
return Results.Ok(await a.GetRealtimeValuesAsync(deviceId));
|
||||
});
|
||||
|
||||
// B4-batch: 批量实时点位值 — 一次请求返回多个设备的值
|
||||
app.MapPost("/api/gateway/realtime/{adapter}/batch", async (string adapter, BatchRealtimeRequest req) =>
|
||||
{
|
||||
var a = registry.FindByCode<IHasPoints>(adapter);
|
||||
if (a == null) return Results.NotFound(new { error = "CAPABILITY_NOT_SUPPORTED" });
|
||||
var results = new Dictionary<string, List<PointValue>>();
|
||||
foreach (var deviceId in req.DeviceIds ?? new())
|
||||
try { results[deviceId] = await a.GetRealtimeValuesAsync(deviceId); } catch { }
|
||||
return Results.Ok(results);
|
||||
});
|
||||
|
||||
// B5: 设备控制 — 向 IoT 设备下发控制指令
|
||||
app.MapPost("/api/gateway/realtime/{adapter}/control", async (string adapter, ControlRequest req) =>
|
||||
{
|
||||
@@ -303,6 +336,7 @@ record PtzRequest(string? Direction, string Action, float Speed);
|
||||
/// <param name="PointIndex">点位索引</param>
|
||||
/// <param name="Value">目标值</param>
|
||||
record ControlRequest(string? DeviceId, int PointIndex, double Value);
|
||||
record BatchRealtimeRequest(List<string>? DeviceIds);
|
||||
record GatewayControlRequest(string? DeviceId, string? Command, Dictionary<string, object?>? Parameters);
|
||||
record SyncRequest(string? DataType, List<object>? Items);
|
||||
record SyncDeleteRequest(string? DataType, List<string>? Ids);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<DeleteExistingFiles>false</DeleteExistingFiles>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<PublishProvider>FileSystem</PublishProvider>
|
||||
<PublishUrl>bin\Release\net8.0\publish\</PublishUrl>
|
||||
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
|
||||
<ProjectGuid>6d56687b-d31c-6b60-3205-966929747297</ProjectGuid>
|
||||
<SelfContained>true</SelfContained>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -24,7 +24,9 @@
|
||||
"NodeCode": "gw-31ku",
|
||||
"NodeToken": "changeme",
|
||||
"HeartbeatIntervalSec": 15,
|
||||
"AdapterInitTimeoutSec": 30
|
||||
"AdapterInitTimeoutSec": 30,
|
||||
"GatewayKey": null,
|
||||
"_comment_NodeToken": "生产环境由 SECMPS_GATEWAY_TOKEN 环境变量注入"
|
||||
},
|
||||
"KMS": [
|
||||
{
|
||||
|
||||
13
gateway/src/IntegrationGateway.Host/dotnet-tools.json
Normal file
13
gateway/src/IntegrationGateway.Host/dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.8",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
window.apiConfig = {
|
||||
// 根据环境变量配置不同的基础URL
|
||||
baseURL: {
|
||||
development: 'http://localhost:9100/',
|
||||
debug: 'http://localhost:9100/',
|
||||
production: 'http://localhost:9100/'
|
||||
development: 'http://192.168.3.108:9100/',
|
||||
debug: 'http://192.168.3.108:9100/',
|
||||
production: 'http://192.168.3.108:9100/'
|
||||
},
|
||||
// 大屏地址配置
|
||||
dataViewUrl: {
|
||||
development: 'http://localhost:9200/',
|
||||
debug: 'http://localhost:9200/', // 调试环境也使用相同配置
|
||||
production: 'http://localhost:9200/'
|
||||
development: 'http://192.168.3.108:9200/',
|
||||
debug: 'http://192.168.3.108:9200/', // 调试环境也使用相同配置
|
||||
production: 'http://192.168.3.108:9200/'
|
||||
},
|
||||
// API路径配置
|
||||
apiPaths: {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 所有网关 API 调用使用 fetch() 直连,不走 Vol.Pro 后端中转
|
||||
*/
|
||||
|
||||
const GW_BASE = 'http://localhost:5100'
|
||||
const GW_BASE = 'http://192.168.3.108:5100'
|
||||
|
||||
/** GET 请求网关 */
|
||||
export async function gwGet(url: string): Promise<any> {
|
||||
|
||||
@@ -22,7 +22,7 @@ if (apiConfig.baseURL && apiConfig.baseURL[env]) {
|
||||
axios.defaults.baseURL = apiConfig.baseURL[env];
|
||||
} else {
|
||||
// 降级方案,确保系统能正常运行
|
||||
axios.defaults.baseURL = 'http://localhost:9100/';
|
||||
axios.defaults.baseURL = 'http://192.168.3.108:9100/';
|
||||
}
|
||||
|
||||
// 设置大屏地址
|
||||
@@ -30,10 +30,10 @@ if (apiConfig.dataViewUrl && apiConfig.dataViewUrl[env]) {
|
||||
dataViewUrl = apiConfig.dataViewUrl[env];
|
||||
} else {
|
||||
// 降级方案,确保系统能正常运行
|
||||
dataViewUrl = 'http://localhost:9200/';
|
||||
dataViewUrl = 'http://192.168.3.108:9200/';
|
||||
}
|
||||
//后台接口地址
|
||||
//axios.defaults.baseURL = 'http://localhost:9100/'
|
||||
//axios.defaults.baseURL = 'http://192.168.3.108:9100/'
|
||||
if (!axios.defaults.baseURL.endsWith('/')) {
|
||||
axios.defaults.baseURL += '/'
|
||||
}
|
||||
|
||||
126
web.vite/src/api/gateway.js
Normal file
126
web.vite/src/api/gateway.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 网关 B 组接口封装
|
||||
* 所有网关 API 调用使用 fetch() 直连,不走 Vol.Pro 后端中转
|
||||
*/
|
||||
|
||||
const GW_BASE = 'http://192.168.3.108:5100'
|
||||
|
||||
/** GET 请求网关 */
|
||||
export async function gwGet(url: string): Promise<any> {
|
||||
const resp = await fetch(`${GW_BASE}${url}`)
|
||||
if (!resp.ok) throw new Error(`网关请求失败: ${resp.status}`)
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
/** POST 请求网关 */
|
||||
export async function gwPost(url: string, body?: object): Promise<any> {
|
||||
const resp = await fetch(`${GW_BASE}${url}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
})
|
||||
if (!resp.ok) throw new Error(`网关请求失败: ${resp.status}`)
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 数据模型
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/** 统一设备模型(对应网关 StandardDevice) */
|
||||
export interface StandardDevice {
|
||||
deviceId: number
|
||||
adapterCode: string
|
||||
sourceId: string
|
||||
name: string
|
||||
category: string
|
||||
group: string
|
||||
isParent: boolean
|
||||
parentSourceId?: string
|
||||
isOnline: boolean
|
||||
ipAddress?: string
|
||||
port?: number
|
||||
extra?: Record<string, any>
|
||||
}
|
||||
|
||||
/** 摄像机视图模型 */
|
||||
export interface Camera {
|
||||
id: string
|
||||
name: string
|
||||
location: string
|
||||
status: 'online' | 'offline'
|
||||
adapterCode: string
|
||||
sourceId: string
|
||||
streamUrl?: string
|
||||
hasPtz: boolean
|
||||
}
|
||||
|
||||
/** 视频流地址集合 */
|
||||
export interface StreamUrls {
|
||||
wsFlv?: string
|
||||
httpFlv?: string
|
||||
hls?: string
|
||||
webRtc?: string
|
||||
rtmp?: string
|
||||
rtsp?: string
|
||||
}
|
||||
|
||||
/** 分页容器 */
|
||||
export interface PagedResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/** 实时点位值 */
|
||||
export interface PointValue {
|
||||
sourceDeviceId: string
|
||||
pointIndex: number
|
||||
value: number
|
||||
updateTime?: string
|
||||
interval: number
|
||||
}
|
||||
|
||||
/** 告警模型 */
|
||||
export interface StandardAlarm {
|
||||
alarmId: string
|
||||
deviceId?: string
|
||||
adapterCode: string
|
||||
level: string
|
||||
title: string
|
||||
content?: string
|
||||
occurTime: string
|
||||
status: string
|
||||
actualValue?: number
|
||||
thresholdValue?: number
|
||||
}
|
||||
|
||||
/** 设备树节点 */
|
||||
export interface DeviceTreeNode {
|
||||
id: number
|
||||
sourceId: string
|
||||
name: string
|
||||
type: number
|
||||
objectType: number
|
||||
tag?: string
|
||||
option?: Record<string, any>
|
||||
children: DeviceTreeNode[]
|
||||
}
|
||||
|
||||
/** StandardDevice → Camera 映射 */
|
||||
export function toCamera(d: StandardDevice): Camera {
|
||||
return {
|
||||
id: d.sourceId,
|
||||
name: d.name,
|
||||
location: d.extra?.location || '',
|
||||
status: d.isOnline ? 'online' : 'offline',
|
||||
adapterCode: d.adapterCode,
|
||||
sourceId: d.sourceId,
|
||||
hasPtz: d.extra?.hasPtz === '1' || d.extra?.hasPtz === true
|
||||
}
|
||||
}
|
||||
|
||||
/** 从网关获取设备列表并映射为 Camera[] */
|
||||
export async function fetchCameras(adapter: string): Promise<Camera[]> {
|
||||
const data = await gwGet(`/api/gateway/devices?adapter=${adapter}&page=1&size=100`)
|
||||
return (data.items || []).map(toCamera)
|
||||
}
|
||||
@@ -82,7 +82,7 @@
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { table, editFormFields, editFormOptions, searchFormFields, searchFormOptions, columns, detail, details } = reactive(viewOptions())
|
||||
|
||||
const GW = 'http://localhost:5100'
|
||||
const GW = window.apiConfig?.gatewayUrl || 'http://localhost:5100'
|
||||
let gridRef, _timer;
|
||||
|
||||
//── 对话框状态 ──
|
||||
|
||||
Reference in New Issue
Block a user