Files
IOT_API/Service/Implement/Config/DeviceCommandService.cs
dengbolingandClaude Sonnet 4.6 6aca0297bb feat: 网关管理模块 + RBAC权限认证 + 组织架构 + 审计日志
- 网关管理:GatewayEntity/Service/Controller CRUD + 测试连接;设备 GatewayId 外键关联
- JWT 认证:登录签发 Token(权限编码写入 Claims)、RequirePermission 权限过滤器、CurrentUser 上下文
- RBAC:用户/角色/权限实体与 CRUD,预定义 4 角色 + 6 权限种子(admin/admin123)
- 组织架构:sys_org 固定层级树(公司/实验室/部门/班组白夜班),层级校验,用户挂 OrgId/岗位/技能标签
- 数据权限:角色 DataScope(全部/本组织及下级),用户列表按组织子树过滤
- 防锁死保护:禁止删/禁自己,保证至少一名活跃管理员,角色摘除 user:manage 前校验
- 审计日志:AuditRecorder 接入设备增删改/指令下发/登录登出/组织变更,AuditController 查询
- 设备指令下发按网关表取连接参数;设备列表支持 gatewayId/productId 筛选

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-10 17:31:39 +08:00

188 lines
8.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using DeviceCommand.Base;
using Model;
using Model.Dto.Config;
using Model.Entity.Config;
using ORM;
using Service.Interface;
using Service.Interface.Config;
using SqlSugar;
using System;
using System.Net;
using System.Threading.Tasks;
namespace Service.Implement.Config
{
/// <summary>
/// 设备指令下发 服务实现
/// 目前无网关运行时/采集引擎:每次调用先落一条设备日志(始终可验证),
/// 设备在线时才实际尝试 Modbus 写(连接参数从网关表按 GatewayId 加载)。
/// 每次下发都写审计日志(操作人/角色/时间/前后值),满足实验室审计要求。
/// </summary>
public class DeviceCommandService : IDeviceCommandService
{
private readonly IAuditRecorder _audit;
public DeviceCommandService(IAuditRecorder audit)
{
_audit = audit;
}
public async Task<Result> 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<IotDeviceEntity>()
.Where(x => x.Id == deviceId && x.IsDel == 0).FirstAsync();
if (device == null)
return Result.Error("设备不存在或已被删除");
var point = await SqlSugarContext.DbContext.Queryable<ThingModelPointEntity>()
.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("设备离线,指令已记录但未发送");
}
// 从网关表加载连接参数
var gateway = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Id == device.GatewayId && x.IsDel == 0).FirstAsync();
if (gateway == null)
return Result.Error($"设备关联的网关不存在(GatewayId={device.GatewayId}");
if (string.IsNullOrWhiteSpace(gateway.Host) || !gateway.Port.HasValue)
return Result.Error($"网关「{gateway.Code}」未配置 IP 地址或端口号");
var host = gateway.Host!;
var port = gateway.Port!.Value;
// 实际 Modbus 写
using var modbus = new ModbusTcp();
modbus.ConfigureDevice(host, port, 3000, 3000);
bool connected = await modbus.ConnectAsync();
if (!connected)
{
await WriteLogAsync(device, "Error", "指令",
$"网关 {gateway.Code}({host}:{port}) 连接失败,指令未发送(点 {point.Name ?? point.Code} 值 {dto.Value}");
return Result.Error($"网关 {gateway.Code} 连接失败,指令未发送");
}
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<IotDeviceEntity>()
.SetColumns(x => new IotDeviceEntity { LastCollectTime = now, LastError = null })
.Where(x => x.Id == device.Id).ExecuteCommandAsync();
await WriteLogAsync(device, "Info", "指令", $"下发成功:{desc}");
await _audit.RecordAsync("Command", "IotDevice", device.Id, device.Id,
oldValue: null,
newValue: $"点={point.Code}, 值={dto.Value}, 模拟={dto.IsSimulated}",
description: $"设备 {device.Code} 下发指令:{desc}");
return Result.Success();
}
catch (Exception ex)
{
await WriteLogAsync(device, "Error", "指令", $"下发失败:{ex.Message}");
return Result.Error($"指令下发异常:{ex.Message}", ex);
}
}
/// <summary>
/// 工程值 → 寄存器原始值:raw = (value - Offset) / Scale,取整并夹到寄存器范围
/// Int16 按有符号处理(补码),其余类型按 0..65535
/// </summary>
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);
}
/// <summary>
/// 工程值 → 32 位双寄存器字数组(raw32 = (value - Offset) / Scale
/// Int32 按有符号整数编码,Float 按 IEEE754 编码;字序按 Modbus 常规高字在前(AB CD
/// </summary>
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) };
}
/// <summary>
/// 写一条设备日志(设备独享日志,前端设备日志抽屉可见)
/// </summary>
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();
}
}
}