From 4546d57a7c3b25b1668b1f14754172f087dae423 Mon Sep 17 00:00:00 2001 From: g82tt Date: Sun, 17 May 2026 10:38:43 +0800 Subject: [PATCH] =?UTF-8?q?V2=20Controller=E6=89=A9=E5=B1=95:=20A1-A4?= =?UTF-8?q?=E7=BD=91=E5=85=B3API=20+=20GetRegionTree=20+=20GetDevicesByPoi?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Partial/base_deviceController.cs | 107 +++++++-- .../Partial/gateway_nodesController.cs | 223 ++++++++++++++++-- 2 files changed, 302 insertions(+), 28 deletions(-) diff --git a/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs b/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs index 5530382..be1d362 100644 --- a/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs +++ b/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs @@ -1,7 +1,6 @@ /* - *接口编写处... -*如果接口需要做Action的权限验证,请在Action上使用属性 -*如: [ApiActionPermission("base_device",Enums.ActionPermissionOptions.Search)] + *设备管理扩展 — 区域树 + 点位设备列表 + *所有改动在 Partial 目录,不破坏框架可升级性 */ using Microsoft.AspNetCore.Mvc; using System; @@ -11,23 +10,103 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using VolPro.Entity.DomainModels; using Warehouse.IServices; +using System.Linq; +using Microsoft.EntityFrameworkCore; namespace Warehouse.Controllers { public partial class base_deviceController { - private readonly Ibase_deviceService _service;//访问业务代码 - private readonly IHttpContextAccessor _httpContextAccessor; - - [ActivatorUtilitiesConstructor] - public base_deviceController( - Ibase_deviceService service, - IHttpContextAccessor httpContextAccessor - ) - : base(service) + /// + /// 获取区域→点位→设备树。 + /// 用于管理端左侧树形控件展示层级结构。 + /// 格式: [{ id, label, type:"region", children: [{ id, label, type:"point", deviceCount }] }] + /// + [HttpGet] + [Route("/api/DeviceManager/GetRegionTree")] + public async Task GetRegionTree() { - _service = service; - _httpContextAccessor = httpContextAccessor; + // 获取区域和点位服务 + var regionSvcType = _service.GetType().Assembly.GetType("Warehouse.Services.warehouse_regionsService"); + var pointSvcType = _service.GetType().Assembly.GetType("Warehouse.Services.warehouse_devicepointService"); + + var regionSvc = regionSvcType?.GetProperty("Instance")?.GetValue(null) as IService; + var pointSvc = pointSvcType?.GetProperty("Instance")?.GetValue(null) as IService; + + // 查所有区域 + var regions = regionSvc != null + ? await regionSvc.FindAsIQueryable(x => true).ToListAsync() + : new List(); + + // 查所有点位 + var points = pointSvc != null + ? await pointSvc.FindAsIQueryable(x => true).ToListAsync() + : new List(); + + // 统计每个点位下的设备数量 + var deviceCounts = new Dictionary(); + 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(); + foreach (var g in allDevices) + deviceCounts[g.PointId] = g.Count; + + // 构建树形结构 + var tree = new List(); + 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(); + + tree.Add(new + { + id = $"r_{region.Id}", + label = region.RegionName ?? $"区域{region.Id}", + type = "region", + deviceCount = regionChildren.Count, + children = regionChildren + }); + } + + return Ok(tree); + } + + /// + /// 获取指定点位下的设备列表(含子设备)。 + /// 支持分页参数 page 和 size。 + /// + [HttpGet] + [Route("/api/DeviceManager/GetDevicesByPoint")] + public async Task 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 }); } } } diff --git a/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs b/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs index 2e6bac6..8bc9355 100644 --- a/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs +++ b/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs @@ -1,7 +1,7 @@ /* - *接口编写处... -*如果接口需要做Action的权限验证,请在Action上使用属性 -*如: [ApiActionPermission("gateway_nodes",Enums.ActionPermissionOptions.Search)] + *网关节点管理 — A1注册/A2心跳/A3设备同步/A4告警同步 + *A组接口使用 [AllowAnonymous] + NodeToken 二次认证 + *所有改动在 Partial 目录,不破坏框架可升级性 */ using Microsoft.AspNetCore.Mvc; using System; @@ -11,23 +11,218 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using VolPro.Entity.DomainModels; using Warehouse.IServices; +using System.Linq; +using Microsoft.EntityFrameworkCore; namespace Warehouse.Controllers { public partial class gateway_nodesController { - private readonly Igateway_nodesService _service;//访问业务代码 - private readonly IHttpContextAccessor _httpContextAccessor; - - [ActivatorUtilitiesConstructor] - public gateway_nodesController( - Igateway_nodesService service, - IHttpContextAccessor httpContextAccessor - ) - : base(service) + /// A1: 网关注册(Upsert)。认证方式: NodeToken + [HttpPost] + [Route("/api/gateway/register")] + [AllowAnonymous] + public async Task RegisterGateway([FromBody] GatewayRegisterRequest req) { - _service = service; - _httpContextAccessor = httpContextAccessor; + 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 deviceSvc = _service.GetType().Assembly.GetType("Warehouse.Services.base_deviceService") + ?.GetProperty("Instance")?.GetValue(null) as Ibase_deviceService; + var devices = deviceSvc != null + ? await deviceSvc.GetDevicesByGatewayNodeAsync(node.NodeId) + : new List(); + + 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 无效" }); + } + } + + /// A2: 心跳。认证方式: NodeToken。每15秒调用一次。 + [HttpPost] + [Route("/api/gateway/heartbeat")] + [AllowAnonymous] + public async Task 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 = "认证失败" }); + } + } + + /// A3: 设备数据同步(字段分治 + parentSourceId 映射)。认证方式: NodeToken + [HttpPost] + [Route("/api/gateway/sync/devices")] + [AllowAnonymous] + public async Task 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 = "认证失败" }); + } + } + + /// A4: 告警同步(DeviceSourceId→DeviceId 映射 + SourceAlarmId 去重)。认证方式: NodeToken + [HttpPost] + [Route("/api/gateway/sync/alarms")] + [AllowAnonymous] + public async Task 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 alarmSvcType = _service.GetType().Assembly.GetType("Warehouse.Services.iot_alarmService"); + var alarmSvc = alarmSvcType?.GetProperty("Instance")?.GetValue(null) as Iiot_alarmService; + + // 批量查询 DeviceSourceId → DeviceId 映射 + var deviceSvcType = _service.GetType().Assembly.GetType("Warehouse.Services.base_deviceService"); + var deviceSvc = deviceSvcType?.GetProperty("Instance")?.GetValue(null) as Ibase_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 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 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; } = ""; + } }