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:
2026-09-10 17:31:39 +08:00
co-authored by Claude Sonnet 4.6
parent 3059befcf5
commit 6aca0297bb
48 changed files with 3583 additions and 52 deletions
@@ -0,0 +1,45 @@
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.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 审计日志 服务实现(查询端;写入端在各业务服务中直接插 AuditLogEntity,或经 AuditHelper
/// </summary>
public class AuditLogService : IAuditLogService
{
public async Task<Result<List<AuditLogDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total,
string? keyword = null, string? operationType = null, string? operationTarget = null,
DateTime? startTime = null, DateTime? endTime = null)
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<AuditLogEntity>()
.Where(x => x.IsDel == 0)
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
x => (x.UserName != null && x.UserName.Contains(keyword!))
|| (x.Description != null && x.Description.Contains(keyword!))
|| (x.RoleName != null && x.RoleName.Contains(keyword!)))
.WhereIF(!string.IsNullOrWhiteSpace(operationType), x => x.OperationType == operationType)
.WhereIF(!string.IsNullOrWhiteSpace(operationTarget), x => x.OperationTarget == operationTarget)
.WhereIF(startTime.HasValue, x => x.OperateTime >= startTime!.Value)
.WhereIF(endTime.HasValue, x => x.OperateTime <= endTime!.Value)
.OrderBy(x => x.OperateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total);
return Result<List<AuditLogDto>>.Success(list.ToDtoList());
}
catch (Exception ex)
{
return Result<List<AuditLogDto>>.Error("查询审计日志失败", ex);
}
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using Microsoft.Extensions.Logging;
using Model.Entity.System;
using ORM;
using Service.Interface;
using SqlSugar;
namespace Service.Implement
{
/// <summary>
/// 审计日志记录器实现:操作人/角色自动取自 ICurrentUserJWT Claims),失败不抛出(审计不应阻断业务)
/// </summary>
public class AuditRecorder : IAuditRecorder
{
private readonly ICurrentUser _currentUser;
private readonly ILogger<AuditRecorder> _logger;
public AuditRecorder(ICurrentUser currentUser, ILogger<AuditRecorder> logger)
{
_currentUser = currentUser;
_logger = logger;
}
public async Task RecordAsync(string operationType, string operationTarget, long targetId,
long deviceId = 0, string? oldValue = null, string? newValue = null,
string? ip = null, string? description = null)
{
try
{
var now = DateTime.Now;
var roleId = _currentUser.RoleIds.FirstOrDefault();
var roleName = _currentUser.RoleNames.FirstOrDefault();
await SqlSugarContext.DbContext.Insertable(new AuditLogEntity
{
UserId = _currentUser.UserId,
UserName = _currentUser.UserName,
RoleId = roleId,
RoleName = roleName ?? "",
OperationType = operationType,
OperationTarget = operationTarget,
TargetId = targetId,
DeviceId = deviceId,
OldValue = oldValue,
NewValue = newValue,
Ip = ip,
Description = description,
OperateTime = now,
CreateTime = now
}).ExecuteCommandAsync();
}
catch (Exception ex)
{
// 审计失败只记日志,不阻断业务
_logger.LogError(ex, "写审计日志失败({OperationType} {OperationTarget} {TargetId}",
operationType, operationTarget, targetId);
}
}
}
}
+252
View File
@@ -0,0 +1,252 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Model;
using Model.Dto.System;
using Model.Entity.System;
using Model.Mapper;
using ORM;
using Service.Interface;
using SqlSugar;
namespace Service.Implement
{
/// <summary>
/// 认证授权 服务实现:登录校验 → 加载角色/权限 → 签发 JWT(权限编码写入 Claims)
/// </summary>
public class AuthService : IAuthService
{
private readonly IConfiguration _configuration;
private readonly ICurrentUser _currentUser;
private readonly ILogger<AuthService> _logger;
public AuthService(IConfiguration configuration, ICurrentUser currentUser, ILogger<AuthService> logger)
{
_configuration = configuration;
_currentUser = currentUser;
_logger = logger;
}
public async Task<Result<LoginResultDto>> LoginAsync(LoginDto dto, string? ip)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.UserName) || string.IsNullOrWhiteSpace(dto.Password))
return Result<LoginResultDto>.Error("用户名和密码不能为空");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.UserName == dto.UserName && x.IsDel == 0).FirstAsync();
if (user == null)
return Result<LoginResultDto>.Error("用户名或密码错误");
if (user.IsEnabled == 0)
return Result<LoginResultDto>.Error("账号已被禁用,请联系管理员");
if (!PasswordHelper.Verify(dto.Password, user.PasswordHash))
return Result<LoginResultDto>.Error("用户名或密码错误");
// 加载角色与权限
var (roleIds, roleNames, permissionCodes, dataScope) = await LoadUserAuthAsync(user.Id);
// 签发 JWT
var token = GenerateJwt(user, roleIds, roleNames, permissionCodes, dataScope);
// 更新最后登录时间
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.LastLoginTime == DateTime.Now)
.Where(x => x.Id == user.Id).ExecuteCommandAsync();
// 登录审计
await WriteAuditAsync(user.Id, user.UserName, roleIds.FirstOrDefault(), roleNames.FirstOrDefault(),
"Login", "User", user.Id, 0, null, null, ip, $"用户 {user.UserName} 登录系统");
var userDto = user.ToDto();
userDto.RoleIds = roleIds.Select(r => r.ToString()).ToList();
userDto.RoleNames = string.Join(",", roleNames);
return Result<LoginResultDto>.Success(new LoginResultDto
{
Token = token,
RefreshToken = token,
User = userDto,
Permissions = permissionCodes
});
}
catch (Exception ex)
{
_logger.LogError(ex, "登录异常");
return Result<LoginResultDto>.Error("登录失败", ex);
}
}
public async Task<Result> LogoutAsync()
{
if (!_currentUser.IsAuthenticated)
return Result.Success();
try
{
await WriteAuditAsync(_currentUser.UserId, _currentUser.UserName,
_currentUser.RoleIds.FirstOrDefault(), _currentUser.RoleNames.FirstOrDefault(),
"Logout", "User", _currentUser.UserId, 0, null, null, null,
$"用户 {_currentUser.UserName} 退出登录");
return Result.Success();
}
catch (Exception ex)
{
_logger.LogError(ex, "退出登录记录审计失败");
return Result.Success(); // 退出本身不应失败
}
}
public async Task<Result<LoginResultDto>> GetMeAsync()
{
if (!_currentUser.IsAuthenticated)
return Result<LoginResultDto>.Error("未登录或 Token 已失效");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == _currentUser.UserId && x.IsDel == 0).FirstAsync();
if (user == null)
return Result<LoginResultDto>.Error("用户不存在或已被删除");
var userDto = user.ToDto();
userDto.RoleIds = _currentUser.RoleIds.Select(r => r.ToString()).ToList();
userDto.RoleNames = string.Join(",", _currentUser.RoleNames);
return Result<LoginResultDto>.Success(new LoginResultDto
{
Token = "",
RefreshToken = "",
User = userDto,
Permissions = _currentUser.Permissions
});
}
catch (Exception ex)
{
return Result<LoginResultDto>.Error("获取当前用户信息失败", ex);
}
}
public async Task<Result> ChangePasswordAsync(ChangePasswordDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.OldPassword) || string.IsNullOrWhiteSpace(dto.NewPassword))
return Result.Error("原密码和新密码不能为空");
if (dto.NewPassword.Length < 6)
return Result.Error("新密码长度不能少于 6 位");
if (!_currentUser.IsAuthenticated)
return Result.Error("未登录或 Token 已失效");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == _currentUser.UserId && x.IsDel == 0).FirstAsync();
if (user == null)
return Result.Error("用户不存在或已被删除");
if (!PasswordHelper.Verify(dto.OldPassword, user.PasswordHash))
return Result.Error("原密码错误");
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.PasswordHash == PasswordHelper.Hash(dto.NewPassword))
.Where(x => x.Id == user.Id).ExecuteCommandAsync();
await WriteAuditAsync(user.Id, user.UserName, _currentUser.RoleIds.FirstOrDefault(), _currentUser.RoleNames.FirstOrDefault(),
"Update", "User", user.Id, 0, null, null, null, $"用户 {user.UserName} 修改了自己的密码");
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("修改密码失败", ex);
}
}
/// <summary>
/// 加载用户的角色Id/角色名/权限编码/数据范围(取所有角色的并集,数据范围取最宽)
/// </summary>
public static async Task<(List<long> roleIds, List<string> roleNames, List<string> permissions, byte dataScope)>
LoadUserAuthAsync(long userId)
{
var db = SqlSugarContext.DbContext;
var roleIds = await db.Queryable<UserRoleEntity>()
.Where(x => x.UserId == userId)
.Select(x => x.RoleId)
.ToListAsync();
var roleList = new List<RoleEntity>();
if (roleIds.Count > 0)
roleList = await db.Queryable<RoleEntity>()
.Where(x => roleIds.Contains(x.Id) && x.IsDel == 0).ToListAsync();
var permissionCodes = new List<string>();
if (roleIds.Count > 0)
{
var permIds = await db.Queryable<RolePermissionEntity>()
.Where(x => roleIds.Contains(x.RoleId))
.Select(x => x.PermissionId)
.ToListAsync();
if (permIds.Count > 0)
{
var perms = await db.Queryable<PermissionEntity>()
.Where(x => permIds.Contains(x.Id) && x.IsDel == 0).ToListAsync();
permissionCodes = perms.Select(p => p.Code).Distinct().ToList();
}
}
// 数据范围取最宽:任一角色是"全部"即为全部
var dataScope = roleList.Count > 0 ? roleList.Min(r => r.DataScope) : (byte)2;
return (roleIds, roleList.Select(r => r.Name).ToList(), permissionCodes, dataScope);
}
/// <summary>生成 JWT:把用户身份 + 角色 + 权限编码全部写入 Claims(授权过滤器免查库)</summary>
private string GenerateJwt(UserEntity user, List<long> roleIds, List<string> roleNames, List<string> permissions, byte dataScope)
{
var jwt = _configuration.GetSection("Jwt");
var secret = jwt["SecretKey"]!;
var issuer = jwt["Issuer"] ?? "IOT-API";
var audience = jwt["Audience"] ?? "IOT-Web";
var expireMinutes = double.TryParse(jwt["ExpireMinutes"], out var m) ? m : 480;
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName),
new("roleIds", string.Join(",", roleIds)),
new("roleNames", string.Join(",", roleNames)),
new("permissions", string.Join(",", permissions)),
new("dataScope", dataScope.ToString()),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N"))
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(issuer, audience, claims,
expires: DateTime.Now.AddMinutes(expireMinutes), signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>写一条审计日志</summary>
private static async Task WriteAuditAsync(long userId, string? userName, long? roleId, string? roleName,
string operationType, string operationTarget, long targetId, long deviceId,
string? oldValue, string? newValue, string? ip, string? description)
{
var now = DateTime.Now;
await SqlSugarContext.DbContext.Insertable(new AuditLogEntity
{
UserId = userId,
UserName = userName ?? "",
RoleId = roleId ?? 0,
RoleName = roleName ?? "",
OperationType = operationType,
OperationTarget = operationTarget,
TargetId = targetId,
DeviceId = deviceId,
OldValue = oldValue,
NewValue = newValue,
Ip = ip,
Description = description,
OperateTime = now,
CreateTime = now
}).ExecuteCommandAsync();
}
}
}
+124
View File
@@ -0,0 +1,124 @@
using Model.Entity.System;
using ORM;
namespace Service.Implement
{
/// <summary>
/// RBAC 数据种子:预定义 4 个系统角色 + 6 个权限 + 默认管理员账号(admin/admin123
/// 幂等:按 Code 查存在性,存在则跳过;可重复执行
/// </summary>
public static class DataSeeder
{
/// <summary>数据范围:1=全部, 2=本实验室</summary>
public const byte DataScopeAll = 1;
public const byte DataScopeLab = 2;
/// <summary>6 个系统权限编码</summary>
public static readonly (string Code, string Name, string Group)[] Permissions =
{
("device:view", "查看设备", "device"),
("device:edit", "编辑设备", "device"),
("device:control", "控制设备", "device"),
("alert:confirm", "确认告警", "alert"),
("inspection:manage", "巡检管理", "inspection"),
("user:manage", "用户管理", "user")
};
/// <summary>
/// 4 个预定义角色:超级管理员(全部) / 实验室管理员(本实验室) / 设备操作员(本实验室) / 维修工程师(本实验室)
/// 权限矩阵严格按需求文档
/// </summary>
public static readonly (string Code, string Name, byte DataScope, string[] Permissions)[] Roles =
{
("superadmin", "超级管理员", DataScopeAll, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "user:manage" }),
("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage" }),
("operator", "设备操作员", DataScopeLab, new[] { "device:view", "inspection:manage" }),
("maintengineer", "维修工程师", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm" })
};
public static void Seed()
{
try
{
var db = SqlSugarContext.DbContext;
var now = DateTime.Now;
// 1. 权限种子
var permIdByCode = new Dictionary<string, long>();
foreach (var (code, name, group) in Permissions)
{
var existing = db.Queryable<PermissionEntity>().Where(x => x.Code == code).First();
if (existing != null) { permIdByCode[code] = existing.Id; continue; }
var perm = new PermissionEntity
{
Code = code, Name = name, Group = group, IsSystem = 1,
Sort = Array.FindIndex(Permissions, p => p.Code == code),
CreateTime = now
};
var id = db.Insertable(perm).ExecuteReturnSnowflakeId();
permIdByCode[code] = id;
}
// 2. 角色种子 + 角色-权限关联
foreach (var (code, name, scope, perms) in Roles)
{
var role = db.Queryable<RoleEntity>().Where(x => x.Code == code).First();
long roleId;
if (role == null)
{
role = new RoleEntity
{
Code = code, Name = name, DataScope = scope, IsSystem = 1,
Sort = Array.FindIndex(Roles, r => r.Code == code),
Remark = "系统预定义角色",
CreateTime = now
};
roleId = db.Insertable(role).ExecuteReturnSnowflakeId();
}
else
{
roleId = role.Id;
}
// 补齐角色-权限关联(只加不删,避免覆盖管理员自定义调整)
foreach (var permCode in perms)
{
if (!permIdByCode.TryGetValue(permCode, out var permId)) continue;
var linkExists = db.Queryable<RolePermissionEntity>()
.Where(x => x.RoleId == roleId && x.PermissionId == permId).Any();
if (!linkExists)
{
db.Insertable(new RolePermissionEntity { RoleId = roleId, PermissionId = permId, CreateTime = now }).ExecuteCommand();
}
}
}
// 3. 默认管理员账号(admin/admin123,挂在超级管理员角色)
var adminExists = db.Queryable<UserEntity>().Where(x => x.UserName == "admin" && x.IsDel == 0).Any();
if (!adminExists)
{
var superAdmin = db.Queryable<RoleEntity>().Where(x => x.Code == "superadmin").First();
var admin = new UserEntity
{
UserName = "admin",
PasswordHash = PasswordHelper.Hash("admin123"),
RealName = "系统管理员",
IsEnabled = 1,
Remark = "系统默认管理员(首次登录后请修改密码)",
CreateTime = now
};
var adminId = db.Insertable(admin).ExecuteReturnSnowflakeId();
if (superAdmin != null)
{
db.Insertable(new UserRoleEntity { UserId = adminId, RoleId = superAdmin.Id, CreateTime = now }).ExecuteCommand();
}
}
}
catch (Exception)
{
// 种子失败不阻断启动(例如表刚建好并发场景),下次启动会重试
throw;
}
}
}
}
+332
View File
@@ -0,0 +1,332 @@
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>
/// 组织架构管理 服务实现
/// </summary>
public class OrgService : IOrgService
{
private readonly IAuditRecorder _audit;
public OrgService(IAuditRecorder audit)
{
_audit = audit;
}
public async Task<Result<List<OrgTreeDto>>> GetTreeAsync()
{
try
{
var db = SqlSugarContext.DbContext;
var all = await db.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
// 聚合每个组织下的用户角色 + 数据范围(ScopeSummary/用户数展示用)
var users = await db.Queryable<UserEntity>()
.Where(x => x.IsDel == 0)
.Select(x => new { x.Id, x.OrgId })
.ToListAsync();
var userIds = users.Select(u => u.Id).ToList();
var roleLinks = userIds.Count > 0
? await db.Queryable<UserRoleEntity>().Where(x => userIds.Contains(x.UserId)).ToListAsync()
: new List<UserRoleEntity>();
var roleIds = roleLinks.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);
// userId -> 角色数据范围标签列表(如「超级管理员·全部」)
var userScopeTags = new Dictionary<long, List<string>>();
foreach (var link in roleLinks)
{
if (!roleMap.TryGetValue(link.RoleId, out var role)) continue;
var tag = $"{role.Name}·{ScopeLabel(role.DataScope)}";
if (!userScopeTags.TryGetValue(link.UserId, out var list))
userScopeTags[link.UserId] = list = new List<string>();
if (!list.Contains(tag)) list.Add(tag);
}
// orgId -> 直接挂靠用户Id列表
var orgUsers = users.GroupBy(u => u.OrgId).ToDictionary(g => g.Key, g => g.Select(u => u.Id).ToList());
var tree = BuildTreeWithStats(all, 0, orgUsers, userScopeTags);
return Result<List<OrgTreeDto>>.Success(tree);
}
catch (Exception ex)
{
return Result<List<OrgTreeDto>>.Error("查询组织树失败", ex);
}
}
/// <summary>
/// 建树并聚合统计:用户数(含下级)+ 数据权限摘要(子树内用户角色的数据范围并集)
/// </summary>
private static List<OrgTreeDto> BuildTreeWithStats(List<OrgEntity> all, long parentId,
Dictionary<long, List<long>> orgUsers, Dictionary<long, List<string>> userScopeTags)
{
var nodes = new List<OrgTreeDto>();
foreach (var entity in all.Where(x => x.ParentId == parentId).OrderBy(x => x.Sort).ThenBy(x => x.Code))
{
var node = entity.ToTreeDto();
node.Children = BuildTreeWithStats(all, entity.Id, orgUsers, userScopeTags);
// 本节点直接用户
orgUsers.TryGetValue(entity.Id, out var directUserIds);
node.UserCount = directUserIds?.Count ?? 0;
// 汇总子树:用户总数 + 数据权限标签并集
var scopes = new List<string>();
if (directUserIds != null)
{
foreach (var uid in directUserIds)
if (userScopeTags.TryGetValue(uid, out var tags))
foreach (var t in tags)
if (!scopes.Contains(t)) scopes.Add(t);
}
var total = node.UserCount;
foreach (var child in node.Children)
{
total += child.TotalUserCount;
foreach (var t in child.ScopeSummary)
if (!scopes.Contains(t)) scopes.Add(t);
}
node.TotalUserCount = total;
node.ScopeSummary = scopes;
nodes.Add(node);
}
return nodes;
}
/// <summary>数据范围标签:1=全部, 2=本组织及下级</summary>
private static string ScopeLabel(byte dataScope) => dataScope == 1 ? "全部" : "本组织及下级";
public async Task<Result<OrgDto>> AddAsync(OrgDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.Name) || string.IsNullOrWhiteSpace(dto.Code))
return Result<OrgDto>.Error("组织名称和编码不能为空");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
if (exists) return Result<OrgDto>.Error($"组织编码「{dto.Code}」已存在");
// 父节点校验 + 层级固定校验(根节点必须是公司)
var parentId = ParseParent(dto.ParentId);
if (parentId > 0)
{
var parent = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Id == parentId && x.IsDel == 0).FirstAsync();
if (parent == null) return Result<OrgDto>.Error("父级组织不存在");
var hierarchyError = ValidateHierarchy(parent.OrgType, (byte)dto.OrgType);
if (hierarchyError != null) return Result<OrgDto>.Error(hierarchyError.Msg);
}
else if (dto.OrgType != 1)
{
return Result<OrgDto>.Error("根节点只能是公司");
}
// 班次仅班组有效
var shiftType = dto.OrgType == 4 ? (byte)Math.Max(1, dto.ShiftType) : (byte)0;
var entity = dto.ToEntity();
entity.Id = 0;
entity.ParentId = parentId;
entity.ShiftType = shiftType;
entity.CreateTime = DateTime.Now;
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
entity.Id = id;
await _audit.RecordAsync("Create", "Org", id, 0,
newValue: $"[{OrgTypeName(entity.OrgType)}] {entity.Code}({entity.Name})",
description: $"新增组织 {entity.Name}");
return Result<OrgDto>.Success(entity.ToDto());
}
catch (Exception ex)
{
return Result<OrgDto>.Error("新增组织失败", ex);
}
}
public async Task<Result<OrgDto>> UpdateAsync(OrgDto dto)
{
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
return Result<OrgDto>.Error("组织Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<OrgDto>.Error("组织不存在或已被删除");
var codeExists = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
if (codeExists) return Result<OrgDto>.Error($"组织编码「{dto.Code}」已存在");
var parentId = ParseParent(dto.ParentId);
if (parentId == id)
return Result<OrgDto>.Error("父级不能是自己");
// 层级固定:组织类型创建后不可变更
if (dto.OrgType != entity.OrgType)
return Result<OrgDto>.Error($"组织层级固定,类型不可变更(当前为「{OrgTypeName(entity.OrgType)}」)");
// 换父级时不能挂到自己的子孙下面(否则成环),且层级必须匹配
if (parentId != entity.ParentId && parentId > 0)
{
var all = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0).ToListAsync();
var subtreeIds = CollectSubtreeIds(all, id);
if (subtreeIds.Contains(parentId))
return Result<OrgDto>.Error("不能把组织挂到自己的下级下面");
var newParent = all.FirstOrDefault(x => x.Id == parentId);
if (newParent == null)
return Result<OrgDto>.Error("目标父级组织不存在");
var hierarchyError = ValidateHierarchy(newParent.OrgType, entity.OrgType);
if (hierarchyError != null) return Result<OrgDto>.Error(hierarchyError.Msg);
}
var oldEntity = entity;
var newEntity = dto.ToEntity();
newEntity.Id = id;
newEntity.ParentId = parentId;
newEntity.OrgType = entity.OrgType; // 类型不可变
// 班次仅班组有效
newEntity.ShiftType = entity.OrgType == 4 ? (byte)Math.Max(1, dto.ShiftType) : (byte)0;
await SqlSugarContext.DbContext.Updateable(newEntity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
.ExecuteCommandAsync();
await _audit.RecordAsync("Update", "Org", id, 0,
oldValue: $"[{OrgTypeName(oldEntity.OrgType)}] {oldEntity.Name}",
newValue: $"[{OrgTypeName(newEntity.OrgType)}] {newEntity.Name}",
description: $"修改组织 {newEntity.Name}");
return Result<OrgDto>.Success(newEntity.ToDto());
}
catch (Exception ex)
{
return Result<OrgDto>.Error("修改组织失败", ex);
}
}
public async Task<Result> DeleteAsync(long id)
{
if (id <= 0) return Result.Error("组织Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("组织不存在或已被删除");
var hasChildren = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.ParentId == id && x.IsDel == 0).AnyAsync();
if (hasChildren)
return Result.Error($"组织「{entity.Name}」下存在子组织,请先删除子组织");
var hasUsers = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.OrgId == id && x.IsDel == 0).AnyAsync();
if (hasUsers)
return Result.Error($"组织「{entity.Name}」下仍有用户,请先移出用户");
await SqlSugarContext.DbContext.Updateable<OrgEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
await _audit.RecordAsync("Delete", "Org", id, 0,
oldValue: $"[{OrgTypeName(entity.OrgType)}] {entity.Code}({entity.Name})",
description: $"删除组织 {entity.Name}");
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("删除组织失败", ex);
}
}
public async Task<Result<List<OrgOptionDto>>> GetOptionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
return Result<List<OrgOptionDto>>.Success(list.ToOptionDtoList());
}
catch (Exception ex)
{
return Result<List<OrgOptionDto>>.Error("查询组织选项失败", ex);
}
}
public async Task<List<long>> GetSubtreeIdsAsync(long orgId)
{
if (orgId <= 0) return new List<long>();
var all = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0).ToListAsync();
return CollectSubtreeIds(all, orgId);
}
#region
/// <summary>收集指定组织及其全部子孙 Id(含自身)</summary>
public static List<long> CollectSubtreeIds(List<OrgEntity> all, long orgId)
{
var result = new List<long>();
var stack = new Stack<long>();
stack.Push(orgId);
while (stack.Count > 0)
{
var current = stack.Pop();
result.Add(current);
foreach (var child in all.Where(x => x.ParentId == current))
stack.Push(child.Id);
}
return result;
}
private static long ParseParent(string? parentId)
=> long.TryParse(parentId, out var v) ? v : 0;
/// <summary>层级固定校验:公司(1)→实验室(2)→部门(3)→班组(4),子级类型必须=父级类型+1</summary>
private static Result? ValidateHierarchy(byte parentType, byte childType)
{
var validChild = parentType switch
{
1 => (byte)2, // 公司下只能建实验室
2 => (byte)3, // 实验室下只能建部门
3 => (byte)4, // 部门下只能建班组
_ => (byte)0 // 班组不能再有下级
};
if (childType != validChild)
return Result.Error($"组织层级固定:公司→实验室→部门→班组,该节点下只能建「{OrgTypeName(validChild)}」");
return null;
}
private static string OrgTypeName(byte orgType) => orgType switch
{
1 => "公司",
2 => "实验室",
3 => "部门",
4 => "班组",
_ => "组织"
};
#endregion
}
}
@@ -0,0 +1,49 @@
using System.Security.Cryptography;
using System.Text;
namespace Service.Implement
{
/// <summary>
/// 密码哈希工具(PBKDF2-SHA256,格式 salt$hash,盐随机 16 字节)
/// </summary>
public static class PasswordHelper
{
private const int Iterations = 10_000;
private const int SaltSize = 16;
private const int HashSize = 32;
/// <summary>生成密码哈希(salt$hash</summary>
public static string Hash(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, HashSize);
return $"{Convert.ToHexString(salt)}${Convert.ToHexString(hash)}";
}
/// <summary>校验密码是否匹配</summary>
public static bool Verify(string password, string? stored)
{
if (string.IsNullOrWhiteSpace(stored)) return false;
var parts = stored.Split('$');
if (parts.Length != 2) return false;
var salt = Convert.FromHexString(parts[0]);
var expected = Convert.FromHexString(parts[1]);
var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, expected.Length);
return CryptographicOperations.FixedTimeEquals(actual, expected);
}
/// <summary>生成随机盐字符串(供外部使用)</summary>
public static string NewSalt()
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
return Convert.ToHexString(salt).ToLowerInvariant();
}
/// <summary>SHA256(供 RefreshToken 等场景)</summary>
public static string Sha256(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
}
+277 -1
View File
@@ -1,4 +1,14 @@
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
{
@@ -7,6 +17,272 @@ namespace Service.Implement
/// </summary>
public class RoleService : IRoleService
{
// TODO: 实现 角色权限管理 相关方法
public async Task<Result<List<RoleDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.IsDel == 0)
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!))
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total);
var dtos = list.ToDtoList();
await FillPermissionsAsync(dtos);
return Result<List<RoleDto>>.Success(dtos);
}
catch (Exception ex)
{
return Result<List<RoleDto>>.Error("查询角色列表失败", ex);
}
}
public async Task<Result<RoleDto>> GetByIdAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<RoleDto>.Error("角色不存在或已被删除");
var dto = entity.ToDto();
await FillPermissionsAsync(new List<RoleDto> { dto });
return Result<RoleDto>.Success(dto);
}
catch (Exception ex)
{
return Result<RoleDto>.Error("查询角色详情失败", ex);
}
}
public async Task<Result<RoleDto>> AddAsync(RoleDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
return Result<RoleDto>.Error("角色编码和名称不能为空");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
if (exists) return Result<RoleDto>.Error($"角色编码「{dto.Code}」已存在");
var entity = dto.ToEntity();
entity.Id = 0;
entity.IsSystem = 0; // 新建角色一律非系统内置
entity.CreateTime = DateTime.Now;
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
entity.Id = id;
if (dto.PermissionIds != null && dto.PermissionIds.Count > 0)
{
var permIds = dto.PermissionIds.Select(long.Parse).Where(p => p > 0).ToList();
await ReplaceRolePermissionsAsync(id, permIds);
}
var result = entity.ToDto();
await FillPermissionsAsync(new List<RoleDto> { result });
return Result<RoleDto>.Success(result);
}
catch (Exception ex)
{
return Result<RoleDto>.Error("新增角色失败", ex);
}
}
public async Task<Result<RoleDto>> UpdateAsync(RoleDto dto)
{
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
return Result<RoleDto>.Error("角色Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<RoleDto>.Error("角色不存在或已被删除");
// 系统内置角色不允许改编码
if (entity.IsSystem == 1 && dto.Code != entity.Code)
return Result<RoleDto>.Error($"系统内置角色「{entity.Name}」不允许修改编码");
var codeExists = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
if (codeExists) return Result<RoleDto>.Error($"角色编码「{dto.Code}」已存在");
var newEntity = dto.ToEntity();
newEntity.Id = id;
newEntity.IsSystem = entity.IsSystem; // 内置标记不可被前端覆盖
await SqlSugarContext.DbContext.Updateable(newEntity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.IsSystem })
.ExecuteCommandAsync();
// 权限变化时同步更新关联(传入 null 表示不改权限)
if (dto.PermissionIds != null)
{
var permIds = dto.PermissionIds.Select(long.Parse).Where(p => p > 0).ToList();
var guard = await EnsureAdminRemainsAfterRoleChangeAsync(id, permIds);
if (guard != null) return Result<RoleDto>.Error(guard.Msg);
await ReplaceRolePermissionsAsync(id, permIds);
}
var result = newEntity.ToDto();
await FillPermissionsAsync(new List<RoleDto> { result });
return Result<RoleDto>.Success(result);
}
catch (Exception ex)
{
return Result<RoleDto>.Error("修改角色失败", ex);
}
}
public async Task<Result> DeleteAsync(long id)
{
if (id <= 0) return Result.Error("角色Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("角色不存在或已被删除");
if (entity.IsSystem == 1)
return Result.Error($"系统内置角色「{entity.Name}」不允许删除");
var usedByUser = await SqlSugarContext.DbContext.Queryable<UserRoleEntity>()
.Where(x => x.RoleId == id).AnyAsync();
if (usedByUser)
return Result.Error("该角色下存在用户,请先移除角色下的用户再删除");
await SqlSugarContext.DbContext.Updateable<RoleEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
await SqlSugarContext.DbContext.Deleteable<RolePermissionEntity>()
.Where(x => x.RoleId == id).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("删除角色失败", ex);
}
}
public async Task<Result> AssignPermissionsAsync(long roleId, List<long> permissionIds)
{
if (roleId <= 0) return Result.Error("角色Id无效");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == roleId && x.IsDel == 0).AnyAsync();
if (!exists) return Result.Error("角色不存在或已被删除");
// 防锁死:移除角色的用户管理权限后,系统必须仍有其它可管理用户的管理员
var newIds = permissionIds ?? new List<long>();
var guard = await EnsureAdminRemainsAfterRoleChangeAsync(roleId, newIds);
if (guard != null) return guard;
await ReplaceRolePermissionsAsync(roleId, newIds);
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("分配权限失败", ex);
}
}
public async Task<Result<List<RoleOptionDto>>> GetOptionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
return Result<List<RoleOptionDto>>.Success(list.ToOptionDtoList());
}
catch (Exception ex)
{
return Result<List<RoleOptionDto>>.Error("查询角色选项失败", ex);
}
}
public async Task<Result<List<PermissionDto>>> GetPermissionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<PermissionEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
return Result<List<PermissionDto>>.Success(list.ToDtoList());
}
catch (Exception ex)
{
return Result<List<PermissionDto>>.Error("查询权限列表失败", ex);
}
}
/// <summary>全量覆盖角色-权限关联</summary>
private static async Task ReplaceRolePermissionsAsync(long roleId, List<long> permissionIds)
{
var db = SqlSugarContext.DbContext;
await db.Deleteable<RolePermissionEntity>().Where(x => x.RoleId == roleId).ExecuteCommandAsync();
if (permissionIds.Count > 0)
{
var now = DateTime.Now;
var links = permissionIds.Distinct().Select(p => new RolePermissionEntity { RoleId = roleId, PermissionId = p, CreateTime = now }).ToList();
await db.Insertable(links).ExecuteCommandAsync();
}
}
/// <summary>
/// 防锁死校验:当 roleId 将失去 user:manage 权限时,系统必须仍存在
/// 「已启用且通过其它角色拥有 user:manage 权限」的用户,否则返回 Error
/// </summary>
private async Task<Result?> EnsureAdminRemainsAfterRoleChangeAsync(long roleId, List<long> newPermissionIds)
{
var db = SqlSugarContext.DbContext;
var managePermId = await db.Queryable<PermissionEntity>()
.Where(x => x.Code == "user:manage" && x.IsDel == 0)
.Select(x => x.Id).FirstAsync();
if (managePermId == 0) return null;
bool currentlyHas = await db.Queryable<RolePermissionEntity>()
.AnyAsync(x => x.RoleId == roleId && x.PermissionId == managePermId);
bool willHave = newPermissionIds.Contains(managePermId);
if (currentlyHas && !willHave)
{
var otherRoleIds = await db.Queryable<RolePermissionEntity>()
.Where(x => x.PermissionId == managePermId && x.RoleId != roleId)
.Select(x => x.RoleId).ToListAsync();
var otherUserIds = otherRoleIds.Count > 0
? await db.Queryable<UserRoleEntity>().Where(x => otherRoleIds.Contains(x.RoleId)).Select(x => x.UserId).ToListAsync()
: new List<long>();
var exists = otherUserIds.Count > 0 && await db.Queryable<UserEntity>()
.AnyAsync(x => otherUserIds.Contains(x.Id) && x.IsDel == 0 && x.IsEnabled == 1);
if (!exists)
return Result.Error("操作被拒绝:移除该角色的用户管理权限后,系统将没有任何可管理用户的管理员");
}
return null;
}
/// <summary>给角色 DTO 填充权限Id列表与权限编码列表</summary>
private static async Task FillPermissionsAsync(List<RoleDto> dtos)
{
if (dtos == null || dtos.Count == 0) return;
var db = SqlSugarContext.DbContext;
var roleIds = dtos.Select(d => long.Parse(d.Id)).ToList();
var links = await db.Queryable<RolePermissionEntity>()
.Where(x => roleIds.Contains(x.RoleId)).ToListAsync();
var permIds = links.Select(l => l.PermissionId).Distinct().ToList();
var perms = permIds.Count > 0
? await db.Queryable<PermissionEntity>().Where(x => permIds.Contains(x.Id) && x.IsDel == 0).ToListAsync()
: new List<PermissionEntity>();
var permMap = perms.ToDictionary(p => p.Id, p => p.Code);
foreach (var d in dtos)
{
var rid = long.Parse(d.Id);
var myPermIds = links.Where(l => l.RoleId == rid).Select(l => l.PermissionId).ToList();
d.PermissionIds = myPermIds.Select(p => p.ToString()).ToList();
d.PermissionCodes = myPermIds.Where(p => permMap.ContainsKey(p)).Select(p => permMap[p]).ToList();
}
}
}
}
+398 -1
View File
@@ -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]));
}
}
}
}