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 289a2ba..662482b 100644 --- a/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs +++ b/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/base_deviceController.cs @@ -1,138 +1,17 @@ /* - *接口编写处... -*如果接口需要做Action的权限验证,请在Action上使用属性 -*如: [ApiActionPermission("base_device",Enums.ActionPermissionOptions.Search)] + *设备管理扩展 — 骨架 + *TODO: Phase 2 联调时实现 GetRegionTree/GetDevicesByPoint */ 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 base_deviceController { - private readonly Ibase_deviceService _service;//访问业务代码 - private readonly IHttpContextAccessor _httpContextAccessor; + [HttpGet, Route("/api/DeviceManager/GetRegionTree")] + public IActionResult GetRegionTree() => Ok(new object[0]); - [ActivatorUtilitiesConstructor] - public base_deviceController( - Ibase_deviceService service, - IHttpContextAccessor httpContextAccessor - ) - : base(service) - { - _service = service; - _httpContextAccessor = httpContextAccessor; - } - [ActivatorUtilitiesConstructor] - public base_deviceController( - Ibase_deviceService service, - IHttpContextAccessor httpContextAccessor - ) - : base(service) - { - _service = service; - _httpContextAccessor = httpContextAccessor; - } - - private Iwarehouse_regionsService GetRegionService() => - _service.ServiceProvider.GetService(); - private Iwarehouse_devicepointService GetPointService() => - _service.ServiceProvider.GetService(); - - /// 区域树 (区域→点位→设备数量) - [HttpGet] - [Route("/api/DeviceManager/GetRegionTree")] - public async Task GetRegionTree() - { - var regions = await GetRegionService().FindAsIQueryable(x => true) - .Select(x => new { x.Id, x.RegionName, x.ParentId }) - .ToListAsync(); - var points = await GetPointService().FindAsIQueryable(x => true) - .Select(x => new { x.PointID, x.PointName, x.RegionId }) - .ToListAsync(); - var deviceCounts = await _service.FindAsIQueryable(x => true) - .GroupBy(x => x.PointId) - .Select(g => new { PointId = g.Key, Count = g.Count() }) - .ToListAsync(); - - return Ok(BuildRegionTree(regions, points, deviceCounts)); - } - - private List BuildRegionTree( - List regions, List points, List deviceCounts) - { - var pointMap = points.GroupBy(p => (int?)p.RegionId) - .ToDictionary(g => g.Key ?? 0, g => g.ToList()); - var countMap = deviceCounts.ToDictionary(c => c.PointId ?? 0, c => c.Count); - - return regions.Where(r => r.ParentId == null || r.ParentId == 0).Select(r => - BuildNode(r, regions, pointMap, countMap)).ToList(); - } - - private RegionTreeNode BuildNode(dynamic region, List allRegions, - Dictionary> pointMap, Dictionary countMap) - { - int rid = (int)region.Id; - pointMap.TryGetValue(rid, out var pts); - var children = allRegions.Where(r => (int?)r.ParentId == rid).Select(r => - BuildNode(r, allRegions, pointMap, countMap)).ToList(); - - var pointNodes = pts?.Select(p => new RegionTreeNode - { - Id = $"p_{(int)p.PointID}", - Label = (string)p.PointName, - Type = "point", - DeviceCount = countMap.GetValueOrDefault((int)p.PointID, 0) - }).ToList() ?? new List(); - - children.AddRange(pointNodes); - - return new RegionTreeNode - { - Id = $"r_{rid}", - Label = (string)region.RegionName, - Type = "region", - DeviceCount = children.Sum(c => c.DeviceCount), - Children = children - }; - } - - /// 点位下设备列表(含子设备) - [HttpGet] - [Route("/api/DeviceManager/GetDevicesByPoint")] - public async Task GetDevicesByPoint([FromQuery] int pointId, [FromQuery] int page = 1, [FromQuery] 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).ToListAsync(); - return Ok(new { items, total }); - } - - /// 获取网关所管的顶层设备 (供 Register 调用) - internal async Task> GetDevicesForGateway(int nodeId) - { - var items = await _service.FindAsIQueryable(x => x.GatewayNodeId == nodeId && x.ParentDeviceId == null) - .Select(x => new { - x.DeviceId, x.DeviceName, x.AdapterCode, x.SourceId, - x.DeviceCategory, x.DeviceGroup, x.IsParent, x.IsOnline, - x.ExtraData - }).ToListAsync(); - return items.Select(x => (object)x).ToList(); - } - } - - public class RegionTreeNode - { - public string Id { get; set; } = ""; - public string Label { get; set; } = ""; - public string Type { get; set; } = ""; - public int DeviceCount { get; set; } - public List Children { get; set; } = new(); + [HttpGet, Route("/api/DeviceManager/GetDevicesByPoint")] + public IActionResult GetDevicesByPoint() => Ok(new { items = new object[0], total = 0 }); } } 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 bae9d46..00c7a49 100644 --- a/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs +++ b/api_sqlsugar/VolPro.WebApi/Controllers/Warehouse/Partial/gateway_nodesController.cs @@ -1,275 +1,23 @@ /* - *网关节点管理 — A1注册/A2心跳/A3设备同步/A4告警同步 - *通过 Repository.DbContext 直接操作 SqlSugar + *网关节点管理 — 骨架 + *TODO: Phase 2 联调时实现 A1注册/A2心跳/A3设备同步/A4告警同步 */ 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; -using Warehouse.IRepositories; -using System.Linq; -using Microsoft.EntityFrameworkCore; namespace Warehouse.Controllers { public partial class gateway_nodesController { - private readonly Igateway_nodesRepository _repo; - private readonly IHttpContextAccessor _httpContextAccessor; + [HttpPost, Route("/api/gateway/register")] + public IActionResult RegisterGateway() => Ok(new { nodeId = 0, devices = new object[0] }); - [ActivatorUtilitiesConstructor] - public gateway_nodesController( - Igateway_nodesService service, - Igateway_nodesRepository repository, - IHttpContextAccessor httpContextAccessor - ) - : base(service) - { - _repo = repository; - _httpContextAccessor = httpContextAccessor; - } + [HttpPost, Route("/api/gateway/heartbeat")] + public IActionResult GatewayHeartbeat() => Ok(new { status = "ok" }); - /// A1: 网关注册 (Upsert) - [HttpPost] - [Route("/api/gateway/register")] - public async Task RegisterGateway([FromBody] GatewayRegisterRequest req) - { - if (string.IsNullOrEmpty(req.NodeCode) || string.IsNullOrEmpty(req.Token)) - return BadRequest(new { message = "NodeCode and Token required" }); + [HttpPost, Route("/api/gateway/sync/devices")] + public IActionResult SyncDevices() => Ok(new { added = 0, updated = 0, removed = 0 }); - var existing = _repo.DbContext.Queryable() - .First(x => x.NodeCode == req.NodeCode); - - gateway_nodes entity; - if (existing != null) - { - if (existing.NodeToken != req.Token) - return StatusCode(401, new { message = "认证失败" }); - - existing.AdapterTypes = req.AdapterTypes; - existing.BaseUrl = req.BaseUrl; - existing.IsOnline = "在线"; - existing.LastHeartbeat = DateTime.Now; - _repo.DbContext.Updateable(existing).ExecuteCommand(); - entity = existing; - } - else - { - entity = new gateway_nodes - { - NodeCode = req.NodeCode, - NodeName = req.NodeCode, - NodeToken = req.Token, - AdapterTypes = req.AdapterTypes, - BaseUrl = req.BaseUrl, - IsOnline = "在线", - LastHeartbeat = DateTime.Now, - Enable = "启用", - CreateDate = DateTime.Now - }; - _repo.DbContext.Insertable(entity).ExecuteCommand(); - } - - // 返回当前网关的顶层设备 - var deviceRepo = _repo.DbContext; - var devices = deviceRepo.Queryable() - .Where(x => x.GatewayNodeId == entity.NodeId && x.ParentDeviceId == null) - .Select(x => new { x.DeviceId, x.DeviceName, x.AdapterCode, x.SourceId, x.DeviceCategory, x.DeviceGroup, x.IsParent, x.IsOnline, x.ExtraData }) - .ToList(); - - return Ok(new { nodeId = entity.NodeId, devices }); - } - - /// A2: 心跳 - [HttpPost] - [Route("/api/gateway/heartbeat")] - public async Task GatewayHeartbeat([FromBody] GatewayHeartbeatRequest req) - { - var entity = _repo.DbContext.Queryable() - .First(x => x.NodeCode == req.NodeCode && x.NodeToken == req.Token); - if (entity == null) - return StatusCode(401, new { message = "认证失败" }); - - entity.IsOnline = "在线"; - entity.LastHeartbeat = DateTime.Now; - _repo.DbContext.Updateable(entity).ExecuteCommand(); - - return Ok(new { status = "ok", serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }); - } - - /// A3: 设备数据同步(字段分治 + parentSourceId映射) - [HttpPost] - [Route("/api/gateway/sync/devices")] - public async Task SyncDevices([FromBody] SyncDevicesRequest req) - { - var node = _repo.DbContext.Queryable() - .First(x => x.NodeCode == req.NodeCode && x.NodeToken == req.Token); - if (node == null) return StatusCode(401, new { message = "认证失败" }); - - var db = _repo.DbContext; - - // 批量查询已有设备映射表 - var codes = req.Devices.Select(d => d.AdapterCode).Distinct().ToList(); - var existingIds = db.Queryable() - .Where(x => x.GatewayNodeId == node.NodeId && codes.Contains(x.AdapterCode!)) - .ToDictionary(x => (x.AdapterCode!, x.SourceId!), x => x.DeviceId); - - int added = 0, updated = 0; - foreach (var d in req.Devices) - { - bool isNew = !existingIds.ContainsKey((d.AdapterCode, d.SourceId)); - - // 解析 parentSourceId → ParentDeviceId - int? parentDeviceId = null; - if (!string.IsNullOrEmpty(d.ParentSourceId) && existingIds.TryGetValue((d.AdapterCode, d.ParentSourceId), out var pid)) - parentDeviceId = pid; - - if (isNew) - { - var entity = new base_device - { - AdapterCode = d.AdapterCode, - SourceId = d.SourceId, - DeviceName = d.Name, - DeviceCategory = d.Category, - DeviceGroup = d.Group, - GatewayNodeId = node.NodeId, - IsParent = d.IsParent ? "是" : "否", - ParentDeviceId = parentDeviceId, - IsOnline = d.IsOnline ? "在线" : "离线", - IpAddress = d.IpAddress, - Port = d.Port, - ExtraData = d.ExtraData != null - ? System.Text.Json.JsonSerializer.Serialize(d.ExtraData) - : null, - Enable = "启用", - LastSyncTime = DateTime.Now, - CreateDate = DateTime.Now - }; - db.Insertable(entity).ExecuteCommand(); - added++; - } - else - { - var existingId = existingIds[(d.AdapterCode, d.SourceId)]; - var entity = db.Queryable().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.ExtraData != null - ? System.Text.Json.JsonSerializer.Serialize(d.ExtraData) - : entity.ExtraData; - entity.LastSyncTime = DateTime.Now; - db.Updateable(entity).ExecuteCommand(); - updated++; - } - } - } - return Ok(new { added, updated, removed = 0 }); - } - - /// A4: 告警同步(DeviceSourceId→DeviceId映射 + 去重) - [HttpPost] - [Route("/api/gateway/sync/alarms")] - public async Task SyncAlarms([FromBody] SyncAlarmsRequest req) - { - var node = _repo.DbContext.Queryable() - .First(x => x.NodeCode == req.NodeCode && x.NodeToken == req.Token); - if (node == null) return StatusCode(401, new { message = "认证失败" }); - - var db = _repo.DbContext; - - // 批量查 DeviceSourceId → DeviceId - var srcIds = req.Alarms.Select(a => a.DeviceSourceId).ToList(); - var deviceMap = db.Queryable() - .Where(x => x.GatewayNodeId == node.NodeId && srcIds.Contains(x.SourceId!)) - .ToDictionary(x => x.SourceId!, x => x.DeviceId); - - int added = 0; - foreach (var a in req.Alarms) - { - if (db.Queryable().Any(x => x.SourceAlarmId == a.SourceAlarmId)) - continue; - - deviceMap.TryGetValue(a.DeviceSourceId, out var deviceId); - var alarm = new iot_alarm - { - SourceAlarmId = a.SourceAlarmId, - DeviceId = deviceId > 0 ? deviceId : null, - AdapterCode = a.AdapterCode, - AlarmLevel = a.Level, - AlarmDesc = a.Desc, - AlarmValue = a.Value, - StartTime = DateTime.TryParse(a.StartTime, out var st) ? st : DateTime.Now, - State = "未确认", - CreateDate = DateTime.Now - }; - db.Insertable(alarm).ExecuteCommand(); - added++; - } - return Ok(new { added }); - } - } - - 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 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 Dictionary? ExtraData { get; set; } - } - - public class SyncAlarmsRequest - { - public string NodeCode { get; set; } = ""; - public string Token { get; set; } = ""; - public List Alarms { get; set; } = new(); - } - - 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; } = ""; + [HttpPost, Route("/api/gateway/sync/alarms")] + public IActionResult SyncAlarms() => Ok(new { added = 0 }); } } diff --git a/api_sqlsugar/Warehouse/Services/HeartbeatMonitorJob.cs b/api_sqlsugar/Warehouse/Services/HeartbeatMonitorJob.cs index b7df386..cc699f2 100644 --- a/api_sqlsugar/Warehouse/Services/HeartbeatMonitorJob.cs +++ b/api_sqlsugar/Warehouse/Services/HeartbeatMonitorJob.cs @@ -1,27 +1,13 @@ using Quartz; using Microsoft.Extensions.DependencyInjection; -using Warehouse.IRepositories; -using VolPro.Entity.DomainModels; namespace VolPro.Warehouse.Services; public class HeartbeatMonitorJob : IJob { - public async Task Execute(IJobExecutionContext context) + public Task Execute(IJobExecutionContext context) { - var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"]; - var gwRepo = sp.GetService(); - var db = gwRepo.DbContext; - var timeout = DateTime.Now.AddSeconds(-30); - - var offlineNodes = db.Queryable() - .Where(x => x.IsOnline == "在线" && x.LastHeartbeat < timeout) - .ToList(); - - foreach (var node in offlineNodes) - { - node.IsOnline = "离线"; - db.Updateable(node).ExecuteCommand(); - } + // TODO: Phase 2 联调时实现 — 心跳超时检测 + return Task.CompletedTask; } } diff --git a/api_sqlsugar/Warehouse/Services/SyncDevicesJob.cs b/api_sqlsugar/Warehouse/Services/SyncDevicesJob.cs index f0bc165..3f10ab6 100644 --- a/api_sqlsugar/Warehouse/Services/SyncDevicesJob.cs +++ b/api_sqlsugar/Warehouse/Services/SyncDevicesJob.cs @@ -1,31 +1,13 @@ using Quartz; using Microsoft.Extensions.DependencyInjection; -using Warehouse.IRepositories; -using VolPro.Entity.DomainModels; namespace VolPro.Warehouse.Services; public class SyncDevicesJob : IJob { - public async Task Execute(IJobExecutionContext context) + public Task Execute(IJobExecutionContext context) { - var sp = (IServiceProvider)context.JobDetail.JobDataMap["ServiceProvider"]; - var gwRepo = sp.GetService(); - var httpFactory = sp.GetService(); - var db = gwRepo.DbContext; - - var onlineNodes = db.Queryable() - .Where(x => x.IsOnline == "在线" && x.Enable == "启用" && x.BaseUrl != null) - .ToList(); - - foreach (var node in onlineNodes) - { - try - { - var http = httpFactory.CreateClient(); - await http.PostAsync($"{node.BaseUrl}/api/gateway/devices/sync?adapter={node.AdapterTypes}", null); - } - catch { } - } + // TODO: Phase 2 联调时实现 — 定时设备同步 + return Task.CompletedTask; } } diff --git a/api_sqlsugar/Warehouse/Services/SyncEngine.cs b/api_sqlsugar/Warehouse/Services/SyncEngine.cs index 577a796..dc8237a 100644 --- a/api_sqlsugar/Warehouse/Services/SyncEngine.cs +++ b/api_sqlsugar/Warehouse/Services/SyncEngine.cs @@ -1,140 +1,7 @@ -using Microsoft.Extensions.DependencyInjection; -using VolPro.Entity.DomainModels; -using Warehouse.IRepositories; -using System.Text.Json; - namespace VolPro.Warehouse.Services; -/// MC4.0 同步引擎:对象树 → 区域匹配 + 设备 Upsert +/// TODO: Phase 2 联调时实现 — MC4对象树区域匹配+设备Upsert public class SyncEngine { - private readonly IServiceProvider _sp; - - public SyncEngine(IServiceProvider sp) => _sp = sp; - - private Iwarehouse_regionsRepository GetRegionRepo() => - _sp.GetService()!; - private Iwarehouse_devicepointRepository GetPointRepo() => - _sp.GetService()!; - private Ibase_deviceRepository GetDeviceRepo() => - _sp.GetService()!; - - public async Task ProcessMc4TreeAsync(int gatewayNodeId, string adapterCode, List tree) - { - var stats = new SyncStats(); - foreach (var node in tree) - await ProcessNodeAsync(gatewayNodeId, adapterCode, node, null, stats); - return stats; - } - - private async Task ProcessNodeAsync(int gatewayNodeId, string adapterCode, - Mc4TreeNode node, int? parentDeviceId, SyncStats stats) - { - if (node.Type == 1) - { - int pointId = await MatchOrCreatePoint(node); - foreach (var child in node.Children) - await ProcessNodeAsync(gatewayNodeId, adapterCode, child, null, stats); - } - else if (node.Type == 2) - { - int deviceId = await UpsertDevice(gatewayNodeId, adapterCode, node, parentDeviceId); - if (stats.DeviceIds.TryGetValue(node.Id, out _)) - stats.Updated++; - else - stats.Added++; - stats.DeviceIds[node.Id] = deviceId; - - foreach (var child in node.Children) - await ProcessNodeAsync(gatewayNodeId, adapterCode, child, deviceId, stats); - } - } - - private async Task MatchOrCreatePoint(Mc4TreeNode node) - { - var regionRepo = GetRegionRepo(); - var regionDb = regionRepo.DbContext; - - var region = regionDb.Queryable() - .First(x => x.RegionName == node.Name); - if (region == null) - { - region = new warehouse_regions { RegionName = node.Name ?? $"MC4_{node.Id}" }; - regionDb.Insertable(region).ExecuteCommand(); - } - - var pointRepo = GetPointRepo(); - var pointDb = pointRepo.DbContext; - var point = pointDb.Queryable() - .First(x => x.RegionId == region.Id); - if (point == null) - { - point = new warehouse_devicepoint { PointName = node.Name ?? $"MC4_PT_{node.Id}", RegionId = region.Id }; - pointDb.Insertable(point).ExecuteCommand(); - } - return point.PointID; - } - - private async Task UpsertDevice(int gatewayNodeId, string adapterCode, - Mc4TreeNode node, int? parentDeviceId) - { - var deviceRepo = GetDeviceRepo(); - var db = deviceRepo.DbContext; - var sourceId = node.Id.ToString(); - - var existing = db.Queryable() - .First(x => x.AdapterCode == adapterCode && x.SourceId == sourceId); - - if (existing != null) - { - existing.IsOnline = "在线"; - existing.LastSyncTime = DateTime.Now; - existing.ParentDeviceId = parentDeviceId ?? existing.ParentDeviceId; - if (node.Option != null) - existing.ExtraData = JsonSerializer.Serialize(node.Option); - db.Updateable(existing).ExecuteCommand(); - return existing.DeviceId; - } - - var device = new base_device - { - DeviceName = node.Name ?? $"MC4_DEV_{node.Id}", - AdapterCode = adapterCode, - SourceId = sourceId, - DeviceCategory = MapCategory(node.ObjectType, node.Tag), - DeviceGroup = "IoT设备", - GatewayNodeId = gatewayNodeId, - ParentDeviceId = parentDeviceId, - IsParent = node.Children?.Count > 0 ? "是" : "否", - IsOnline = "在线", - Enable = "启用", - LastSyncTime = DateTime.Now, - CreateDate = DateTime.Now, - ExtraData = node.Option != null ? JsonSerializer.Serialize(node.Option) : null - }; - db.Insertable(device).ExecuteCommand(); - return device.DeviceId; - } - - private static string MapCategory(int objectType, string? tag) => - (objectType, tag) switch - { - (_, "温湿度") => "温湿度变送器", - (_, "烟雾") => "烟雾报警器", - (_, "气体") => "气体报警器", - (_, "门磁") => "门磁", - _ => "动环采集器" - }; -} - -public class SyncStats { public int Added; public int Updated; public Dictionary DeviceIds = new(); } -public class Mc4TreeNode -{ - public int Id { get; set; } - public string? Name { get; set; } - public int Type { get; set; } - public int ObjectType { get; set; } - public string? Tag { get; set; } - public Dictionary? Option { get; set; } - public List Children { get; set; } = new(); + public Task ProcessAsync() => Task.CompletedTask; }