- 网关管理: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>
251 lines
10 KiB
C#
251 lines
10 KiB
C#
using Model;
|
||
using Model.Dto.Config;
|
||
using Model.Entity.Config;
|
||
using Model.Mapper;
|
||
using ORM;
|
||
using Service.Interface;
|
||
using Service.Interface.Config;
|
||
using SqlSugar;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text.Json;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace Service.Implement.Config
|
||
{
|
||
/// <summary>
|
||
/// IOT设备管理 服务实现
|
||
/// </summary>
|
||
public class DeviceService : IDeviceService
|
||
{
|
||
private readonly IAuditRecorder _audit;
|
||
|
||
public DeviceService(IAuditRecorder audit)
|
||
{
|
||
_audit = audit;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选)
|
||
/// </summary>
|
||
public async Task<Result<List<IotDeviceDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword, long? productId = null, long? gatewayId = null)
|
||
{
|
||
try
|
||
{
|
||
var list = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||
.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)
|
||
.WhereIF(gatewayId.HasValue && gatewayId.Value > 0, x => x.GatewayId == gatewayId!.Value)
|
||
.OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||
.ToPageListAsync(pageIndex, pageSize, total);
|
||
|
||
var dtos = list.ToDtoList();
|
||
await FillProductNames(dtos);
|
||
await FillGatewayNames(dtos);
|
||
return Result<List<IotDeviceDto>>.Success(dtos);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result<List<IotDeviceDto>>.Error("查询设备列表失败", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据 Id 获取设备详情
|
||
/// </summary>
|
||
public async Task<Result<IotDeviceDto>> GetByIdAsync(long id)
|
||
{
|
||
try
|
||
{
|
||
var entity = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||
.Where(x => x.Id == id && x.IsDel == 0)
|
||
.FirstAsync();
|
||
if (entity == null)
|
||
return Result<IotDeviceDto>.Error("设备不存在或已被删除");
|
||
|
||
var dto = entity.ToDto();
|
||
await FillProductNames(new List<IotDeviceDto> { dto });
|
||
await FillGatewayNames(new List<IotDeviceDto> { dto });
|
||
return Result<IotDeviceDto>.Success(dto);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result<IotDeviceDto>.Error("查询设备详情失败", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增设备(校验编号唯一性)
|
||
/// </summary>
|
||
public async Task<Result> AddAsync(IotDeviceDto dto)
|
||
{
|
||
if (dto == null || string.IsNullOrWhiteSpace(dto.Code))
|
||
return Result.Error("设备编号不能为空");
|
||
if (string.IsNullOrWhiteSpace(dto.Name))
|
||
return Result.Error("设备名称不能为空");
|
||
|
||
try
|
||
{
|
||
bool exists = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||
.AnyAsync(x => x.Code == dto.Code && x.IsDel == 0);
|
||
if (exists)
|
||
return Result.Error($"设备编号【{dto.Code}】已存在");
|
||
|
||
var entity = dto.ToEntity();
|
||
entity.CreateTime = DateTime.Now;
|
||
// 初始离线
|
||
entity.OnlineStatus = IotDeviceOnlineStatusEnum.Offline;
|
||
await SqlSugarContext.DbContext.Insertable(entity).ExecuteCommandAsync();
|
||
|
||
await _audit.RecordAsync("Create", "IotDevice", entity.Id, entity.Id,
|
||
null, JsonSerializer.Serialize(new { entity.Code, entity.Name, entity.DeviceType, entity.GatewayId, entity.SlaveId }),
|
||
description: $"新增设备 {entity.Code}({entity.Name})");
|
||
return Result.Success();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result.Error("新增设备失败", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 修改设备(在线状态等运行字段不允许被前端覆盖)
|
||
/// </summary>
|
||
public async Task<Result> UpdateAsync(IotDeviceDto dto)
|
||
{
|
||
var entity = dto?.ToEntity();
|
||
if (entity == null || entity.Id <= 0)
|
||
return Result.Error("设备Id无效");
|
||
|
||
try
|
||
{
|
||
// 编号变更时校验唯一性
|
||
bool exists = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||
.AnyAsync(x => x.Code == entity.Code && x.IsDel == 0 && x.Id != entity.Id);
|
||
if (exists)
|
||
return Result.Error($"设备编号【{entity.Code}】已存在");
|
||
|
||
// 操作前值(审计):先查旧记录
|
||
var oldEntity = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||
.Where(x => x.Id == entity.Id).FirstAsync();
|
||
|
||
await SqlSugarContext.DbContext.Updateable(entity)
|
||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.OnlineStatus, x.LastCollectTime, x.LastError })
|
||
.ExecuteCommandAsync();
|
||
|
||
await _audit.RecordAsync("Update", "IotDevice", entity.Id, entity.Id,
|
||
oldValue: oldEntity == null ? null : JsonSerializer.Serialize(new { oldEntity.Code, oldEntity.Name, oldEntity.DeviceType, oldEntity.GatewayId, oldEntity.SlaveId }),
|
||
newValue: JsonSerializer.Serialize(new { entity.Code, entity.Name, entity.DeviceType, entity.GatewayId, entity.SlaveId }),
|
||
description: $"修改设备 {entity.Code}({entity.Name})");
|
||
return Result.Success();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result.Error("修改设备失败", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除设备(软删除:IsDel 置 1)
|
||
/// </summary>
|
||
public async Task<Result> DeleteAsync(long id)
|
||
{
|
||
if (id <= 0)
|
||
return Result.Error("设备Id无效");
|
||
|
||
try
|
||
{
|
||
var oldEntity = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
|
||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||
|
||
await SqlSugarContext.DbContext.Updateable<IotDeviceEntity>()
|
||
.SetColumns(x => x.IsDel == 1)
|
||
.Where(x => x.Id == id)
|
||
.ExecuteCommandAsync();
|
||
|
||
await _audit.RecordAsync("Delete", "IotDevice", id, id,
|
||
oldValue: oldEntity == null ? null : JsonSerializer.Serialize(new { oldEntity.Code, oldEntity.Name }),
|
||
description: $"删除设备 {oldEntity?.Code}({oldEntity?.Name})");
|
||
return Result.Success();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result.Error("删除设备失败", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询指定设备的日志(设备独享日志)
|
||
/// </summary>
|
||
public async Task<Result<List<DeviceLogDto>>> GetDeviceLogsAsync(long deviceId, int pageIndex, int pageSize, RefAsync<int> total)
|
||
{
|
||
try
|
||
{
|
||
var list = await SqlSugarContext.DbContext.Queryable<DeviceLogEntity>()
|
||
.Where(x => x.DeviceId == deviceId && x.IsDel == 0)
|
||
.OrderBy(x => x.LogTime, OrderByType.Desc)
|
||
.ToPageListAsync(pageIndex, pageSize, total);
|
||
|
||
return Result<List<DeviceLogDto>>.Success(list.ToDtoList());
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return Result<List<DeviceLogDto>>.Error("查询设备日志失败", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 给设备 DTO 填充所属网关名称(GatewayName 展示用)
|
||
/// </summary>
|
||
private async Task FillGatewayNames(List<IotDeviceDto> dtos)
|
||
{
|
||
if (dtos == null || dtos.Count == 0)
|
||
return;
|
||
|
||
var ids = dtos.Where(x => long.TryParse(x.GatewayId, out var gid) && gid > 0)
|
||
.Select(x => long.Parse(x.GatewayId!)).Distinct().ToList();
|
||
if (ids.Count == 0)
|
||
return;
|
||
|
||
var gateways = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
|
||
.Where(x => ids.Contains(x.Id) && x.IsDel == 0)
|
||
.ToListAsync();
|
||
var map = gateways.ToDictionary(g => g.Id, g => $"{g.Code}({g.Name})");
|
||
|
||
foreach (var d in dtos)
|
||
{
|
||
if (long.TryParse(d.GatewayId, out var gid) && map.TryGetValue(gid, out var name))
|
||
d.GatewayName = name;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 给设备 DTO 填充所属产品的型号/名称(ProductName 展示用)
|
||
/// </summary>
|
||
private async Task FillProductNames(List<IotDeviceDto> 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<ProductEntity>()
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|