V3 基础设施: GatewayClient+3个Quartz Job+Startup注册
This commit is contained in:
@@ -69,6 +69,14 @@ namespace VolPro.WebApi
|
|||||||
services.AddSession();
|
services.AddSession();
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
// ── 网关集成: HttpClient + GatewayClient 注册 ──
|
||||||
|
services.AddHttpClient("VolPro", c =>
|
||||||
|
{
|
||||||
|
c.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
c.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||||
|
});
|
||||||
|
services.AddSingleton<VolPro.Warehouse.Services.GatewayClient>();
|
||||||
|
|
||||||
services.AddMvc(options =>
|
services.AddMvc(options =>
|
||||||
{
|
{
|
||||||
options.Filters.Add(typeof(ApiAuthorizeFilter));
|
options.Filters.Add(typeof(ApiAuthorizeFilter));
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace VolPro.Warehouse.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 网关 HTTP 客户端。封装 Vol.Pro 调用 IntegrationGateway B 组接口的逻辑。
|
||||||
|
/// 所有对网关的请求统一经此类发出,便于连接池管理和错误处理。
|
||||||
|
/// </summary>
|
||||||
|
public class GatewayClient
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public GatewayClient(IHttpClientFactory httpFactory, IConfiguration config)
|
||||||
|
{
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>创建带超时和默认头的 HttpClient</summary>
|
||||||
|
private HttpClient CreateClient()
|
||||||
|
{
|
||||||
|
var client = _httpFactory.CreateClient("VolPro");
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B3: 手动触发网关全量设备同步。
|
||||||
|
/// POST {baseUrl}/api/gateway/devices/sync?adapter={adapterTypes}
|
||||||
|
/// </summary>
|
||||||
|
public async Task<JsonDocument?> TriggerFullSyncAsync(string baseUrl, string adapterTypes)
|
||||||
|
{
|
||||||
|
var http = CreateClient();
|
||||||
|
var resp = await http.PostAsync(
|
||||||
|
$"{baseUrl.TrimEnd('/')}/api/gateway/devices/sync?adapter={Uri.EscapeDataString(adapterTypes)}",
|
||||||
|
null);
|
||||||
|
if (!resp.IsSuccessStatusCode) return null;
|
||||||
|
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B4: 获取设备实时点位值。
|
||||||
|
/// GET {baseUrl}/api/gateway/realtime/{adapter}/{deviceId}
|
||||||
|
/// </summary>
|
||||||
|
public async Task<JsonDocument?> GetRealtimeAsync(string baseUrl, string adapter, string deviceId)
|
||||||
|
{
|
||||||
|
var http = CreateClient();
|
||||||
|
var resp = await http.GetAsync(
|
||||||
|
$"{baseUrl.TrimEnd('/')}/api/gateway/realtime/{Uri.EscapeDataString(adapter)}/{Uri.EscapeDataString(deviceId)}");
|
||||||
|
if (!resp.IsSuccessStatusCode) return null;
|
||||||
|
return await resp.Content.ReadFromJsonAsync<JsonDocument>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B5: 设备反向控制。
|
||||||
|
/// POST {baseUrl}/api/gateway/realtime/{adapter}/control
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> ControlDeviceAsync(string baseUrl, string adapter, string deviceId, int pointIndex, double value)
|
||||||
|
{
|
||||||
|
var http = CreateClient();
|
||||||
|
var resp = await http.PostAsJsonAsync(
|
||||||
|
$"{baseUrl.TrimEnd('/')}/api/gateway/realtime/{adapter}/control",
|
||||||
|
new { deviceId, pointIndex, value });
|
||||||
|
return resp.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Quartz;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Warehouse.IServices;
|
||||||
|
using VolPro.Entity.DomainModels;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace VolPro.Warehouse.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 心跳超时检测任务。扫描心跳超时 30 秒的网关节点,标记为离线,
|
||||||
|
/// 并级联标记该节点下所有设备为离线。
|
||||||
|
/// Cron 建议: 每 15 秒 ("0/15 * * * * ?")
|
||||||
|
/// </summary>
|
||||||
|
public class HeartbeatMonitorJob : IJob
|
||||||
|
{
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
|
||||||
|
var gwSvc = sp.GetService<Igateway_nodesService>();
|
||||||
|
var devSvc = sp.GetService<Ibase_deviceService>();
|
||||||
|
if (gwSvc == null) return;
|
||||||
|
|
||||||
|
var timeout = DateTime.Now.AddSeconds(-30);
|
||||||
|
|
||||||
|
// 扫描心跳超时的网关(当前在线但心跳超时)
|
||||||
|
var offlineNodes = await gwSvc.FindAsIQueryable(
|
||||||
|
x => x.IsOnline == "在线" && x.LastHeartbeat < timeout)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var node in offlineNodes)
|
||||||
|
{
|
||||||
|
// 标记网关离线
|
||||||
|
node.IsOnline = "离线";
|
||||||
|
await gwSvc.FindAsIQueryable(x => x.NodeId == node.NodeId)
|
||||||
|
.FirstAsync(); // 确保实体被跟踪
|
||||||
|
// 直接通过 DbContext 更新
|
||||||
|
var dbProp = gwSvc.GetType().BaseType?.GetProperty("DbContext");
|
||||||
|
if (dbProp != null) continue; // fallback: 通过 FindAsIQueryable 重新获取更新
|
||||||
|
|
||||||
|
Console.WriteLine($"[HeartbeatMonitorJob] 网关 {node.NodeCode} 心跳超时,标记离线");
|
||||||
|
|
||||||
|
// 级联标记该网关下所有设备离线
|
||||||
|
if (devSvc != null)
|
||||||
|
{
|
||||||
|
var devices = await devSvc.FindAsIQueryable(
|
||||||
|
x => x.GatewayNodeId == node.NodeId && x.IsOnline == "在线")
|
||||||
|
.ToListAsync();
|
||||||
|
foreach (var dev in devices)
|
||||||
|
{
|
||||||
|
dev.IsOnline = "离线";
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[HeartbeatMonitorJob] 级联 {devices.Count} 台设备离线");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Quartz;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace VolPro.Warehouse.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实时数据轮询任务(Phase 2 完善)。
|
||||||
|
/// 定时轮询 MC4.0 IoT 设备实时值 → 更新 iot_devicedata。
|
||||||
|
/// Cron 建议: 每 10 秒 ("0/10 * * * * ?")
|
||||||
|
/// </summary>
|
||||||
|
public class RealtimePollJob : IJob
|
||||||
|
{
|
||||||
|
public Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
// TODO: Phase 2 — 遍历在线 MC4 网关,轮询实时值写入 iot_devicedata
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Quartz;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Warehouse.IServices;
|
||||||
|
using VolPro.Entity.DomainModels;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace VolPro.Warehouse.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定时设备同步任务。遍历所有在线且启用的网关节点,触发全量设备同步。
|
||||||
|
/// Cron 建议: 每 5 分钟 ("0 */5 * * * ?")
|
||||||
|
/// </summary>
|
||||||
|
public class SyncDevicesJob : IJob
|
||||||
|
{
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"];
|
||||||
|
var gwSvc = sp.GetService<Igateway_nodesService>();
|
||||||
|
var client = sp.GetService<GatewayClient>();
|
||||||
|
if (gwSvc == null || client == null) return;
|
||||||
|
|
||||||
|
// 遍历所有在线且启用的网关
|
||||||
|
var onlineNodes = await gwSvc.FindAsIQueryable(
|
||||||
|
x => x.IsOnline == "在线" && x.Enable == "启用" && x.BaseUrl != null)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var node in onlineNodes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 触发网关全量同步
|
||||||
|
await client.TriggerFullSyncAsync(node.BaseUrl!, node.AdapterTypes ?? "");
|
||||||
|
Console.WriteLine($"[SyncDevicesJob] 网关 {node.NodeCode} 同步触发成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[SyncDevicesJob] 网关 {node.NodeCode} 同步失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Phase V3.6: Quartz Job 注册说明
|
||||||
|
═══════════════════════════════
|
||||||
|
|
||||||
|
以下 3 个 Job 需在 Vol.Pro 管理端 → Quartz 任务管理 中手动创建:
|
||||||
|
|
||||||
|
1. SyncDevicesJob
|
||||||
|
- 类名: VolPro.Warehouse.Services.SyncDevicesJob
|
||||||
|
- Cron: 0 */5 * * * ? (每5分钟)
|
||||||
|
- 说明: 遍历在线网关触发全量设备同步
|
||||||
|
|
||||||
|
2. HeartbeatMonitorJob
|
||||||
|
- 类名: VolPro.Warehouse.Services.HeartbeatMonitorJob
|
||||||
|
- Cron: 0/15 * * * * ? (每15秒)
|
||||||
|
- 说明: 检测心跳超时标记离线
|
||||||
|
|
||||||
|
3. RealtimePollJob (Phase 2 启用)
|
||||||
|
- 类名: VolPro.Warehouse.Services.RealtimePollJob
|
||||||
|
- Cron: 0/10 * * * * ? (每10秒)
|
||||||
|
- 说明: 轮询MC4.0实时值写iot_devicedata
|
||||||
Reference in New Issue
Block a user