Files
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

251 lines
10 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 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;
}
}
}
}