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 @@
+