From bcd6484f45224bd7db722849cfba173df4e576d2 Mon Sep 17 00:00:00 2001 From: dengboling Date: Thu, 3 Sep 2026 17:16:28 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=B1=BB=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=A7=E5=93=81=E5=AD=97=E6=AE=B5=E3=80=81=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=89=A9=E6=A8=A1=E5=9E=8B=E7=B1=BB=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BA=A7=E5=93=81CRUD=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Config/IotDeviceController.cs | 25 +- .../Controllers/Config/ProductController.cs | 118 +++++++- .../Config/ThingPointController.cs | 72 +++++ .../Inspection/AlertRuleController.cs | 12 + Model/Dto/Config/DeviceCommandDto.cs | 28 ++ Model/Dto/Config/IotDeviceDto.cs | 6 + Model/Dto/Config/ProductCategoryDto.cs | 26 ++ Model/Dto/Config/ProductDto.cs | 46 ++++ Model/Dto/Config/ProductOptionDto.cs | 17 ++ Model/Dto/Config/ThingModelPointDto.cs | 58 ++++ Model/Dto/Inspection/AlertRuleDto.cs | 9 + Model/Entity/Config/IotDeviceEntity.cs | 6 + Model/Entity/Config/ProductCategoryEntity.cs | 34 +++ Model/Entity/Config/ProductEntity.cs | 64 +++++ Model/Entity/Config/ThingEnums.cs | 91 +++++++ Model/Entity/Config/ThingModelPointEntity.cs | 95 +++++++ Model/Entity/Inspection/AlertRuleEntity.cs | 18 ++ Model/Mapper/EntityMapper.cs | 189 +++++++++++++ .../Implement/Config/DeviceCommandService.cs | 178 +++++++++++++ Service/Implement/Config/DeviceService.cs | 40 ++- Service/Implement/Config/ProductService.cs | 251 +++++++++++++++++- Service/Implement/Config/ThingPointService.cs | 126 +++++++++ .../Implement/Inspection/AlertRuleService.cs | 25 +- .../Interface/Config/IDeviceCommandService.cs | 18 ++ Service/Interface/Config/IDeviceService.cs | 4 +- Service/Interface/Config/IProductService.cs | 41 ++- .../Interface/Config/IThingPointService.cs | 28 ++ .../Interface/Inspection/IAlertRuleService.cs | 8 +- Service/Service.csproj | 1 + 29 files changed, 1611 insertions(+), 23 deletions(-) create mode 100644 IOT_API/Controllers/Config/ThingPointController.cs create mode 100644 Model/Dto/Config/DeviceCommandDto.cs create mode 100644 Model/Dto/Config/ProductCategoryDto.cs create mode 100644 Model/Dto/Config/ProductDto.cs create mode 100644 Model/Dto/Config/ProductOptionDto.cs create mode 100644 Model/Dto/Config/ThingModelPointDto.cs create mode 100644 Model/Entity/Config/ProductCategoryEntity.cs create mode 100644 Model/Entity/Config/ProductEntity.cs create mode 100644 Model/Entity/Config/ThingEnums.cs create mode 100644 Model/Entity/Config/ThingModelPointEntity.cs create mode 100644 Service/Implement/Config/DeviceCommandService.cs create mode 100644 Service/Implement/Config/ThingPointService.cs create mode 100644 Service/Interface/Config/IDeviceCommandService.cs create mode 100644 Service/Interface/Config/IThingPointService.cs diff --git a/IOT_API/Controllers/Config/IotDeviceController.cs b/IOT_API/Controllers/Config/IotDeviceController.cs index d9027f2..0c64249 100644 --- a/IOT_API/Controllers/Config/IotDeviceController.cs +++ b/IOT_API/Controllers/Config/IotDeviceController.cs @@ -15,23 +15,26 @@ namespace WebAPI.Controllers public class IotDeviceController : ControllerBase { private readonly IDeviceService _deviceService; + private readonly IDeviceCommandService _deviceCommandService; - public IotDeviceController(IDeviceService deviceService) + public IotDeviceController(IDeviceService deviceService, IDeviceCommandService deviceCommandService) { _deviceService = deviceService; + _deviceCommandService = deviceCommandService; } /// - /// 设备列表(分页查询,支持关键字搜索) + /// 设备列表(分页查询,支持关键字搜索,可按所属产品筛选) /// /// 页码(从1开始,默认1) /// 每页数量(默认10) /// 关键字(模糊匹配设备编号/名称/类型) + /// 所属产品Id(0 表示不过滤) [HttpGet("list")] - public async Task GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null) + public async Task GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long productId = 0) { RefAsync total = 0; - var result = await _deviceService.GetPagedAsync(pageIndex, pageSize, total, keyword); + var result = await _deviceService.GetPagedAsync(pageIndex, pageSize, total, keyword, productId > 0 ? productId : null); Response.Headers["X-Total-Count"] = total.Value.ToString(); return result.IsSuccess ? Ok(Result>.Success(result.Data)) @@ -92,5 +95,19 @@ namespace WebAPI.Controllers ? Ok(Result>.Success(result.Data)) : Ok(Result>.Error(result.Msg)); } + + /// + /// 按物模型可写点向设备下发指令 + /// + /// 设备主键 Id + /// 指令请求(PointId 点Id + Value 工程值 + IsSimulated 是否模拟) + [HttpPost("{id}/command")] + public async Task SendCommand(long id, [FromBody] DeviceCommandDto dto) + { + if (dto == null) + return Ok(Result.Error("请求体不能为空")); + dto.Id = id.ToString(); + return Ok(await _deviceCommandService.SendCommandAsync(dto)); + } } } diff --git a/IOT_API/Controllers/Config/ProductController.cs b/IOT_API/Controllers/Config/ProductController.cs index 1aed918..533a633 100644 --- a/IOT_API/Controllers/Config/ProductController.cs +++ b/IOT_API/Controllers/Config/ProductController.cs @@ -1,14 +1,128 @@ using Microsoft.AspNetCore.Mvc; +using Model; +using Model.Dto.Config; +using Service.Interface.Config; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; namespace WebAPI.Controllers { /// - /// 产品管理 + /// 产品管理(含产品分类) /// [ApiController] [Route("api/config/product")] public class ProductController : ControllerBase { - // TODO: 实现 产品管理 相关接口 + private readonly IProductService _productService; + + public ProductController(IProductService productService) + { + _productService = productService; + } + + // ===================== 产品分类 ===================== + + /// + /// 产品分类列表 + /// + [HttpGet("category/list")] + public async Task GetCategories() + { + return Ok(await _productService.GetCategoriesAsync()); + } + + /// + /// 新增产品分类 + /// + [HttpPost("category/add")] + public async Task AddCategory([FromBody] ProductCategoryDto dto) + { + return Ok(await _productService.AddCategoryAsync(dto)); + } + + /// + /// 修改产品分类 + /// + [HttpPut("category/update")] + public async Task UpdateCategory([FromBody] ProductCategoryDto dto) + { + return Ok(await _productService.UpdateCategoryAsync(dto)); + } + + /// + /// 删除产品分类(软删除) + /// + [HttpDelete("category/{id}")] + public async Task DeleteCategory(long id) + { + return Ok(await _productService.DeleteCategoryAsync(id)); + } + + // ===================== 产品 ===================== + + /// + /// 产品下拉选项(设备选择所属产品) + /// + [HttpGet("options")] + public async Task GetOptions() + { + return Ok(await _productService.GetOptionsAsync()); + } + + /// + /// 产品列表(分页) + /// + /// 页码(从1开始,默认1) + /// 每页数量(默认10) + /// 关键字(模糊匹配型号/名称) + /// 分类Id(0 表示不过滤) + [HttpGet("list")] + public async Task GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long categoryId = 0) + { + RefAsync total = 0; + var result = await _productService.GetPagedAsync(pageIndex, pageSize, total, keyword, categoryId); + Response.Headers["X-Total-Count"] = total.Value.ToString(); + return result.IsSuccess + ? Ok(Result>.Success(result.Data)) + : Ok(Result>.Error(result.Msg)); + } + + /// + /// 产品详情 + /// + [HttpGet("{id}")] + public async Task GetById(long id) + { + return Ok(await _productService.GetByIdAsync(id)); + } + + /// + /// 新增产品 + /// + [HttpPost] + public async Task Add([FromBody] ProductDto dto) + { + return Ok(await _productService.AddAsync(dto)); + } + + /// + /// 修改产品 + /// + [HttpPut] + public async Task Update([FromBody] ProductDto dto) + { + return Ok(await _productService.UpdateAsync(dto)); + } + + /// + /// 删除产品(软删除) + /// + [HttpDelete("{id}")] + public async Task Delete(long id) + { + return Ok(await _productService.DeleteAsync(id)); + } } } diff --git a/IOT_API/Controllers/Config/ThingPointController.cs b/IOT_API/Controllers/Config/ThingPointController.cs new file mode 100644 index 0000000..0845864 --- /dev/null +++ b/IOT_API/Controllers/Config/ThingPointController.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Mvc; +using Model; +using Model.Dto.Config; +using Service.Interface.Config; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace WebAPI.Controllers +{ + /// + /// 物模型点(产品/设备通用,按 ownerType+ownerId 归属) + /// + [ApiController] + [Route("api/config/thing-point")] + public class ThingPointController : ControllerBase + { + private readonly IThingPointService _thingPointService; + + public ThingPointController(IThingPointService thingPointService) + { + _thingPointService = thingPointService; + } + + /// + /// 点列表 + /// + /// 归属类型(1=产品 2=设备) + /// 归属对象Id + /// 关键字(点编码/名称) + [HttpGet("list")] + public async Task GetList(int ownerType, long ownerId, string? keyword = null) + { + return Ok(await _thingPointService.GetListAsync(ownerType, ownerId, keyword)); + } + + /// + /// 点详情 + /// + [HttpGet("{id}")] + public async Task GetById(long id) + { + return Ok(await _thingPointService.GetByIdAsync(id)); + } + + /// + /// 新增点 + /// + [HttpPost] + public async Task Add([FromBody] ThingModelPointDto dto) + { + return Ok(await _thingPointService.AddAsync(dto)); + } + + /// + /// 修改点 + /// + [HttpPut] + public async Task Update([FromBody] ThingModelPointDto dto) + { + return Ok(await _thingPointService.UpdateAsync(dto)); + } + + /// + /// 删除点(软删除) + /// + [HttpDelete("{id}")] + public async Task Delete(long id) + { + return Ok(await _thingPointService.DeleteAsync(id)); + } + } +} diff --git a/IOT_API/Controllers/Inspection/AlertRuleController.cs b/IOT_API/Controllers/Inspection/AlertRuleController.cs index 2f12f6c..0c4fb27 100644 --- a/IOT_API/Controllers/Inspection/AlertRuleController.cs +++ b/IOT_API/Controllers/Inspection/AlertRuleController.cs @@ -28,6 +28,18 @@ namespace WebAPI.Controllers return Ok(await _alertRuleService.GetListAsync()); } + /// + /// 获取某归属对象(产品/设备)下的规则列表 + /// + /// 归属类型(1=产品 2=设备) + /// 归属对象Id + /// 可选:绑定点编码过滤 + [HttpGet("owner")] + public async Task GetOwnerRules(int ownerType, long ownerId, string? pointCode = null) + { + return Ok(await _alertRuleService.GetOwnerRulesAsync(ownerType, ownerId, pointCode)); + } + /// /// 新增告警规则(前端传 DTO) /// diff --git a/Model/Dto/Config/DeviceCommandDto.cs b/Model/Dto/Config/DeviceCommandDto.cs new file mode 100644 index 0000000..da650fb --- /dev/null +++ b/Model/Dto/Config/DeviceCommandDto.cs @@ -0,0 +1,28 @@ +namespace Model.Dto.Config +{ + /// + /// 设备指令下发请求 DTO + /// + public class DeviceCommandDto + { + /// + /// 设备主键 Id(string) + /// + public string Id { get; set; } + + /// + /// 物模型点 Id(string,决定写哪个寄存器/线圈) + /// + public string PointId { get; set; } + + /// + /// 下发的工程值 + /// + public double Value { get; set; } + + /// + /// 是否仅记录(模拟/离线调试:不实际发网络报文) + /// + public bool IsSimulated { get; set; } + } +} diff --git a/Model/Dto/Config/IotDeviceDto.cs b/Model/Dto/Config/IotDeviceDto.cs index 6ce12a1..0bdc35f 100644 --- a/Model/Dto/Config/IotDeviceDto.cs +++ b/Model/Dto/Config/IotDeviceDto.cs @@ -19,6 +19,12 @@ namespace Model.Dto.Config /// 设备类型(型号,对应 DeviceCommand 驱动类) public string DeviceType { get; set; } + /// 所属产品Id(string,0=未分类) + public string ProductId { get; set; } + + /// 所属产品型号/名称(只读展示) + public string? ProductName { get; set; } + /// 所属网关Code public string GatewayCode { get; set; } diff --git a/Model/Dto/Config/ProductCategoryDto.cs b/Model/Dto/Config/ProductCategoryDto.cs new file mode 100644 index 0000000..ca74376 --- /dev/null +++ b/Model/Dto/Config/ProductCategoryDto.cs @@ -0,0 +1,26 @@ +namespace Model.Dto.Config +{ + /// + /// 产品分类 DTO(Id 由 long 改为 string,避免前端精度丢失) + /// + public class ProductCategoryDto + { + /// 主键 Id(long → string) + public string Id { get; set; } + + /// 分类编码 + public string Code { get; set; } + + /// 分类名称 + public string? Name { get; set; } + + /// 排序号 + public int Sort { get; set; } + + /// 备注 + public string? Remark { get; set; } + + /// 创建时间 + public DateTime? CreateTime { get; set; } + } +} diff --git a/Model/Dto/Config/ProductDto.cs b/Model/Dto/Config/ProductDto.cs new file mode 100644 index 0000000..58809b7 --- /dev/null +++ b/Model/Dto/Config/ProductDto.cs @@ -0,0 +1,46 @@ +using Model.Entity.Config; + +namespace Model.Dto.Config +{ + /// + /// 产品 DTO(Id 由 long 改为 string,避免前端精度丢失) + /// + public class ProductDto + { + /// 主键 Id(long → string) + public string Id { get; set; } + + /// 型号(唯一) + public string Model { get; set; } + + /// 产品名称 + public string? Name { get; set; } + + /// 产品分类Id + public string CategoryId { get; set; } + + /// 分类名称(只读展示) + public string? CategoryName { get; set; } + + /// 制造商 + public string? Manufacturer { get; set; } + + /// 产品描述 + public string? Description { get; set; } + + /// 通讯协议 + public IotDeviceProtocolEnum ProtocolType { get; set; } + + /// 消息模型 + public string? MessageModel { get; set; } + + /// 是否启用 + public bool IsEnabled { get; set; } = true; + + /// 备注 + public string? Remark { get; set; } + + /// 创建时间 + public DateTime? CreateTime { get; set; } + } +} diff --git a/Model/Dto/Config/ProductOptionDto.cs b/Model/Dto/Config/ProductOptionDto.cs new file mode 100644 index 0000000..ec31ec3 --- /dev/null +++ b/Model/Dto/Config/ProductOptionDto.cs @@ -0,0 +1,17 @@ +namespace Model.Dto.Config +{ + /// + /// 产品下拉选项 DTO(设备选择"所属产品"、筛选下拉用) + /// + public class ProductOptionDto + { + /// 主键 Id(string) + public string Id { get; set; } + + /// 型号 + public string Model { get; set; } + + /// 产品名称 + public string? Name { get; set; } + } +} diff --git a/Model/Dto/Config/ThingModelPointDto.cs b/Model/Dto/Config/ThingModelPointDto.cs new file mode 100644 index 0000000..c704aae --- /dev/null +++ b/Model/Dto/Config/ThingModelPointDto.cs @@ -0,0 +1,58 @@ +using Model.Entity.Config; + +namespace Model.Dto.Config +{ + /// + /// 物模型点 DTO(Id 由 long 改为 string,避免前端精度丢失) + /// + public class ThingModelPointDto + { + /// 主键 Id(long → string) + public string Id { get; set; } + + /// 归属类型(1=产品 2=设备) + public ThingOwnerTypeEnum OwnerType { get; set; } + + /// 归属对象Id(string) + public string OwnerId { get; set; } + + /// 点编码 + public string Code { get; set; } + + /// 点名称 + public string? Name { get; set; } + + /// 寄存器类型 + public ThingRegisterTypeEnum RegisterType { get; set; } + + /// 读写权限 + public ThingRwEnum Rw { get; set; } + + /// 数据类型 + public ThingDataTypeEnum DataType { get; set; } + + /// 寄存器起始地址 + public ushort Address { get; set; } + + /// 换算系数 + public double Scale { get; set; } = 1; + + /// 偏移量 + public double Offset { get; set; } + + /// 单位 + public string? Unit { get; set; } + + /// 是否启用 + public bool Enabled { get; set; } = true; + + /// 排序号 + public int Sort { get; set; } + + /// 备注 + public string? Remark { get; set; } + + /// 创建时间 + public DateTime? CreateTime { get; set; } + } +} diff --git a/Model/Dto/Inspection/AlertRuleDto.cs b/Model/Dto/Inspection/AlertRuleDto.cs index e966545..347b26f 100644 --- a/Model/Dto/Inspection/AlertRuleDto.cs +++ b/Model/Dto/Inspection/AlertRuleDto.cs @@ -8,6 +8,15 @@ namespace Model.Dto.Inspection /// 主键 Id(long → string) public string Id { get; set; } + /// 归属类型(0=全局;1=产品 2=设备) + public byte OwnerType { get; set; } + + /// 归属对象Id(0 表示全局) + public long OwnerId { get; set; } + + /// 绑定点编码(空表示整产品/设备级规则) + public string PointCode { get; set; } + /// 删除状态(0、未删除;1、已删除) public byte IsDel { get; set; } diff --git a/Model/Entity/Config/IotDeviceEntity.cs b/Model/Entity/Config/IotDeviceEntity.cs index db7b14e..ac979ae 100644 --- a/Model/Entity/Config/IotDeviceEntity.cs +++ b/Model/Entity/Config/IotDeviceEntity.cs @@ -27,6 +27,12 @@ namespace Model.Entity.Config [SugarColumn(ColumnDescription = "设备类型", Length = 64)] public string DeviceType { get; set; } + /// + /// 所属产品Id(关联 ProductEntity.Id,仅用于归类,0=未分类) + /// + [SugarColumn(ColumnDescription = "所属产品Id", DefaultValue = "0")] + public long ProductId { get; set; } + /// /// 所属网关Code(对应网关实体/通道的标识,连接时据此刻查找 IP/端口/串口) /// diff --git a/Model/Entity/Config/ProductCategoryEntity.cs b/Model/Entity/Config/ProductCategoryEntity.cs new file mode 100644 index 0000000..0dc0deb --- /dev/null +++ b/Model/Entity/Config/ProductCategoryEntity.cs @@ -0,0 +1,34 @@ +using SqlSugar; + +namespace Model.Entity.Config +{ + /// + /// 产品分类(字典:维护产品分类,新增产品时可设置分类) + /// + public class ProductCategoryEntity : BaseEntity + { + /// + /// 分类编码 + /// + [SugarColumn(ColumnDescription = "分类编码", Length = 50)] + public string Code { get; set; } + + /// + /// 分类名称 + /// + [SugarColumn(ColumnDescription = "分类名称", Length = 100, IsNullable = true)] + public string? Name { get; set; } + + /// + /// 排序号 + /// + [SugarColumn(ColumnDescription = "排序号")] + public int Sort { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)] + public string? Remark { get; set; } + } +} diff --git a/Model/Entity/Config/ProductEntity.cs b/Model/Entity/Config/ProductEntity.cs new file mode 100644 index 0000000..a9e0474 --- /dev/null +++ b/Model/Entity/Config/ProductEntity.cs @@ -0,0 +1,64 @@ +using SqlSugar; + +namespace Model.Entity.Config +{ + /// + /// 产品(按型号区分;产品给设备归类,产品的物模型/告警等配置与设备相互独立) + /// + public class ProductEntity : BaseEntity + { + /// + /// 型号(唯一,如 GT-100) + /// + [SugarColumn(ColumnDescription = "型号", Length = 100)] + public string Model { get; set; } + + /// + /// 产品名称 + /// + [SugarColumn(ColumnDescription = "产品名称", Length = 100, IsNullable = true)] + public string? Name { get; set; } + + /// + /// 产品分类Id(关联 ProductCategoryEntity.Id,0 表示未分类) + /// + [SugarColumn(ColumnDescription = "产品分类Id")] + public long CategoryId { get; set; } + + /// + /// 制造商 + /// + [SugarColumn(ColumnDescription = "制造商", Length = 100, IsNullable = true)] + public string? Manufacturer { get; set; } + + /// + /// 产品描述 + /// + [SugarColumn(ColumnDescription = "产品描述", Length = 500, IsNullable = true)] + public string? Description { get; set; } + + /// + /// 通讯协议(复用 IotDeviceProtocolEnum:决定 DeviceCommand 驱动分支) + /// + [SugarColumn(ColumnDescription = "通讯协议")] + public IotDeviceProtocolEnum ProtocolType { get; set; } = IotDeviceProtocolEnum.ModbusTcp; + + /// + /// 消息模型(消息协议/报文格式描述,如 JSON / 透传) + /// + [SugarColumn(ColumnDescription = "消息模型", Length = 64, IsNullable = true)] + public string? MessageModel { get; set; } + + /// + /// 是否启用 + /// + [SugarColumn(ColumnDescription = "是否启用")] + public bool IsEnabled { get; set; } = true; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)] + public string? Remark { get; set; } + } +} diff --git a/Model/Entity/Config/ThingEnums.cs b/Model/Entity/Config/ThingEnums.cs new file mode 100644 index 0000000..7d4b929 --- /dev/null +++ b/Model/Entity/Config/ThingEnums.cs @@ -0,0 +1,91 @@ +namespace Model.Entity.Config +{ + /// + /// 物模型归属类型(物模型点表 OwnerType:点挂在哪一类对象下) + /// + public enum ThingOwnerTypeEnum + { + /// + /// 产品(型号) + /// + Product = 1, + + /// + /// 设备(实例) + /// + Device = 2 + } + + /// + /// 点读写权限(决定该点是否可用于下发指令) + /// + public enum ThingRwEnum + { + /// + /// 只读 + /// + ReadOnly = 1, + + /// + /// 只写 + /// + WriteOnly = 2, + + /// + /// 读写 + /// + ReadWrite = 3 + } + + /// + /// 寄存器类型 + /// + public enum ThingRegisterTypeEnum + { + /// + /// 线圈(读/写 bool) + /// + Coil = 1, + + /// + /// 保持寄存器(读/写) + /// + HoldingRegister = 2, + + /// + /// 输入寄存器(只读,如 UMC1300 的温度 PV) + /// + InputRegister = 3 + } + + /// + /// 数据类型 + /// + public enum ThingDataTypeEnum + { + /// + /// 布尔 + /// + Bool = 1, + + /// + /// 16位有符号整数 + /// + Int16 = 2, + + /// + /// 16位无符号整数 + /// + UInt16 = 3, + + /// + /// 32位整数(占2寄存器,暂不支持下发) + /// + Int32 = 4, + + /// + /// 32位浮点(占2寄存器,暂不支持下发) + /// + Float = 5 + } +} diff --git a/Model/Entity/Config/ThingModelPointEntity.cs b/Model/Entity/Config/ThingModelPointEntity.cs new file mode 100644 index 0000000..3aecfda --- /dev/null +++ b/Model/Entity/Config/ThingModelPointEntity.cs @@ -0,0 +1,95 @@ +using SqlSugar; + +namespace Model.Entity.Config +{ + /// + /// 物模型点(属性/指令通用表;通过 OwnerType+OwnerId 归属产品或设备) + /// 一条点 = 一个寄存器/线圈映射;可写点即"下发指令"的入口 + /// + public class ThingModelPointEntity : BaseEntity + { + /// + /// 归属类型(1=产品 2=设备) + /// + [SugarColumn(ColumnDescription = "归属类型")] + public ThingOwnerTypeEnum OwnerType { get; set; } + + /// + /// 归属对象Id(ProductEntity.Id / IotDeviceEntity.Id) + /// + [SugarColumn(ColumnDescription = "归属对象Id")] + public long OwnerId { get; set; } + + /// + /// 点编码(同一归属内唯一,如 TempPV) + /// + [SugarColumn(ColumnDescription = "点编码", Length = 64)] + public string Code { get; set; } + + /// + /// 点名称 + /// + [SugarColumn(ColumnDescription = "点名称", Length = 100, IsNullable = true)] + public string? Name { get; set; } + + /// + /// 寄存器类型(线圈/保持寄存器/输入寄存器) + /// + [SugarColumn(ColumnDescription = "寄存器类型")] + public ThingRegisterTypeEnum RegisterType { get; set; } + + /// + /// 读写权限 + /// + [SugarColumn(ColumnDescription = "读写权限")] + public ThingRwEnum Rw { get; set; } + + /// + /// 数据类型 + /// + [SugarColumn(ColumnDescription = "数据类型")] + public ThingDataTypeEnum DataType { get; set; } + + /// + /// 寄存器起始地址 + /// + [SugarColumn(ColumnDescription = "寄存器起始地址")] + public ushort Address { get; set; } + + /// + /// 换算系数(工程值 = 寄存器原始值 / Scale + Offset,对应驱动内 SCALE) + /// + [SugarColumn(ColumnDescription = "换算系数", DecimalDigits = 4)] + public double Scale { get; set; } = 1; + + /// + /// 偏移量 + /// + [SugarColumn(ColumnDescription = "偏移量", DecimalDigits = 4)] + public double Offset { get; set; } + + /// + /// 单位 + /// + [SugarColumn(ColumnDescription = "单位", Length = 16, IsNullable = true)] + public string? Unit { get; set; } + + /// + /// 是否启用 + /// + [SugarColumn(ColumnDescription = "是否启用")] + public bool Enabled { get; set; } = true; + + /// + /// 排序号 + /// + [SugarColumn(ColumnDescription = "排序号")] + public int Sort { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)] + public string? Remark { get; set; } + } +} diff --git a/Model/Entity/Inspection/AlertRuleEntity.cs b/Model/Entity/Inspection/AlertRuleEntity.cs index fa56735..7e3756a 100644 --- a/Model/Entity/Inspection/AlertRuleEntity.cs +++ b/Model/Entity/Inspection/AlertRuleEntity.cs @@ -8,6 +8,24 @@ namespace Model.Entity.Inspection /// public class AlertRuleEntity : BaseEntity { + /// + /// 归属类型(0=全局/旧数据;1=产品 2=设备) + /// + [SugarColumn(ColumnDescription = "归属类型", DefaultValue = "0")] + public byte OwnerType { get; set; } + + /// + /// 归属对象Id(ProductEntity.Id / IotDeviceEntity.Id,0 表示全局) + /// + [SugarColumn(ColumnDescription = "归属对象Id", DefaultValue = "0")] + public long OwnerId { get; set; } + + /// + /// 绑定点编码(关联物模型点 Code;为空表示整产品/设备级规则) + /// + [SugarColumn(ColumnDescription = "绑定点编码", Length = 64, IsNullable = true)] + public string? PointCode { get; set; } + /// /// 规则名称 /// diff --git a/Model/Mapper/EntityMapper.cs b/Model/Mapper/EntityMapper.cs index 9f4724c..a8b1143 100644 --- a/Model/Mapper/EntityMapper.cs +++ b/Model/Mapper/EntityMapper.cs @@ -255,6 +255,9 @@ namespace Model.Mapper return new AlertRuleDto { Id = entity.Id.ToString(), + OwnerType = entity.OwnerType, + OwnerId = entity.OwnerId, + PointCode = entity.PointCode, IsDel = entity.IsDel, CreateTime = entity.CreateTime, RuleName = entity.RuleName, @@ -352,6 +355,9 @@ namespace Model.Mapper return new AlertRuleEntity { Id = ParseId(dto.Id), + OwnerType = dto.OwnerType, + OwnerId = dto.OwnerId, + PointCode = dto.PointCode, IsDel = dto.IsDel, CreateTime = dto.CreateTime, RuleName = dto.RuleName, @@ -403,6 +409,7 @@ namespace Model.Mapper Code = entity.Code, Name = entity.Name, DeviceType = entity.DeviceType, + ProductId = entity.ProductId.ToString(), GatewayCode = entity.GatewayCode, SlaveId = entity.SlaveId, Manufacturer = entity.Manufacturer, @@ -437,6 +444,7 @@ namespace Model.Mapper Code = dto.Code, Name = dto.Name, DeviceType = dto.DeviceType, + ProductId = ParseId(dto.ProductId), GatewayCode = dto.GatewayCode, SlaveId = dto.SlaveId, Manufacturer = dto.Manufacturer, @@ -476,5 +484,186 @@ namespace Model.Mapper return entities?.Select(e => e.ToDto()).ToList() ?? new List(); } #endregion + + #region 产品分类 + /// + /// ProductCategoryEntity → ProductCategoryDto + /// + public static ProductCategoryDto ToDto(this ProductCategoryEntity entity) + { + if (entity == null) return null; + return new ProductCategoryDto + { + Id = entity.Id.ToString(), + Code = entity.Code, + Name = entity.Name, + Sort = entity.Sort, + Remark = entity.Remark, + CreateTime = entity.CreateTime + }; + } + + /// + /// List<ProductCategoryEntity> → List<ProductCategoryDto> + /// + public static List ToDtoList(this List entities) + { + return entities?.Select(e => e.ToDto()).ToList() ?? new List(); + } + + /// + /// ProductCategoryDto → ProductCategoryEntity(入参映射) + /// + public static ProductCategoryEntity ToEntity(this ProductCategoryDto dto) + { + if (dto == null) return null; + return new ProductCategoryEntity + { + Id = ParseId(dto.Id), + Code = dto.Code, + Name = dto.Name, + Sort = dto.Sort, + Remark = dto.Remark + }; + } + #endregion + + #region 产品 + /// + /// ProductEntity → ProductDto + /// + public static ProductDto ToDto(this ProductEntity entity) + { + if (entity == null) return null; + return new ProductDto + { + Id = entity.Id.ToString(), + Model = entity.Model, + Name = entity.Name, + CategoryId = entity.CategoryId.ToString(), + Manufacturer = entity.Manufacturer, + Description = entity.Description, + ProtocolType = entity.ProtocolType, + MessageModel = entity.MessageModel, + IsEnabled = entity.IsEnabled, + Remark = entity.Remark, + CreateTime = entity.CreateTime + }; + } + + /// + /// List<ProductEntity> → List<ProductDto> + /// + public static List ToDtoList(this List entities) + { + return entities?.Select(e => e.ToDto()).ToList() ?? new List(); + } + + /// + /// ProductDto → ProductEntity(入参映射,IsDel/CreateTime 等服务端管控字段不映射) + /// + public static ProductEntity ToEntity(this ProductDto dto) + { + if (dto == null) return null; + return new ProductEntity + { + Id = ParseId(dto.Id), + Model = dto.Model, + Name = dto.Name, + CategoryId = ParseId(dto.CategoryId), + Manufacturer = dto.Manufacturer, + Description = dto.Description, + ProtocolType = dto.ProtocolType, + MessageModel = dto.MessageModel, + IsEnabled = dto.IsEnabled, + Remark = dto.Remark + }; + } + + /// + /// ProductEntity → ProductOptionDto(下拉选项) + /// + public static ProductOptionDto ToOptionDto(this ProductEntity entity) + { + if (entity == null) return null; + return new ProductOptionDto + { + Id = entity.Id.ToString(), + Model = entity.Model, + Name = entity.Name + }; + } + + /// + /// List<ProductEntity> → List<ProductOptionDto> + /// + public static List ToOptionDtoList(this List entities) + { + return entities?.Select(e => e.ToOptionDto()).ToList() ?? new List(); + } + #endregion + + #region 物模型点 + /// + /// ThingModelPointEntity → ThingModelPointDto + /// + public static ThingModelPointDto ToDto(this ThingModelPointEntity entity) + { + if (entity == null) return null; + return new ThingModelPointDto + { + Id = entity.Id.ToString(), + OwnerType = entity.OwnerType, + OwnerId = entity.OwnerId.ToString(), + Code = entity.Code, + Name = entity.Name, + RegisterType = entity.RegisterType, + Rw = entity.Rw, + DataType = entity.DataType, + Address = entity.Address, + Scale = entity.Scale, + Offset = entity.Offset, + Unit = entity.Unit, + Enabled = entity.Enabled, + Sort = entity.Sort, + Remark = entity.Remark, + CreateTime = entity.CreateTime + }; + } + + /// + /// List<ThingModelPointEntity> → List<ThingModelPointDto> + /// + public static List ToDtoList(this List entities) + { + return entities?.Select(e => e.ToDto()).ToList() ?? new List(); + } + + /// + /// ThingModelPointDto → ThingModelPointEntity(入参映射) + /// + public static ThingModelPointEntity ToEntity(this ThingModelPointDto dto) + { + if (dto == null) return null; + return new ThingModelPointEntity + { + Id = ParseId(dto.Id), + OwnerType = dto.OwnerType, + OwnerId = ParseId(dto.OwnerId), + Code = dto.Code, + Name = dto.Name, + RegisterType = dto.RegisterType, + Rw = dto.Rw, + DataType = dto.DataType, + Address = dto.Address, + Scale = dto.Scale, + Offset = dto.Offset, + Unit = dto.Unit, + Enabled = dto.Enabled, + Sort = dto.Sort, + Remark = dto.Remark + }; + } + #endregion } } diff --git a/Service/Implement/Config/DeviceCommandService.cs b/Service/Implement/Config/DeviceCommandService.cs new file mode 100644 index 0000000..2ee1904 --- /dev/null +++ b/Service/Implement/Config/DeviceCommandService.cs @@ -0,0 +1,178 @@ +using DeviceCommand.Base; +using Model; +using Model.Dto.Config; +using Model.Entity.Config; +using ORM; +using Service.Interface.Config; +using SqlSugar; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace Service.Implement.Config +{ + /// + /// 设备指令下发 服务实现 + /// 目前无网关运行时/采集引擎:每次调用先落一条设备日志(始终可验证), + /// 设备在线时才实际尝试 Modbus 写(GatewayCode 需为 "ip:port",默认 127.0.0.1:502)。 + /// + public class DeviceCommandService : IDeviceCommandService + { + public async Task SendCommandAsync(DeviceCommandDto dto) + { + if (dto == null || !long.TryParse(dto.Id, out var deviceId) || deviceId <= 0) + return Result.Error("设备Id无效"); + if (string.IsNullOrWhiteSpace(dto.PointId) || !long.TryParse(dto.PointId, out var pointId) || pointId <= 0) + return Result.Error("物模型点Id无效"); + + var now = DateTime.Now; + + // 载入设备与点 + var device = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == deviceId && x.IsDel == 0).FirstAsync(); + if (device == null) + return Result.Error("设备不存在或已被删除"); + + var point = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == pointId && x.OwnerType == ThingOwnerTypeEnum.Device && x.OwnerId == deviceId && x.IsDel == 0).FirstAsync(); + if (point == null) + return Result.Error("物模型点不存在或不属于该设备"); + + // 校验可写 + if (point.Rw == ThingRwEnum.ReadOnly) + return Result.Error($"点【{point.Name ?? point.Code}】为只读,无法下发指令"); + if (device.ProtocolType != IotDeviceProtocolEnum.ModbusTcp) + return Result.Error("当前仅支持 Modbus TCP 协议设备下发指令"); + + try + { + // 模拟模式 / 设备离线 → 只记录、不发网络报文 + if (dto.IsSimulated || device.OnlineStatus != IotDeviceOnlineStatusEnum.Online) + { + await WriteLogAsync(device, "Warn", "指令", dto.IsSimulated + ? $"【模拟】点 {point.Name ?? point.Code} 下发值 {dto.Value}(未发送网络报文)" + : $"设备离线,指令已记录但未发送(点 {point.Name ?? point.Code} 值 {dto.Value})"); + return dto.IsSimulated + ? Result.Success() + : Result.Error("设备离线,指令已记录但未发送"); + } + + // 实际 Modbus 写 + var (host, port) = ParseGateway(device.GatewayCode); + using var modbus = new ModbusTcp(); + modbus.ConfigureDevice(host, port, 3000, 3000); + bool connected = await modbus.ConnectAsync(); + if (!connected) + { + await WriteLogAsync(device, "Error", "指令", + $"网关 {device.GatewayCode} 连接失败,指令未发送(点 {point.Name ?? point.Code} 值 {dto.Value})"); + return Result.Error($"网关 {device.GatewayCode} 连接失败,指令未发送"); + } + + string desc; + if (point.RegisterType == ThingRegisterTypeEnum.Coil) + { + bool coil = dto.Value != 0; + await modbus.WriteSingleCoilAsync(device.SlaveId, point.Address, coil); + desc = $"写线圈 {point.Name ?? point.Code} 地址 {point.Address} = {coil}"; + } + else if (point.DataType == ThingDataTypeEnum.Int32 || point.DataType == ThingDataTypeEnum.Float) + { + // 32 位类型占连续 2 个寄存器,走 FC16 连写(Int32 按有符号整数编码,Float 按 IEEE754 编码) + ushort[] words = ToWords(dto.Value, point); + await modbus.WriteMultipleRegistersAsync(device.SlaveId, point.Address, words); + desc = $"写双寄存器 {point.Name ?? point.Code} 地址 {point.Address}~{point.Address + 1} = [{words[0]}, {words[1]}](工程值 {dto.Value})"; + } + else + { + ushort raw = ToRaw(dto.Value, point); + await modbus.WriteSingleRegisterAsync(device.SlaveId, point.Address, raw); + desc = $"写寄存器 {point.Name ?? point.Code} 地址 {point.Address} = {raw}(工程值 {dto.Value})"; + } + + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => new IotDeviceEntity { LastCollectTime = now, LastError = null }) + .Where(x => x.Id == device.Id).ExecuteCommandAsync(); + + await WriteLogAsync(device, "Info", "指令", $"下发成功:{desc}"); + return Result.Success(); + } + catch (Exception ex) + { + await WriteLogAsync(device, "Error", "指令", $"下发失败:{ex.Message}"); + return Result.Error($"指令下发异常:{ex.Message}", ex); + } + } + + /// + /// 工程值 → 寄存器原始值:raw = (value - Offset) / Scale,取整并夹到寄存器范围 + /// Int16 按有符号处理(补码),其余类型按 0..65535 + /// + private static ushort ToRaw(double value, ThingModelPointEntity point) + { + double raw = (value - point.Offset) / point.Scale; + if (point.DataType == ThingDataTypeEnum.Int16) + { + if (raw <= short.MinValue) return unchecked((ushort)short.MinValue); + if (raw >= short.MaxValue) return (ushort)short.MaxValue; + return unchecked((ushort)(short)Math.Round(raw)); + } + if (raw <= 0) return 0; + if (raw >= 65535) return 65535; + return (ushort)Math.Round(raw); + } + + /// + /// 工程值 → 32 位双寄存器字数组(raw32 = (value - Offset) / Scale) + /// Int32 按有符号整数编码,Float 按 IEEE754 编码;字序按 Modbus 常规高字在前(AB CD) + /// + private static ushort[] ToWords(double value, ThingModelPointEntity point) + { + uint bits; + if (point.DataType == ThingDataTypeEnum.Float) + { + bits = BitConverter.SingleToUInt32Bits((float)((value - point.Offset) / point.Scale)); + } + else + { + double raw = Math.Round((value - point.Offset) / point.Scale); + if (raw < int.MinValue) raw = int.MinValue; + if (raw > int.MaxValue) raw = int.MaxValue; + bits = unchecked((uint)(int)raw); + } + return new[] { (ushort)(bits >> 16), (ushort)(bits & 0xFFFF) }; + } + + /// + /// 解析 GatewayCode 为 host:port;非法/缺失回退 127.0.0.1:502 + /// + private static (string host, int port) ParseGateway(string? gatewayCode) + { + if (!string.IsNullOrWhiteSpace(gatewayCode)) + { + var idx = gatewayCode.LastIndexOf(':'); + if (idx > 0 && IPAddress.TryParse(gatewayCode[..idx], out _) && int.TryParse(gatewayCode[(idx + 1)..], out var p) && p > 0) + return (gatewayCode[..idx], p); + } + return ("127.0.0.1", 502); + } + + /// + /// 写一条设备日志(设备独享日志,前端设备日志抽屉可见) + /// + private static async Task WriteLogAsync(IotDeviceEntity device, string level, string logType, string message) + { + var now = DateTime.Now; + await SqlSugarContext.DbContext.Insertable(new DeviceLogEntity + { + DeviceId = device.Id, + DeviceCode = device.Code, + Level = level, + LogType = logType, + Message = message, + LogTime = now, + CreateTime = now + }).ExecuteCommandAsync(); + } + } +} diff --git a/Service/Implement/Config/DeviceService.cs b/Service/Implement/Config/DeviceService.cs index ffdd447..f90c9bd 100644 --- a/Service/Implement/Config/DeviceService.cs +++ b/Service/Implement/Config/DeviceService.cs @@ -7,6 +7,7 @@ using Service.Interface.Config; using SqlSugar; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Service.Implement.Config @@ -17,9 +18,9 @@ namespace Service.Implement.Config public class DeviceService : IDeviceService { /// - /// 分页查询设备列表(支持关键字搜索编号/名称/类型) + /// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选) /// - public async Task>> GetPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword) + public async Task>> GetPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword, long? productId = null) { try { @@ -27,10 +28,13 @@ namespace Service.Implement.Config .Where(x => x.IsDel == 0) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!) || x.DeviceType.Contains(keyword!)) + .WhereIF(productId.HasValue && productId.Value > 0, x => x.ProductId == productId!.Value) .OrderBy(x => x.CreateTime, OrderByType.Desc) .ToPageListAsync(pageIndex, pageSize, total); - return Result>.Success(list.ToDtoList()); + var dtos = list.ToDtoList(); + await FillProductNames(dtos); + return Result>.Success(dtos); } catch (Exception ex) { @@ -50,7 +54,10 @@ namespace Service.Implement.Config .FirstAsync(); if (entity == null) return Result.Error("设备不存在或已被删除"); - return Result.Success(entity.ToDto()); + + var dto = entity.ToDto(); + await FillProductNames(new List { dto }); + return Result.Success(dto); } catch (Exception ex) { @@ -157,5 +164,30 @@ namespace Service.Implement.Config return Result>.Error("查询设备日志失败", ex); } } + + /// + /// 给设备 DTO 填充所属产品的型号/名称(ProductName 展示用) + /// + private async Task FillProductNames(List dtos) + { + if (dtos == null || dtos.Count == 0) + return; + + var ids = dtos.Where(x => long.TryParse(x.ProductId, out var pid) && pid > 0) + .Select(x => long.Parse(x.ProductId!)).Distinct().ToList(); + if (ids.Count == 0) + return; + + var products = await SqlSugarContext.DbContext.Queryable() + .Where(x => ids.Contains(x.Id) && x.IsDel == 0) + .ToListAsync(); + var map = products.ToDictionary(p => p.Id, p => p.Name ?? p.Model); + + foreach (var d in dtos) + { + if (long.TryParse(d.ProductId, out var pid) && map.TryGetValue(pid, out var name)) + d.ProductName = name; + } + } } } diff --git a/Service/Implement/Config/ProductService.cs b/Service/Implement/Config/ProductService.cs index 1254ca9..4934568 100644 --- a/Service/Implement/Config/ProductService.cs +++ b/Service/Implement/Config/ProductService.cs @@ -1,12 +1,255 @@ -using Service.Interface; +using Model; +using Model.Dto.Config; +using Model.Entity.Config; +using Model.Mapper; +using ORM; +using Service.Interface.Config; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; -namespace Service.Implement +namespace Service.Implement.Config { /// - /// 产品管理 服务实现 + /// 产品管理(含产品分类)服务实现 /// public class ProductService : IProductService { - // TODO: 实现 产品管理 相关方法 + // ===================== 产品分类 ===================== + + public async Task>> GetCategoriesAsync() + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .OrderBy(x => x.Sort) + .ToListAsync(); + return Result>.Success(list.ToDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询产品分类失败", ex); + } + } + + public async Task AddCategoryAsync(ProductCategoryDto dto) + { + if (dto == null || string.IsNullOrWhiteSpace(dto.Code)) + return Result.Error("分类编码不能为空"); + + try + { + bool exists = await SqlSugarContext.DbContext.Queryable() + .AnyAsync(x => x.Code == dto.Code && x.IsDel == 0); + if (exists) + return Result.Error($"分类编码【{dto.Code}】已存在"); + + var entity = dto.ToEntity(); + entity.CreateTime = DateTime.Now; + await SqlSugarContext.DbContext.Insertable(entity).ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("新增产品分类失败", ex); + } + } + + public async Task UpdateCategoryAsync(ProductCategoryDto dto) + { + var entity = dto?.ToEntity(); + if (entity == null || entity.Id <= 0) + return Result.Error("分类Id无效"); + + try + { + bool exists = await SqlSugarContext.DbContext.Queryable() + .AnyAsync(x => x.Code == entity.Code && x.IsDel == 0 && x.Id != entity.Id); + if (exists) + return Result.Error($"分类编码【{entity.Code}】已存在"); + + await SqlSugarContext.DbContext.Updateable(entity) + .IgnoreColumns(x => new { x.CreateTime, x.IsDel }) + .ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("修改产品分类失败", ex); + } + } + + public async Task DeleteCategoryAsync(long id) + { + if (id <= 0) + return Result.Error("分类Id无效"); + + try + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id) + .ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("删除产品分类失败", ex); + } + } + + // ===================== 产品 ===================== + + public async Task>> GetPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword, long categoryId) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), + x => x.Model.Contains(keyword!) || (x.Name != null && x.Name.Contains(keyword!))) + .WhereIF(categoryId > 0, x => x.CategoryId == categoryId) + .OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToPageListAsync(pageIndex, pageSize, total); + + var dtos = list.ToDtoList(); + await FillCategoryNames(dtos); + return Result>.Success(dtos); + } + catch (Exception ex) + { + return Result>.Error("查询产品列表失败", ex); + } + } + + public async Task>> GetOptionsAsync() + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0 && x.IsEnabled) + .OrderBy(x => x.Model) + .ToListAsync(); + return Result>.Success(list.ToOptionDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询产品选项失败", ex); + } + } + + public async Task> GetByIdAsync(long id) + { + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0) + .FirstAsync(); + if (entity == null) + return Result.Error("产品不存在或已被删除"); + + var dto = entity.ToDto(); + await FillCategoryNames(new List { dto }); + return Result.Success(dto); + } + catch (Exception ex) + { + return Result.Error("查询产品详情失败", ex); + } + } + + public async Task AddAsync(ProductDto dto) + { + if (dto == null || string.IsNullOrWhiteSpace(dto.Model)) + return Result.Error("产品型号不能为空"); + + try + { + bool exists = await SqlSugarContext.DbContext.Queryable() + .AnyAsync(x => x.Model == dto.Model && x.IsDel == 0); + if (exists) + return Result.Error($"产品型号【{dto.Model}】已存在"); + + var entity = dto.ToEntity(); + entity.CreateTime = DateTime.Now; + await SqlSugarContext.DbContext.Insertable(entity).ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("新增产品失败", ex); + } + } + + public async Task UpdateAsync(ProductDto dto) + { + var entity = dto?.ToEntity(); + if (entity == null || entity.Id <= 0) + return Result.Error("产品Id无效"); + + try + { + bool exists = await SqlSugarContext.DbContext.Queryable() + .AnyAsync(x => x.Model == entity.Model && x.IsDel == 0 && x.Id != entity.Id); + if (exists) + return Result.Error($"产品型号【{entity.Model}】已存在"); + + await SqlSugarContext.DbContext.Updateable(entity) + .IgnoreColumns(x => new { x.CreateTime, x.IsDel }) + .ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("修改产品失败", ex); + } + } + + public async Task DeleteAsync(long id) + { + if (id <= 0) + return Result.Error("产品Id无效"); + + try + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id) + .ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("删除产品失败", ex); + } + } + + /// + /// 给产品 DTO 填充分类名称(供列表/详情展示) + /// + private async Task FillCategoryNames(List dtos) + { + if (dtos == null || dtos.Count == 0) + return; + + var ids = dtos.Where(x => x.CategoryId != null && long.TryParse(x.CategoryId, out _)) + .Select(x => long.Parse(x.CategoryId!)).Distinct().ToList(); + if (ids.Count == 0) + return; + + var categories = await SqlSugarContext.DbContext.Queryable() + .Where(x => ids.Contains(x.Id) && x.IsDel == 0) + .ToListAsync(); + var map = categories.ToDictionary(c => c.Id, c => c.Name); + + foreach (var d in dtos) + { + if (long.TryParse(d.CategoryId, out var cid) && map.TryGetValue(cid, out var name)) + d.CategoryName = name; + } + } } } diff --git a/Service/Implement/Config/ThingPointService.cs b/Service/Implement/Config/ThingPointService.cs new file mode 100644 index 0000000..3f3d022 --- /dev/null +++ b/Service/Implement/Config/ThingPointService.cs @@ -0,0 +1,126 @@ +using Model; +using Model.Dto.Config; +using Model.Entity.Config; +using Model.Mapper; +using ORM; +using Service.Interface.Config; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Service.Implement.Config +{ + /// + /// 物模型点 服务实现(产品/设备通用,按 OwnerType+OwnerId 归属) + /// + public class ThingPointService : IThingPointService + { + public async Task>> GetListAsync(int ownerType, long ownerId, string? keyword) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .Where(x => x.OwnerType == (ThingOwnerTypeEnum)ownerType && x.OwnerId == ownerId) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), + x => x.Code.Contains(keyword!) || (x.Name != null && x.Name.Contains(keyword!))) + .OrderBy(x => x.Sort) + .ToListAsync(); + return Result>.Success(list.ToDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询物模型点失败", ex); + } + } + + public async Task> GetByIdAsync(long id) + { + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0) + .FirstAsync(); + if (entity == null) + return Result.Error("物模型点不存在或已被删除"); + return Result.Success(entity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("查询物模型点失败", ex); + } + } + + public async Task AddAsync(ThingModelPointDto dto) + { + if (dto == null || string.IsNullOrWhiteSpace(dto.Code)) + return Result.Error("点编码不能为空"); + if (dto.OwnerId == null || !long.TryParse(dto.OwnerId, out var ownerId) || ownerId <= 0) + return Result.Error("归属对象Id无效"); + + try + { + bool exists = await SqlSugarContext.DbContext.Queryable() + .AnyAsync(x => x.OwnerType == dto.OwnerType && x.OwnerId == ownerId + && x.Code == dto.Code && x.IsDel == 0); + if (exists) + return Result.Error($"点编码【{dto.Code}】在该归属下已存在"); + + var entity = dto.ToEntity(); + entity.CreateTime = DateTime.Now; + await SqlSugarContext.DbContext.Insertable(entity).ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("新增物模型点失败", ex); + } + } + + public async Task UpdateAsync(ThingModelPointDto dto) + { + var entity = dto?.ToEntity(); + if (entity == null || entity.Id <= 0) + return Result.Error("物模型点Id无效"); + + try + { + bool exists = await SqlSugarContext.DbContext.Queryable() + .AnyAsync(x => x.OwnerType == entity.OwnerType && x.OwnerId == entity.OwnerId + && x.Code == entity.Code && x.IsDel == 0 && x.Id != entity.Id); + if (exists) + return Result.Error($"点编码【{entity.Code}】在该归属下已存在"); + + await SqlSugarContext.DbContext.Updateable(entity) + .IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.OwnerType, x.OwnerId }) + .ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("修改物模型点失败", ex); + } + } + + public async Task DeleteAsync(long id) + { + if (id <= 0) + return Result.Error("物模型点Id无效"); + + try + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id) + .ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("删除物模型点失败", ex); + } + } + } +} diff --git a/Service/Implement/Inspection/AlertRuleService.cs b/Service/Implement/Inspection/AlertRuleService.cs index aaf748d..b51383a 100644 --- a/Service/Implement/Inspection/AlertRuleService.cs +++ b/Service/Implement/Inspection/AlertRuleService.cs @@ -16,14 +16,14 @@ namespace Service.Implement public class AlertRuleService : IAlertRuleService { /// - /// 获取全部告警规则列表 + /// 获取全局告警规则列表(OwnerType=0,不含产品/设备归属规则) /// public async Task>> GetListAsync() { try { var list = await SqlSugarContext.DbContext.Queryable() - .Where(x => x.IsDel == 0) + .Where(x => x.IsDel == 0 && x.OwnerType == 0) .OrderBy(x => x.CreateTime, SqlSugar.OrderByType.Desc) .ToListAsync(); @@ -36,6 +36,27 @@ namespace Service.Implement } } + /// + /// 获取某归属对象下的规则列表(OwnerType=产品/设备,可选按点过滤) + /// + public async Task>> GetOwnerRulesAsync(int ownerType, long ownerId, string? pointCode) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0 && x.OwnerType == (byte)ownerType && x.OwnerId == ownerId) + .WhereIF(!string.IsNullOrWhiteSpace(pointCode), x => x.PointCode == pointCode) + .OrderBy(x => x.CreateTime, SqlSugar.OrderByType.Desc) + .ToListAsync(); + + return Result>.Success(list.ToDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询归属告警规则失败", ex); + } + } + /// /// 新增告警规则(DTO → Entity,string Id 转 long) /// diff --git a/Service/Interface/Config/IDeviceCommandService.cs b/Service/Interface/Config/IDeviceCommandService.cs new file mode 100644 index 0000000..ba3385b --- /dev/null +++ b/Service/Interface/Config/IDeviceCommandService.cs @@ -0,0 +1,18 @@ +using Model; +using Model.Dto.Config; +using System.Threading.Tasks; + +namespace Service.Interface.Config +{ + /// + /// 设备指令下发 服务接口 + /// + public interface IDeviceCommandService + { + /// + /// 按物模型可写点向设备下发指令 + /// 每次调用都会写入设备日志;网关不可达时优雅降级(记录未连通)。 + /// + Task SendCommandAsync(DeviceCommandDto dto); + } +} diff --git a/Service/Interface/Config/IDeviceService.cs b/Service/Interface/Config/IDeviceService.cs index 34c2e2c..680d191 100644 --- a/Service/Interface/Config/IDeviceService.cs +++ b/Service/Interface/Config/IDeviceService.cs @@ -12,9 +12,9 @@ namespace Service.Interface.Config public interface IDeviceService { /// - /// 分页查询设备列表(支持关键字搜索编号/名称/类型) + /// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选) /// - Task>> GetPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword); + Task>> GetPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword, long? productId = null); /// /// 根据 Id 获取设备详情 diff --git a/Service/Interface/Config/IProductService.cs b/Service/Interface/Config/IProductService.cs index c2f8448..d0b6123 100644 --- a/Service/Interface/Config/IProductService.cs +++ b/Service/Interface/Config/IProductService.cs @@ -1,10 +1,45 @@ -namespace Service.Interface +using Model; +using Model.Dto.Config; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Service.Interface.Config { /// - /// 产品管理 服务接口 + /// 产品管理(含产品分类)服务接口 /// public interface IProductService { - // TODO: 定义 产品管理 相关方法 + // ===== 产品分类 ===== + /// 产品分类列表 + Task>> GetCategoriesAsync(); + + /// 新增产品分类 + Task AddCategoryAsync(ProductCategoryDto dto); + + /// 修改产品分类 + Task UpdateCategoryAsync(ProductCategoryDto dto); + + /// 删除产品分类(软删除) + Task DeleteCategoryAsync(long id); + + // ===== 产品 ===== + /// 分页查询产品列表(关键字搜型号/名称;可按分类筛选) + Task>> GetPagedAsync(int pageIndex, int pageSize, SqlSugar.RefAsync total, string? keyword, long categoryId); + + /// 下拉选项(不分页,设备选择所属产品用) + Task>> GetOptionsAsync(); + + /// 产品详情 + Task> GetByIdAsync(long id); + + /// 新增产品(型号唯一校验) + Task AddAsync(ProductDto dto); + + /// 修改产品 + Task UpdateAsync(ProductDto dto); + + /// 删除产品(软删除) + Task DeleteAsync(long id); } } diff --git a/Service/Interface/Config/IThingPointService.cs b/Service/Interface/Config/IThingPointService.cs new file mode 100644 index 0000000..e77f435 --- /dev/null +++ b/Service/Interface/Config/IThingPointService.cs @@ -0,0 +1,28 @@ +using Model; +using Model.Dto.Config; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Service.Interface.Config +{ + /// + /// 物模型点 服务接口(产品/设备通用,按 OwnerType+OwnerId 归属) + /// + public interface IThingPointService + { + /// 查询某归属下的点列表 + Task>> GetListAsync(int ownerType, long ownerId, string? keyword); + + /// 查询单点详情 + Task> GetByIdAsync(long id); + + /// 新增点(同一归属内 Code 唯一) + Task AddAsync(ThingModelPointDto dto); + + /// 修改点 + Task UpdateAsync(ThingModelPointDto dto); + + /// 删除点(软删除) + Task DeleteAsync(long id); + } +} diff --git a/Service/Interface/Inspection/IAlertRuleService.cs b/Service/Interface/Inspection/IAlertRuleService.cs index 751e48f..ff61697 100644 --- a/Service/Interface/Inspection/IAlertRuleService.cs +++ b/Service/Interface/Inspection/IAlertRuleService.cs @@ -1,6 +1,5 @@ using Model; using Model.Dto.Inspection; -using Model.Entity.Inspection; using System.Collections.Generic; using System.Threading.Tasks; @@ -12,10 +11,15 @@ namespace Service.Interface public interface IAlertRuleService { /// - /// 获取全部告警规则列表 + /// 获取全局告警规则列表(OwnerType=0,不含产品/设备归属规则) /// Task>> GetListAsync(); + /// + /// 获取某归属对象下的规则列表(OwnerType=产品/设备,可选按点过滤) + /// + Task>> GetOwnerRulesAsync(int ownerType, long ownerId, string? pointCode); + /// /// 新增告警规则(前端传 DTO,Id 为 string) /// diff --git a/Service/Service.csproj b/Service/Service.csproj index f0af90e..109571b 100644 --- a/Service/Service.csproj +++ b/Service/Service.csproj @@ -13,6 +13,7 @@ +