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>
This commit is contained in:
@@ -1,12 +1,409 @@
|
||||
using Model;
|
||||
using Model.Dto.System;
|
||||
using Model.Entity.System;
|
||||
using Model.Mapper;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户管理 服务实现
|
||||
/// 数据权限:当前用户角色 DataScope=1 看全部;=2 只能看自己组织及下级组织下的用户
|
||||
/// </summary>
|
||||
public class UserService : IUserService
|
||||
{
|
||||
// TODO: 实现 用户管理 相关方法
|
||||
private readonly IOrgService _orgService;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
|
||||
public UserService(IOrgService orgService, ICurrentUser currentUser)
|
||||
{
|
||||
_orgService = orgService;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<Result<List<UserDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, long? roleId = null, long? orgId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 按角色筛选时先查出该角色下的用户Id
|
||||
List<long>? userIdsInRole = null;
|
||||
if (roleId.HasValue && roleId.Value > 0)
|
||||
{
|
||||
userIdsInRole = await SqlSugarContext.DbContext.Queryable<UserRoleEntity>()
|
||||
.Where(x => x.RoleId == roleId!.Value)
|
||||
.Select(x => x.UserId)
|
||||
.ToListAsync();
|
||||
if (userIdsInRole.Count == 0)
|
||||
return Result<List<UserDto>>.Success(new List<UserDto>());
|
||||
}
|
||||
|
||||
// 数据范围过滤:DataScope=2 时只允许看本组织及下级的用户
|
||||
List<long>? allowedOrgIds = null;
|
||||
if (_currentUser.IsAuthenticated && _currentUser.DataScope != 1)
|
||||
{
|
||||
var me = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.Id == _currentUser.UserId).FirstAsync();
|
||||
if (me != null && me.OrgId > 0)
|
||||
allowedOrgIds = await _orgService.GetSubtreeIdsAsync(me.OrgId);
|
||||
else
|
||||
return Result<List<UserDto>>.Success(new List<UserDto>()); // 无组织归属则看不到任何用户
|
||||
}
|
||||
|
||||
// 前端按组织节点筛选时叠加(同时受数据范围约束)
|
||||
List<long>? orgFilterIds = null;
|
||||
if (orgId.HasValue && orgId.Value > 0)
|
||||
{
|
||||
orgFilterIds = await _orgService.GetSubtreeIdsAsync(orgId.Value);
|
||||
if (allowedOrgIds != null)
|
||||
orgFilterIds = orgFilterIds.Where(allowedOrgIds.Contains).ToList();
|
||||
}
|
||||
|
||||
var list = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.UserName.Contains(keyword!) || (x.RealName != null && x.RealName.Contains(keyword!)))
|
||||
.WhereIF(userIdsInRole != null, x => userIdsInRole!.Contains(x.Id))
|
||||
.WhereIF(allowedOrgIds != null, x => allowedOrgIds!.Contains(x.OrgId))
|
||||
.WhereIF(orgFilterIds != null, x => orgFilterIds!.Contains(x.OrgId))
|
||||
.OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
|
||||
var dtos = list.ToDtoList();
|
||||
await FillRolesAsync(dtos);
|
||||
await FillOrgNamesAsync(dtos);
|
||||
return Result<List<UserDto>>.Success(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<UserDto>>.Error("查询用户列表失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<UserDto>> GetByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<UserDto>.Error("用户不存在或已被删除");
|
||||
var dto = entity.ToDto();
|
||||
await FillRolesAsync(new List<UserDto> { dto });
|
||||
await FillOrgNamesAsync(new List<UserDto> { dto });
|
||||
return Result<UserDto>.Success(dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<UserDto>.Error("查询用户详情失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<UserDto>> AddAsync(UserDto dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.UserName))
|
||||
return Result<UserDto>.Error("用户名不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.UserName == dto.UserName && x.IsDel == 0).AnyAsync();
|
||||
if (exists) return Result<UserDto>.Error($"用户名「{dto.UserName}」已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.CreateTime = DateTime.Now;
|
||||
// 新增用户初始密码必填(前端通过 InitialPassword 明文传入,落库前 PBKDF2 哈希)
|
||||
var initialPassword = dto.InitialPassword;
|
||||
if (string.IsNullOrWhiteSpace(initialPassword) || initialPassword.Length < 6)
|
||||
return Result<UserDto>.Error("初始密码不能为空且长度不少于 6 位");
|
||||
entity.PasswordHash = PasswordHelper.Hash(initialPassword);
|
||||
|
||||
// 角色先解析校验再落库,避免"用户已插入但角色解析失败"留下无角色用户
|
||||
var newRoleIds = new List<long>();
|
||||
if (dto.RoleIds != null && dto.RoleIds.Count > 0)
|
||||
{
|
||||
foreach (var r in dto.RoleIds)
|
||||
{
|
||||
if (!long.TryParse(r, out var rid) || rid <= 0)
|
||||
return Result<UserDto>.Error($"角色Id无效:{r}");
|
||||
newRoleIds.Add(rid);
|
||||
}
|
||||
}
|
||||
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
|
||||
if (newRoleIds.Count > 0)
|
||||
await ReplaceUserRolesAsync(id, newRoleIds);
|
||||
|
||||
var result = entity.ToDto();
|
||||
await FillRolesAsync(new List<UserDto> { result });
|
||||
return Result<UserDto>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<UserDto>.Error("新增用户失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<UserDto>> UpdateAsync(UserDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<UserDto>.Error("用户Id无效");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<UserDto>.Error("用户不存在或已被删除");
|
||||
|
||||
var nameExists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.UserName == dto.UserName && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (nameExists) return Result<UserDto>.Error($"用户名「{dto.UserName}」已存在");
|
||||
|
||||
// 防锁死:把自己禁用不允许;禁用或改角色后必须仍存在可管理用户的管理员
|
||||
if (dto.IsEnabled == 0 && id == _currentUser.UserId)
|
||||
return Result<UserDto>.Error("不能禁用当前登录的账号");
|
||||
if (dto.IsEnabled == 0 || dto.RoleIds != null)
|
||||
{
|
||||
var guard = await EnsureAnotherActiveAdminAsync(id);
|
||||
if (guard != null) return Result<UserDto>.Error(guard.Msg);
|
||||
}
|
||||
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
// 密码、登录时间不允许在此接口覆盖
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.PasswordHash, x.LastLoginTime })
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 角色变化时同步更新关联(传入 null 表示不改角色)
|
||||
if (dto.RoleIds != null)
|
||||
{
|
||||
var roleIds = dto.RoleIds.Select(long.Parse).Where(r => r > 0).ToList();
|
||||
await ReplaceUserRolesAsync(id, roleIds);
|
||||
}
|
||||
|
||||
var result = newEntity.ToDto();
|
||||
await FillRolesAsync(new List<UserDto> { result });
|
||||
return Result<UserDto>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<UserDto>.Error("修改用户失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("用户Id无效");
|
||||
if (id == _currentUser.UserId)
|
||||
return Result.Error("不能删除当前登录的账号");
|
||||
|
||||
try
|
||||
{
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).AnyAsync();
|
||||
if (!exists) return Result.Error("用户不存在或已被删除");
|
||||
|
||||
// 防锁死:删除后必须仍存在可管理用户的管理员
|
||||
var guard = await EnsureAnotherActiveAdminAsync(id);
|
||||
if (guard != null) return guard;
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<UserEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
await SqlSugarContext.DbContext.Deleteable<UserRoleEntity>()
|
||||
.Where(x => x.UserId == id).ExecuteCommandAsync();
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除用户失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> AssignRolesAsync(long userId, List<long> roleIds)
|
||||
{
|
||||
if (userId <= 0) return Result.Error("用户Id无效");
|
||||
try
|
||||
{
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.Id == userId && x.IsDel == 0).AnyAsync();
|
||||
if (!exists) return Result.Error("用户不存在或已被删除");
|
||||
|
||||
// 防锁死:改角色后必须仍存在可管理用户的管理员
|
||||
var guard = await EnsureAnotherActiveAdminAsync(userId);
|
||||
if (guard != null) return guard;
|
||||
|
||||
await ReplaceUserRolesAsync(userId, roleIds ?? new List<long>());
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("分配角色失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> ResetPasswordAsync(long userId, string newPassword)
|
||||
{
|
||||
if (userId <= 0) return Result.Error("用户Id无效");
|
||||
if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
|
||||
return Result.Error("新密码不能为空且长度不少于 6 位");
|
||||
try
|
||||
{
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.Id == userId && x.IsDel == 0).AnyAsync();
|
||||
if (!exists) return Result.Error("用户不存在或已被删除");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<UserEntity>()
|
||||
.SetColumns(x => x.PasswordHash == PasswordHelper.Hash(newPassword))
|
||||
.Where(x => x.Id == userId).ExecuteCommandAsync();
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("重置密码失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> SetEnabledAsync(long userId, bool enabled)
|
||||
{
|
||||
if (userId <= 0) return Result.Error("用户Id无效");
|
||||
if (!enabled && userId == _currentUser.UserId)
|
||||
return Result.Error("不能禁用当前登录的账号");
|
||||
try
|
||||
{
|
||||
// 防锁死:禁用后必须仍存在可管理用户的管理员
|
||||
if (!enabled)
|
||||
{
|
||||
var guard = await EnsureAnotherActiveAdminAsync(userId);
|
||||
if (guard != null) return guard;
|
||||
}
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<UserEntity>()
|
||||
.SetColumns(x => x.IsEnabled == (byte)(enabled ? 1 : 0))
|
||||
.Where(x => x.Id == userId).ExecuteCommandAsync();
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("设置用户状态失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<UserOptionDto>>> GetOptionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<UserEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.IsEnabled == 1)
|
||||
.OrderBy(x => x.UserName)
|
||||
.ToListAsync();
|
||||
return Result<List<UserOptionDto>>.Success(list.ToOptionDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<UserOptionDto>>.Error("查询用户选项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>全量覆盖用户-角色关联</summary>
|
||||
private static async Task ReplaceUserRolesAsync(long userId, List<long> roleIds)
|
||||
{
|
||||
var db = SqlSugarContext.DbContext;
|
||||
await db.Deleteable<UserRoleEntity>().Where(x => x.UserId == userId).ExecuteCommandAsync();
|
||||
if (roleIds.Count > 0)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var links = roleIds.Distinct().Select(r => new UserRoleEntity { UserId = userId, RoleId = r, CreateTime = now }).ToList();
|
||||
await db.Insertable(links).ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 防锁死校验:排除 excludeUserId 后,系统必须仍存在「已启用且拥有 user:manage 权限」的用户,
|
||||
/// 否则返回 Error(保证管理员不会把所有管理员删光/禁用光导致无人可登录管理)
|
||||
/// </summary>
|
||||
private async Task<Result?> EnsureAnotherActiveAdminAsync(long excludeUserId)
|
||||
{
|
||||
var db = SqlSugarContext.DbContext;
|
||||
var permId = await db.Queryable<PermissionEntity>()
|
||||
.Where(x => x.Code == "user:manage" && x.IsDel == 0)
|
||||
.Select(x => x.Id).FirstAsync();
|
||||
if (permId == 0) return null; // 权限码不存在时不拦,避免权限体系变更后管理功能彻底锁死
|
||||
|
||||
var roleIds = await db.Queryable<RolePermissionEntity>()
|
||||
.Where(x => x.PermissionId == permId)
|
||||
.Select(x => x.RoleId).ToListAsync();
|
||||
if (roleIds.Count == 0) return null;
|
||||
|
||||
var userIds = await db.Queryable<UserRoleEntity>()
|
||||
.Where(x => roleIds.Contains(x.RoleId))
|
||||
.Select(x => x.UserId).ToListAsync();
|
||||
if (userIds.Count == 0)
|
||||
return Result.Error("操作被拒绝:系统将没有任何可管理用户的管理员");
|
||||
|
||||
var exists = await db.Queryable<UserEntity>()
|
||||
.AnyAsync(x => userIds.Contains(x.Id) && x.IsDel == 0 && x.IsEnabled == 1 && x.Id != excludeUserId);
|
||||
return exists ? null : Result.Error("操作被拒绝:系统至少需要保留一名已启用且拥有用户管理权限的账号");
|
||||
}
|
||||
|
||||
/// <summary>给用户 DTO 填充所属组织名称(组织链路径,如 公司/实验室/部门)</summary>
|
||||
private async Task FillOrgNamesAsync(List<UserDto> dtos)
|
||||
{
|
||||
if (dtos == null || dtos.Count == 0) return;
|
||||
var orgIds = dtos.Where(d => long.TryParse(d.OrgId, out var oid) && oid > 0)
|
||||
.Select(d => long.Parse(d.OrgId)).Distinct().ToList();
|
||||
if (orgIds.Count == 0) return;
|
||||
|
||||
var allOrgs = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
|
||||
.Where(x => x.IsDel == 0).ToListAsync();
|
||||
var orgMap = allOrgs.ToDictionary(o => o.Id, o => o);
|
||||
|
||||
foreach (var d in dtos)
|
||||
{
|
||||
if (!long.TryParse(d.OrgId, out var oid) || !orgMap.TryGetValue(oid, out var org))
|
||||
continue;
|
||||
// 沿父级向上拼组织路径
|
||||
var path = new List<string>();
|
||||
var cursor = org;
|
||||
var guard = 0;
|
||||
while (cursor != null && guard++ < 10)
|
||||
{
|
||||
path.Insert(0, cursor.Name);
|
||||
cursor = cursor.ParentId > 0 && orgMap.TryGetValue(cursor.ParentId, out var parent) ? parent : null;
|
||||
}
|
||||
d.OrgName = string.Join(" / ", path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>给用户 DTO 填充角色Id列表与角色名称</summary>
|
||||
private static async Task FillRolesAsync(List<UserDto> dtos)
|
||||
{
|
||||
if (dtos == null || dtos.Count == 0) return;
|
||||
var db = SqlSugarContext.DbContext;
|
||||
var userIds = dtos.Select(d => long.Parse(d.Id)).ToList();
|
||||
|
||||
var links = await db.Queryable<UserRoleEntity>()
|
||||
.Where(x => userIds.Contains(x.UserId)).ToListAsync();
|
||||
var roleIds = links.Select(l => l.RoleId).Distinct().ToList();
|
||||
var roles = roleIds.Count > 0
|
||||
? await db.Queryable<RoleEntity>().Where(x => roleIds.Contains(x.Id) && x.IsDel == 0).ToListAsync()
|
||||
: new List<RoleEntity>();
|
||||
var roleMap = roles.ToDictionary(r => r.Id, r => r.Name);
|
||||
|
||||
foreach (var d in dtos)
|
||||
{
|
||||
var uid = long.Parse(d.Id);
|
||||
var myRoleIds = links.Where(l => l.UserId == uid).Select(l => l.RoleId).ToList();
|
||||
d.RoleIds = myRoleIds.Select(r => r.ToString()).ToList();
|
||||
d.RoleNames = string.Join(",", myRoleIds.Where(r => roleMap.ContainsKey(r)).Select(r => roleMap[r]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user