V3 基础设施: GatewayClient+3个Quartz Job+Startup注册

This commit is contained in:
2026-05-17 10:41:59 +08:00
parent 4546d57a7c
commit 12ace53650
6 changed files with 221 additions and 0 deletions

View File

@@ -69,6 +69,14 @@ namespace VolPro.WebApi
services.AddSession();
services.AddMemoryCache();
services.AddHttpContextAccessor();
// ── 网关集成: HttpClient + GatewayClient 注册 ──
services.AddHttpClient("VolPro", c =>
{
c.Timeout = TimeSpan.FromSeconds(30);
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
services.AddSingleton<VolPro.Warehouse.Services.GatewayClient>();
services.AddMvc(options =>
{
options.Filters.Add(typeof(ApiAuthorizeFilter));

View File

@@ -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;
}
}

View File

@@ -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} 台设备离线");
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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}");
}
}
}
}