/* *设备管理扩展 — 区域树 + 点位设备列表 *所有改动在 Partial 目录,不破坏框架可升级性 */ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using VolPro.Core.BaseProvider; using VolPro.Core.Configuration; using VolPro.Core.Enums; using VolPro.Core.Extensions; using VolPro.Core.Filters; using VolPro.Core.ManageUser; using VolPro.Entity.DomainModels; using VolPro.Sys.IRepositories; using VolPro.Sys.Repositories; using Warehouse.IRepositories; using Warehouse.IServices; using Warehouse.Repositories; namespace Warehouse.Controllers { public partial class base_deviceController { private readonly Ibase_deviceService _service;//访问业务代码 private readonly Iwarehouse_regionsService _regionsService; private readonly Iwarehouse_devicepointService _pointService; private readonly Ibase_deviceRepository _repository; private readonly IHttpContextAccessor _httpContextAccessor; [ActivatorUtilitiesConstructor] public base_deviceController( Ibase_deviceService service, Iwarehouse_regionsService regionsService, Iwarehouse_devicepointService pointService, Ibase_deviceRepository repository, IHttpContextAccessor httpContextAccessor ) : base(service) { _service = service; _regionsService = regionsService; _pointService = pointService; _repository = repository; _httpContextAccessor = httpContextAccessor; } /// /// 获取区域→点位→设备树。 /// 用于管理端左侧树形控件展示层级结构。 /// 格式: [{ id, label, type:"region", children: [{ id, label, type:"point", deviceCount }] }] /// [HttpGet] [Route("/api/DeviceManager/GetRegionTree")] public async Task GetRegionTree() { // 查所有区域 var regions = await _regionsService.FindAsIQueryable(x => true).ToListAsync(); // 查所有点位 var points = await _pointService.FindAsIQueryable(x => true).ToListAsync(); // 统计每个点位下的设备数量 //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(); var deviceCounts = new Dictionary(); var devices = await _service.FindAsIQueryable(x => x.PointId != null) .Select(x => new { x.PointId }) .ToListAsync(); deviceCounts = devices .Where(x => x.PointId.HasValue) .GroupBy(x => x.PointId!.Value) .ToDictionary(g => g.Key, g => 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 }); } /// /// V1.12.2 修复:通用分页查询(带 Filter/Wheres/Sort 支持) /// /// **历史**:V1.5 之前此端点 override 直接调 GetTreeTableRootData(硬编码 ParentDeviceId==0 /// 或 null 查顶层设备),**完全忽略 options.Filter**。这导致 warehouse 大端 Filter.vue /// 按 MapModelId 查询设备时永远返回空(子设备查不到)。 /// /// **修复**:智能分流 /// - options.Filter 不为空 或 options.Wheres 不为空 → 走通用分页(支持任意字段过滤) /// - 都为空 → 走原 GetTreeTableRootData 逻辑(保持 tree-table 组件兼容) /// /// 注意:基类 ApiBaseController.GetPageData 是 virtual ActionResult 同步方法, /// 这里用 override 保留二进制兼容;内部用 .Result 包装 async 调用(不会死锁, /// 因为 GetFilteredPageData 内部是纯查询不涉及同步上下文切换)。 /// [ApiActionPermission(ActionPermissionOptions.Search)] [HttpPost, Route("GetPageData")] public override ActionResult GetPageData([FromBody] PageDataOptions loadData) { // 1) 智能分流:有 filter/wheres → 通用分页;无 → 走原 tree-table 根节点 var hasFilter = (loadData?.Filter != null && loadData.Filter.Count > 0) || !string.IsNullOrEmpty(loadData?.Wheres); if (hasFilter) { return GetFilteredPageData(loadData); } return GetTreeTableRootData(loadData).Result; } /// /// V1.12.2 新增:通用分页查询(不限制 ParentDeviceId,支持任意字段 Filter)。 /// 用于 warehouse 大端按 MapModelId 等任意字段查设备列表。 /// private ActionResult GetFilteredPageData(PageDataOptions options) { // 直接用 _service 调用 IService.GetPageData 默认实现 // 默认实现会自动处理 options.Filter(按 name 字段名+value 值+displayType 比较类型) // 并按 options.Page/Rows 分页、按 options.Sort 排序 var pageData = _service.GetPageData(options); // 统一响应结构:{ total, rows }(与 GetTreeTableRootData 一致) return JsonNormal(new { total = pageData.total, rows = pageData.rows }); } /// /// treetable 获取一级(根)节点数据 /// /// [HttpPost, Route("getTreeTableRootData")] [ApiActionPermission(ActionPermissionOptions.Search)] public async Task GetTreeTableRootData([FromBody] PageDataOptions options) { //页面加载根节点数据条件x => x.ParentId == 0,自己根据需要设置 var dbServiceId = UserContext.CurrentServiceId; // V1.8 增量补遗:过滤条件兼容 ParentDeviceId=0 和 =null 两种情况 // 原因:历史数据 + 网关未给 IsParent 赋值时顶层设备 ParentDeviceId 可能为 NULL // 框架默认条件 x => x.ParentId == 0 会漏掉所有 NULL 记录 var query = _repository.FindAsIQueryable(x => x.ParentDeviceId == 0 || x.ParentDeviceId == null || x.DeviceId == 1); var queryChild = _repository.FindAsIQueryable(x => true); // 先获取总数 var total = await query.CountAsync(); // 按需先排序,再 Skip/Take 分页 var rows = await query .OrderBy(x => x.DeviceId) .Skip((options.Page - 1) * options.Rows) .Take(options.Rows) .Select(s => new { s.DeviceId, s.ParentDeviceId, s.DeviceName, s.AdapterCode, s.SourceId, s.DeviceCategory, s.DeviceGroup, s.IsParent, s.IsOnline, s.IpAddress, s.Port, s.Location, s.ExtraData, s.LastSyncTime, s.MapModelId, s.MapModelScale, s.MapModelRotation, s.Enable, s.CreateDate, s.Creator, s.Modifier, s.ModifyDate, hasChildren = SqlSugar.SqlFunc.Subqueryable().Where(x => x.ParentDeviceId == s.DeviceId).Any() }).ToListAsync(); return JsonNormal(new { total, rows }); } /// ///treetable 获取子节点数据 /// /// [HttpPost, Route("getTreeTableChildrenData")] [ApiActionPermission(ActionPermissionOptions.Search)] public async Task GetTreeTableChildrenData(int deviceId) { //点击节点时,加载子节点数据 var basedeviceRepository = base_deviceRepository.Instance.FindAsIQueryable(x => true); var query = basedeviceRepository.Where(x => x.ParentDeviceId == deviceId); //if (AppSetting.UseDynamicShareDB) //{ // query = query.Where(x => x.DbServiceId == UserContext.CurrentServiceId); //} var rows = await query .Select(s => new { s.DeviceId, s.ParentDeviceId, s.DeviceName, s.AdapterCode, s.SourceId, s.DeviceCategory, s.DeviceGroup, s.IsParent, s.IsOnline, s.IpAddress, s.Port, s.Location, s.ExtraData, s.LastSyncTime, s.MapModelId, s.MapModelScale, s.MapModelRotation, s.Enable, s.CreateDate, s.Creator, s.Modifier, s.ModifyDate, hasChildren = SqlSugar.SqlFunc.Subqueryable().Where(x => x.ParentDeviceId == s.DeviceId).Any() }).ToListAsync(); return JsonNormal(new { rows }); } } }