using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Rmsys.Domain.Entities;
using Rmsys.Infrastructure;
namespace Rmsys.Api.Endpoints;
///
/// 用户与登录管理(P0-7):开户、改资料、重置口令、停用/启用、删除。
/// 停用与重置口令都会 TokenVersion+1,已签发的令牌立即失效。
///
public static class UserEndpoints
{
private const string DefaultPassword = "Hd@2026";
public static void MapUserEndpoints(this WebApplication app)
{
// ============ 新增账号 ============
app.MapPost("/api/admin/users", async (UserCreateReq req, ClaimsPrincipal p, RmsysDbContext db) =>
{
var acc = (req.account ?? "").Trim();
var name = (req.name ?? "").Trim();
if (acc.Length < 3 || acc.Length > 30)
return Results.Ok(new { ok = false, message = "登录账号需 3~30 位" });
if (!System.Text.RegularExpressions.Regex.IsMatch(acc, "^[A-Za-z0-9_.]+$"))
return Results.Ok(new { ok = false, message = "登录账号只能包含字母、数字、下划线" });
if (string.IsNullOrWhiteSpace(name))
return Results.Ok(new { ok = false, message = "姓名不能为空" });
if (await db.SysUsers.AnyAsync(x => x.Account == acc))
return Results.Ok(new { ok = false, message = "登录账号已存在" });
var role = await FindRole(db, req.role);
if (role == null) return Results.Ok(new { ok = false, message = "角色不存在" });
var pwd = string.IsNullOrWhiteSpace(req.password) ? DefaultPassword : req.password!.Trim();
if (pwd.Length < 6) return Results.Ok(new { ok = false, message = "初始口令至少 6 位" });
var (hash, salt) = Auth.PasswordHasher.Hash(pwd);
var u = new SysUser
{
Account = acc,
RealName = name,
DeptId = await DeptIdOf(db, req.dept),
DataScope = string.IsNullOrWhiteSpace(req.scope) ? null : req.scope!.Trim(),
PwdHash = hash,
PwdSalt = salt,
PwdUpdatedAt = DateTime.Now,
Enabled = req.enabled == 0 ? 0 : 1,
CreatedAt = DateTime.Now
};
db.SysUsers.Add(u);
await db.SaveChangesAsync();
db.SysUserRoles.Add(new SysUserRole { UserId = u.Id, RoleId = role.Id });
await db.SaveChangesAsync();
await Audit(db, p, "新增账号", acc, $"{name} / 角色={role.Name} / 初始口令已设置");
return Results.Ok(new { ok = true, id = u.Id, message = "账号已创建" });
}).RequireAuthorization();
// ============ 编辑资料(账号不可改)============
app.MapPut("/api/admin/users/{acc}", async (string acc, UserUpdateReq req, ClaimsPrincipal p, RmsysDbContext db) =>
{
var u = await db.SysUsers.FirstOrDefaultAsync(x => x.Account == acc);
if (u == null) return Results.Ok(new { ok = false, message = "账号不存在" });
if (!string.IsNullOrWhiteSpace(req.name)) u.RealName = req.name!.Trim();
if (req.dept != null) u.DeptId = await DeptIdOf(db, req.dept);
if (req.scope != null) u.DataScope = string.IsNullOrWhiteSpace(req.scope) ? null : req.scope!.Trim();
var disable = false;
if (req.enabled.HasValue && req.enabled.Value != u.Enabled)
{
if (req.enabled.Value == 0)
{
var guard = await LastAdminGuard(db, u.Id, "停用");
if (guard != null) return Results.Ok(new { ok = false, message = guard });
disable = true;
}
u.Enabled = req.enabled.Value;
u.TokenVersion += 1; // 状态变化立即踢掉已登录会话
}
if (!string.IsNullOrWhiteSpace(req.role))
{
var role = await FindRole(db, req.role);
if (role == null) return Results.Ok(new { ok = false, message = "角色不存在" });
var old = await db.SysUserRoles.Where(x => x.UserId == u.Id).ToListAsync();
if (!old.Any(x => x.RoleId == role.Id))
{
// 换角色前也要保证不会把最后一个管理员改掉
var guard2 = await LastAdminGuard(db, u.Id, "改角色");
if (guard2 != null) return Results.Ok(new { ok = false, message = guard2 });
foreach (var x in old) db.SysUserRoles.Remove(x);
db.SysUserRoles.Add(new SysUserRole { UserId = u.Id, RoleId = role.Id });
}
}
await db.SaveChangesAsync();
await Audit(db, p, "编辑账号", acc,
$"{u.RealName} / 角色={(await FindRole(db, req.role))?.Name ?? "不变"} / 状态={(u.Enabled == 1 ? "启用" : "停用")}" +
(disable ? "(已强制下线)" : ""));
return Results.Ok(new { ok = true, message = "账号已保存" });
}).RequireAuthorization();
// ============ 重置口令 ============
app.MapPost("/api/admin/users/{acc}/reset-password", async (string acc, UserPwdReq req, ClaimsPrincipal p, RmsysDbContext db) =>
{
var u = await db.SysUsers.FirstOrDefaultAsync(x => x.Account == acc);
if (u == null) return Results.Ok(new { ok = false, message = "账号不存在" });
var pwd = string.IsNullOrWhiteSpace(req.password) ? DefaultPassword : req.password!.Trim();
if (pwd.Length < 6) return Results.Ok(new { ok = false, message = "口令至少 6 位" });
var (hash, salt) = Auth.PasswordHasher.Hash(pwd);
u.PwdHash = hash;
u.PwdSalt = salt;
u.PwdUpdatedAt = DateTime.Now;
u.FailCount = 0;
u.LockedUntil = null;
u.TokenVersion += 1; // 旧口令签发的令牌立即失效
await db.SaveChangesAsync();
await Audit(db, p, "重置口令", acc, $"{u.RealName}(已强制下线,旧令牌失效)");
return Results.Ok(new { ok = true, message = "口令已重置", password = pwd });
}).RequireAuthorization();
// ============ 删除账号 ============
app.MapDelete("/api/admin/users/{acc}", async (string acc, ClaimsPrincipal p, RmsysDbContext db) =>
{
var u = await db.SysUsers.FirstOrDefaultAsync(x => x.Account == acc);
if (u == null) return Results.Ok(new { ok = false, message = "账号不存在" });
var selfId = int.TryParse(p.FindFirst("uid")?.Value, out var uid) ? uid : 0;
if (u.Id == selfId) return Results.Ok(new { ok = false, message = "不能删除当前登录的账号" });
var guard = await LastAdminGuard(db, u.Id, "删除");
if (guard != null) return Results.Ok(new { ok = false, message = guard });
foreach (var x in await db.SysUserRoles.Where(x => x.UserId == u.Id).ToListAsync())
db.SysUserRoles.Remove(x);
db.SysUsers.Remove(u);
await db.SaveChangesAsync();
await Audit(db, p, "删除账号", acc, u.RealName);
return Results.Ok(new { ok = true, message = "账号已删除" });
}).RequireAuthorization();
}
// ---------------- 辅助 ----------------
private static async Task FindRole(RmsysDbContext db, string? role)
{
if (string.IsNullOrWhiteSpace(role)) return null;
var r = role.Trim();
return await db.SysRoles.FirstOrDefaultAsync(x => x.Name == r || x.Code == r);
}
private static async Task DeptIdOf(RmsysDbContext db, string? dept)
{
if (string.IsNullOrWhiteSpace(dept) || dept == "all") return null;
var d = await db.Depts.FirstOrDefaultAsync(x => x.Code == dept);
return d?.Id;
}
/// 要动的是最后一个启用的管理员时,拒绝操作
private static async Task LastAdminGuard(RmsysDbContext db, int userId, string action)
{
var adminRoleIds = await db.SysRoles.Where(x => x.Code == "admin").Select(x => x.Id).ToListAsync();
if (adminRoleIds.Count == 0) return null;
var isTargetAdmin = await db.SysUserRoles.AnyAsync(x => x.UserId == userId && adminRoleIds.Contains(x.RoleId));
if (!isTargetAdmin) return null;
var enabledAdminIds = await db.SysUsers.Where(x => x.Enabled == 1)
.Where(x => db.SysUserRoles.Any(r => r.UserId == x.Id && adminRoleIds.Contains(r.RoleId)))
.Select(x => x.Id).ToListAsync();
if (enabledAdminIds.Count <= 1 && enabledAdminIds.Contains(userId))
return $"这是最后一个启用的系统管理员,不能{action};请先新增或启用另一个管理员";
return null;
}
private static async Task Audit(RmsysDbContext db, ClaimsPrincipal p, string action, string target, string? detail)
{
db.AuditLogs.Add(new AuditLog
{
At = DateTime.Now,
UserId = int.TryParse(p.FindFirst("uid")?.Value, out var uid) ? uid : null,
UserName = p.FindFirst(ClaimTypes.Name)?.Value,
Role = p.FindFirst(ClaimTypes.Role)?.Value,
Action = action,
Target = target,
Detail = detail,
Device = "API"
});
await db.SaveChangesAsync();
}
public record UserCreateReq(string? account, string? name, string? role, string? dept,
string? scope, int? enabled, string? password);
public record UserUpdateReq(string? name, string? role, string? dept, string? scope, int? enabled);
public record UserPwdReq(string? password);
}