RuoVea.OmiApi.UserRoleMenu
10.0.0.5
dotnet add package RuoVea.OmiApi.UserRoleMenu --version 10.0.0.5
NuGet\Install-Package RuoVea.OmiApi.UserRoleMenu -Version 10.0.0.5
<PackageReference Include="RuoVea.OmiApi.UserRoleMenu" Version="10.0.0.5" />
<PackageVersion Include="RuoVea.OmiApi.UserRoleMenu" Version="10.0.0.5" />
<PackageReference Include="RuoVea.OmiApi.UserRoleMenu" />
paket add RuoVea.OmiApi.UserRoleMenu --version 10.0.0.5
#r "nuget: RuoVea.OmiApi.UserRoleMenu, 10.0.0.5"
#:package RuoVea.OmiApi.UserRoleMenu@10.0.0.5
#addin nuget:?package=RuoVea.OmiApi.UserRoleMenu&version=10.0.0.5
#tool nuget:?package=RuoVea.OmiApi.UserRoleMenu&version=10.0.0.5
RuoVea.OmiApi.UserRoleMenu
用户角色菜单管理 API 组件 —— 基于 .NET 构建的轻量级、跨平台 RBAC 权限管理系统后端。
RuoVea.OmiApi.UserRoleMenu 是一个开箱即用的用户-角色-菜单权限管理 NuGet 包,提供用户 CRUD、角色 CRUD、菜单树管理、角色菜单授权、用户角色授权、按钮权限控制、JWT 认证集成、缓存管理的完整能力。基于 SqlSugar ORM 和 DynamicWebApi,注册即自动生成 RESTful API 端点,支持 MySql / SqlServer / PostgreSQL / SQLite / Oracle 等多种数据库。
目录
概览
功能特性
| 模块 | 功能 |
|---|---|
| 👤 用户管理 | 分页查询、增删改查、状态启用/停用、密码修改/重置、登录锁定解除、用户角色授权、基本信息管理 |
| 🔐 角色管理 | 分页查询、增删改查、状态控制、角色菜单授权、角色用户关联查询 |
| 📋 菜单管理 | 树形菜单 CRUD、按钮权限管理、菜单类型(目录/菜单/按钮)校验、登录菜单树动态构建 |
| 🔒 权限控制 | 基于按钮权限标识(xxx:xxx 格式)的细粒度权限控制,按钮权限缓存自动刷新 |
| 🌱 种子数据 | 自动建表、预置菜单/角色/用户种子数据(Web 菜单与 API 菜单双模式) |
| 🗄️ 缓存管理 | 按钮权限缓存、黑名单缓存、密码错误次数缓存,支持按前缀批量删除与查询 |
| 🛡️ 安全防护 | 超级管理员禁止删除/修改状态、禁止操作本人账号状态、禁止删除含用户的角色 |
| 🏢 多租户 | 所有实体实现 ITenantEntity,天然支持租户隔离 |
| 🔑 JWT 集成 | 内置 JwtBearer 认证配置,与 RuoVea.ExJwtBearer 无缝对接 |
| 🗄️ 多库支持 | MySql、SqlServer、PostgreSQL、SQLite、Oracle、Dm 等 |
架构一览
┌─────────────────────────────────────────────────────────────┐
│ NuGet Package │
│ RuoVea.OmiApi.UserRoleMenu │
├─────────────────────────────────────────────────────────────┤
│ Service Layer (7 Services) │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ SysUser │ │ SysRole │ │ SysMenu │ │
│ │ Service │ │ Service │ │ Service │ │
│ │ 14 endpoints │ │ 8 endpoints │ │ 8 endpoints │ │
│ ├───────────────┤ ├───────────────┤ ├───────────────┤ │
│ │ SysUserRole │ │ SysRoleMenu │ │ SysCache │ │
│ │ Service │ │ Service │ │ Service │ │
│ │ (internal) │ │ (internal) │ │ 9 endpoints │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
│ DI Extensions │
│ ├─ AddOmiSystemSetup() (3 overloads) │
│ └─ AddSystemInitSetup() (种子数据 + 建表) │
├─────────────────────────────────────────────────────────────┤
│ Domain Layer (5 Entities) │
│ SysUser ──< SysUserRole >── SysRole │
│ SysRole ──< SysRoleMenu >── SysMenu │
│ SysMenu ─── SysMenu (Self-ref tree, Pid) │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure │
│ SqlSugar ORM · DynamicWebApi · ExSugar Repo │
│ ExJwtBearer · ExFilter · ExPws · OmiApi.Config │
└─────────────────────────────────────────────────────────────┘
支持的 .NET 版本
| TFM | NuGet 版本 |
|---|---|
net8.0 |
8.0.2.19 |
net10.0 |
10.0.0.4 |
安装
NuGet 包管理器
# .NET 8 项目
Install-Package RuoVea.OmiApi.UserRoleMenu -Version 8.0.2.19
# .NET 10 项目
Install-Package RuoVea.OmiApi.UserRoleMenu -Version 10.0.0.4
.NET CLI
dotnet add package RuoVea.OmiApi.UserRoleMenu --version 8.0.2.19
依赖项
本包依赖以下组件(安装时会自动引入):
| 包名 | 用途 |
|---|---|
RuoVea.DynamicWebApi |
动态 API 控制器生成 |
RuoVea.ExSugar |
SqlSugar 仓储模式封装 |
RuoVea.ExJwtBearer |
JWT 认证集成 |
RuoVea.ExFilter |
请求过滤与拦截 |
RuoVea.ExPws |
密码加密服务 |
RuoVea.OmiApi.Config |
系统配置管理 |
30 秒快速开始
1. 配置数据库连接 (appsettings.json)
{
"ConnectionConfigs": [
{
"DbType": "Sqlite",
"ConnectionString": "DataSource=./ruovea.db"
}
],
/* Jwt 配置 */
"Jwt": {
"ValidateIssuerSigningKey": true,
"IssuerSigningKey": "3c1cbc3f546eda35168c3aa3cb91780fbe703f0996c6d123ea96dc85c70bbc0a",
"ValidateIssuer": true,
"ValidIssuer": "SecurityDemo.Authentication.JWT",
"ValidateAudience": true,
"ValidAudience": "jwtAudience",
"ValidateLifetime": true,
"ExpiredTime": 1440,
"ClockSkew": 5
},
"Swagger": {
"ApiVersions": [
{
"Title": "系统应用",
"Version": "system"
}
]
}
}
支持的 DbType 值:
MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、OpenGauss、ClickHouse等。
2. 注册服务 (Program.cs)
// <summary>
// 在 Program.cs 中注册 OmiApi.UserRoleMenu 组件服务
// </summary>
var builder = WebApplication.CreateBuilder(args);
// 注册动态 Web API(自动将 Service 映射为 REST 控制器)
builder.Services.AddDynamicWebApi(options =>
{
options.RemoveControllerPostfixes = new List<string> { "AppService", "Service" };
options.RemovePrefix = new List<string> { "get", "post" };
});
// 注册用户角色菜单模块服务(默认 Scoped 生命周期)
builder.Services.AddOmiSystemSetup();
// 注册 SqlSugar ORM
builder.Services.AddSqlSugarSetup();
// 注册 HttpContext 用户上下文
builder.Services.AddHttpContextSetup<AspNetUser>();
// 注册 JWT 认证
builder.Services.AddAuthenticationSetup(IdentifyEnum.Jwt, true);
// 初始化数据库表结构和种子数据(isWeb: true=Web菜单, false=API菜单)
builder.Services.AddSystemInitSetup(isWeb: true);
// 注册请求拦截、验证、异常处理
builder.Services
.RequestActionSetup()
.ResultSetup()
.ExceptionSetup();
// 注册 Swagger
builder.Services.AddSwaggerSetup();
// 跨域配置
builder.Services.AddCors(option =>
{
option.AddDefaultPolicy(builder =>
{
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
});
});
var app = builder.Build();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
3. 启动并访问 Swagger
启动项目后,访问 https://localhost:xxxx/swagger,即可看到 "系统应用" 分组下的全部 RESTful API 端点。
核心场景
场景一:创建用户并授权角色
// <summary>
// 创建用户并授权角色 —— 异步写法。先创建用户,再为其分配角色集合。
// </summary>
public async Task<bool> CreateUserWithRolesAsync(
SysUserService userService, AddUserInput input, List<long> roleIds)
{
// 1. 创建用户(账号去重检查自动执行)
var user = await userService.AddUser(input);
// 2. 授权用户角色
await userService.GrantRole(new UserRolesInput
{
UserId = user.Id,
RoleIds = roleIds
});
return true;
}
// <summary>
// 创建用户并授权角色 —— 同步写法(通过 .GetAwaiter().GetResult() 调用)
// ⚠️ 注意:在 ASP.NET 上下文中可能导致死锁,仅推荐在 Console/测试环境使用。
// </summary>
public bool CreateUserWithRoles(SysUserService userService, AddUserInput input, List<long> roleIds)
{
var user = userService.AddUser(input).GetAwaiter().GetResult();
userService.GrantRole(new UserRolesInput
{
UserId = user.Id,
RoleIds = roleIds
}).GetAwaiter().GetResult();
return true;
}
创建用户并授权流程:
开始
│
├─ 1. 校验账号是否已存在 → 存在则抛出 i18n.account_exists
│
├─ 2. 密码加密(IPasswordServer)
│
├─ 3. INSERT SysUser(用户主表)
│
└─ 4. DELETE + INSERT SysUserRole[](先清空旧角色,再批量写入新角色)
│
完成
场景二:角色菜单授权(完整权限配置)
// <summary>
// 为角色授予菜单权限 —— 异步写法。传入角色ID和菜单ID集合,先清空旧权限再批量写入。
// </summary>
public async Task GrantMenuToRoleAsync(SysRoleService roleService, long roleId, List<long> menuIds)
{
await roleService.GrantMenu(new RoleMenuInput
{
RoleId = roleId,
MenuIds = menuIds
});
}
// <summary>
// 查询角色已有的菜单权限 —— 用于授权页面的回显
// </summary>
public async Task<List<long>> GetRoleMenuIdsAsync(SysRoleService roleService, long roleId)
{
return await roleService.GetOwnMenuList(new RoleInput { Id = roleId });
}
角色菜单授权流程:
开始事务
│
├─ 1. DELETE FROM SysRoleMenu WHERE RoleId = @roleId(清空旧权限)
│
├─ 2. INSERT SysRoleMenu[](批量写入新权限)
│
└─ 3. 清除按钮权限缓存(按前缀批量删除)
│
提交事务
场景三:获取登录用户菜单树(动态导航)
// <summary>
// 获取当前登录用户的菜单树 —— 根据用户角色动态构建,自动过滤禁用菜单和按钮类型节点。
// 异步写法:适用于 Controller 或 Service 中直接调用。
// </summary>
public async Task<List<MenuTreeOutput>> GetUserMenuTreeAsync(SysMenuService menuService)
{
return await menuService.GetLoginMenuTree();
}
// <summary>
// 获取系统菜单树(用于角色授权时选择) —— 包含所有节点供管理员勾选。
// </summary>
public async Task<List<MenuTreeOutput>> GetGrantMenuTreeAsync(
SysMenuService menuService, long roleId)
{
return await menuService.TreeForGrant(
new TreeForGrantInput { RoleId = roleId },
includeButton: true,
roleId: roleId
);
}
// <summary>
// 获取登录用户菜单树 —— 同步写法
// ⚠️ 注意:在 ASP.NET 上下文中可能导致死锁,仅推荐在 Console/测试环境使用。
// </summary>
public List<MenuTreeOutput> GetUserMenuTree(SysMenuService menuService)
{
return menuService.GetLoginMenuTree().GetAwaiter().GetResult();
}
登录菜单树构建流程:
1. 获取当前用户信息(ICurrentUser)
│
2. 查询用户角色集合(SysUserRole)
│
3. 查询角色菜单集合(SysRoleMenu)
│
4. 查询所有菜单(SysMenu)
│
5. 过滤:
├─ 仅保留用户角色拥有的菜单
├─ 排除 IsDisable = Y(已禁用)
└─ 排除 Type = 按钮类型(目录/菜单保留)
│
6. 递归构建树形结构(Pid 自引用)
│
返回树形菜单
场景四:按钮权限校验(细粒度权限控制)
// <summary>
// 获取当前用户的按钮权限标识集合 —— 支持缓存,用于前端按钮显隐控制。
// 异步写法:返回 List<string>,如 ["user:add", "user:delete", "role:grant"]。
// </summary>
public async Task<List<string>> GetUserButtonPermissionsAsync(SysMenuService menuService)
{
return await menuService.GetOwnBtnPermList();
}
// <summary>
// 检查用户是否拥有特定按钮权限 —— 用于后端 API 鉴权。
// </summary>
public async Task<bool> HasPermissionAsync(SysMenuService menuService, string permission)
{
var perms = await menuService.GetOwnBtnPermList();
return perms.Contains(permission);
}
// <summary>
// 检查用户是否拥有特定按钮权限 —— 同步写法
// </summary>
public bool HasPermission(SysMenuService menuService, string permission)
{
var perms = menuService.GetOwnBtnPermList().GetAwaiter().GetResult();
return perms.Contains(permission);
}
按钮权限缓存流程:
首次请求 GetOwnBtnPermList()
│
├─ 缓存命中 → 直接返回
│
└─ 缓存未命中:
│
├─ 1. 获取用户角色(SysUserRole)
├─ 2. 获取角色菜单(SysRoleMenu)
├─ 3. 查询按钮类型菜单(Type = 按钮)
├─ 4. 提取 Permission 字段
├─ 5. 过滤空值和非法格式
├─ 6. 写入缓存,设置过期时间
│
返回权限标识集合
角色权限变更时:
→ GrantMenu() 自动调用 RemoveByPrefixKey() 清除按钮权限缓存
场景五:密码修改与重置(安全闭环)
// <summary>
// 用户自行修改密码 —— 需验证旧密码,且新密码不能与旧密码相同。
// 异步写法。
// </summary>
public async Task<bool> ChangePasswordAsync(
SysUserService userService, ChangePwdInput input)
{
// input 包含: OldPassword, NewPassword
await userService.ChangePwd(input);
return true;
}
// <summary>
// 管理员重置用户密码 —— 无需旧密码,直接设置为新密码。
// 异步写法。
// </summary>
public async Task<bool> ResetUserPasswordAsync(
SysUserService userService, ResetPwdUserInput input)
{
// input 包含: Id (用户ID), Password (新密码)
await userService.ResetPwd(input);
return true;
}
// <summary>
// 解除用户登录锁定 —— 清除密码错误次数缓存。
// 异步写法。
// </summary>
public async Task UnlockUserAsync(SysUserService userService, long userId)
{
await userService.UnlockLogin(new UnlockLoginInput { Id = userId });
}
// <summary>
// 修改密码 —— 同步写法
// </summary>
public bool ChangePassword(SysUserService userService, ChangePwdInput input)
{
userService.ChangePwd(input).GetAwaiter().GetResult();
return true;
}
密码修改流程:
1. 获取当前用户信息(ICurrentUser)
│
2. 验证旧密码(IPasswordServer.Verify)
├─ 不匹配 → 抛出 i18n.password_error
│
3. 校验新旧密码不能相同
├─ 相同 → 抛出 i18n.new_password_same_as_old
│
4. 加密新密码(IPasswordServer.Hash)
│
5. UPDATE SysUser SET Password = @newPwd WHERE Id = @userId
│
完成
配置选项详解
数据库连接配置 (ConnectionConfigs)
{
"ConnectionConfigs": [
{
"DbType": "Sqlite",
"ConnectionString": "DataSource=./ruovea.db",
"EnableUnderLine": false,
"EnableDiffLog": false,
"IsEncrypt": false,
"DbSecurity": "",
"IsDeleteFilter": true,
"IsUserIdFilter": false,
"IsTenantIdFilter": false,
"CommandTimeOut": 30
}
]
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
DbType |
string | 必填 | 数据库类型 |
ConnectionString |
string | 必填 | 连接字符串 |
EnableUnderLine |
bool | false |
驼峰转下划线 |
EnableDiffLog |
bool | false |
启用库表差异日志 |
IsEncrypt |
bool | false |
连接字符串是否加密 |
DbSecurity |
string | "" |
解密密钥(IsEncrypt=true 时使用) |
IsDeleteFilter |
bool | true |
⚠️ 全局软删除过滤(实体需继承 IDeletedEntity) |
IsUserIdFilter |
bool | false |
按创建者过滤(实体需继承 ICreatorFilter 或 EntityBase) |
IsTenantIdFilter |
bool | false |
按租户过滤(实体需继承 ITenantIdFilter) |
CommandTimeOut |
int | 30 |
SQL 命令超时时间(秒) |
JWT 认证配置 (Jwt)
{
"Jwt": {
"ValidateIssuerSigningKey": true,
"IssuerSigningKey": "3c1cbc3f546eda35168c3aa3cb91780fbe703f0996c6d123ea96dc85c70bbc0a",
"ValidateIssuer": true,
"ValidIssuer": "SecurityDemo.Authentication.JWT",
"ValidateAudience": true,
"ValidAudience": "jwtAudience",
"ValidateLifetime": true,
"ExpiredTime": 1440,
"ClockSkew": 5
}
}
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ValidateIssuerSigningKey |
bool | true |
是否验证密钥 |
IssuerSigningKey |
string | 必填 | ⚠️ 密钥,长度必须大于 16 且足够复杂 |
ValidateIssuer |
bool | true |
是否验证签发方 |
ValidIssuer |
string | 必填 | 签发方标识 |
ValidateAudience |
bool | true |
是否验证签收方 |
ValidAudience |
string | 必填 | 签收方标识 |
ValidateLifetime |
bool | true |
是否验证过期时间 |
ExpiredTime |
int | 1440 |
过期时间(分钟),默认 24 小时 |
ClockSkew |
int | 5 |
❗ 过期时间容错值(秒),默认 5 秒 |
Swagger API 版本配置
{
"Swagger": {
"ApiVersions": [
{
"Title": "系统应用",
"Version": "system"
}
]
}
}
| 配置项 | 类型 | 说明 |
|---|---|---|
Title |
string | API 版本显示标题,用于 Swagger UI 中展示 |
Version |
string | API 版本标识,用于路由和文档区分 |
DI 注册配置
// <summary>
// AddOmiSystemSetup —— 三种重载,适应不同配置来源。
// </summary>
// 重载 1:自动从全局 AppSettings 读取配置
builder.Services.AddOmiSystemSetup();
// 重载 2:传入自定义 IConfiguration
builder.Services.AddOmiSystemSetup(configuration.GetSection("MySystem"));
// 重载 3:通过 Action 委托配置 DbInitConfig
builder.Services.AddOmiSystemSetup(options =>
{
options.InitTable = true;
});
// 自定义服务生命周期
builder.Services.AddOmiSystemSetup(ServiceLifetime.Singleton);
| 方法 | 参数 | 默认值 | 说明 |
|---|---|---|---|
AddOmiSystemSetup() |
无 | — | 从 AppSettings 自动绑定配置,Scoped 生命周期 |
AddOmiSystemSetup(IConfiguration config) |
config |
— | 通过 IConfiguration 传入配置节 |
AddOmiSystemSetup(Action<DbInitConfig> config) |
config 委托 |
— | 代码内配置 DbInitConfig |
serviceLifetime |
ServiceLifetime |
Scoped |
注册所有服务的生命周期 |
// <summary>
// AddSystemInitSetup —— 初始化数据库表和种子数据。
// </summary>
// isWeb = true:初始化 Web 菜单种子数据
builder.Services.AddSystemInitSetup(isWeb: true);
// isWeb = false:初始化 API 菜单种子数据
builder.Services.AddSystemInitSetup(isWeb: false);
| 方法 | 参数 | 默认值 | 说明 |
|---|---|---|---|
AddSystemInitSetup(bool isWeb) |
isWeb |
false |
异步初始化数据库表和种子数据,控制菜单种子类型 |
⚠️ 线程安全: 切换为
Singleton生命周期时,确保注入的SugarRepository<T>和ISqlSugarClient本身支持并发访问。SqlSugar 的SqlSugarClient是线程安全的,但仓储的某些操作依赖请求上下文(如ICurrentUser自动填充),单例模式下可能导致用户信息串扰。
❗ 性能提醒:
AddSystemInitSetup使用后台任务执行表结构检查和种子数据写入。生产环境首次启动后,已完成初始化的数据库无需重复执行,可通过AddOmiSystemSetup(options => { options.InitTable = false; })关闭。
API 接口速览
所有接口自动归入 Swagger "system" 分组,默认路由前缀由 DynamicWebApi 配置决定。
SysUserService —— 用户管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync(PageUserInput) |
获取用户分页列表 |
| POST | GetRoleUserListByRoleId(long roleId) |
获取角色用户相关信息 |
| GET | UserList() |
获取用户列表 |
| GET | GetAllAsync() |
获取全部用户简要列表(仅 ID、账号、姓名) |
| POST | AddUser(AddUserInput) |
增加用户 |
| PUT | UpdateUser(UpdateUserInput) |
更新用户 |
| DELETE | DeleteUser(DeleteUserInput) |
删除用户 |
| GET | GetBaseInfo() |
查看用户基本信息 |
| PUT | UpdateBaseInfo(SysUser) |
更新用户基本信息 |
| POST | SetStatus(UserInput) |
设置用户状态(启用/停用) |
| POST | GrantRole(UserRolesInput) |
授权用户角色 |
| POST | ChangePwd(ChangePwdInput) |
修改用户密码 |
| POST | ResetPwd(ResetPwdUserInput) |
重置用户密码 |
| POST | UnlockLogin(UnlockLoginInput) |
解除登录锁定 |
| GET | GetOwnRoleList(long userId) |
获取用户拥有角色集合 |
SysRoleService —— 角色管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetPagesAsync(PageRoleInput) |
获取角色分页列表 |
| GET | GetList() |
获取角色列表 |
| POST | AddRole(AddRoleInput) |
增加角色 |
| PUT | UpdateRole(AddRoleInput) |
更新角色 |
| DELETE | DeleteRole(DeleteRoleInput) |
删除角色 |
| POST | GrantMenu(RoleMenuInput) |
授权角色菜单 |
| GET | GetOwnMenuList(RoleInput) |
根据角色 Id 获取菜单 Id 集合 |
| POST | SetStatus(RoleInput) |
设置角色状态(启用/停用) |
SysMenuService —— 菜单管理
| HTTP | 方法 | 说明 |
|---|---|---|
| GET | GetLoginMenuTree() |
获取登录用户菜单树(含权限过滤) |
| GET | GetList(MenuInput) |
获取菜单列表 |
| POST | AddMenu(AddMenuInput) |
增加菜单(含父节点校验) |
| PUT | UpdateMenu(UpdateMenuInput) |
更新菜单 |
| DELETE | DeleteMenu(DeleteMenuInput) |
删除菜单 |
| GET | GetOwnBtnPermList() |
获取用户按钮权限集合(缓存) |
| GET | GetMenuTree() |
获取系统菜单树(用于上级节点选择) |
| POST | TreeForGrant(TreeForGrantInput, bool, long) |
获取系统菜单树(用于角色授权选择) |
SysCacheService —— 缓存管理
| HTTP | 方法 | 说明 |
|---|---|---|
| DELETE | Remove(string key) |
删除指定缓存 |
| DELETE | Clear(string profix) |
清空所有缓存 |
| DELETE | RemoveByPrefixKey(string prefixKey) |
根据键名前缀批量删除缓存 |
| GET | GetKeysByPrefixKey(string prefixKey) |
根据键名前缀获取键名集合 |
| GET | GetValue(string key) |
获取缓存值 |
| POST | Set(string key, object value) |
增加缓存(内部方法,NonAction) |
| POST | Set(string key, object value, TimeSpan expire) |
增加缓存并设置过期时间(内部方法) |
| GET | Get<T>(string key) |
获取泛型缓存(内部方法) |
| GET | ExistKey(string key) |
检查缓存是否存在(内部方法) |
内部服务(不公开 API)
| 服务 | 依赖 | 说明 |
|---|---|---|
SysUserRoleService |
SugarRepository<SysUserRole>, SysCacheService |
用户角色关联 CRUD(供 SysUserService 和 SysRoleService 内部调用) |
SysRoleMenuService |
SugarRepository<SysRoleMenu>, SysCacheService |
角色菜单关联 CRUD(供 SysRoleService 和 SysMenuService 内部调用) |
错误处理与日志
错误码速查
组件内部使用 i18n 国际化资源管理错误信息,支持多语言切换:
| 错误码 | 含义 | 触发场景 |
|---|---|---|
i18n.account_exists |
账号已存在 | 创建用户时账号重复 |
i18n.account_not_exists |
账号不存在 | 登录或查询时账号未找到 |
i18n.data_exists |
数据已存在 | 创建角色/菜单时 Code 或名称重复 |
i18n.dict_status_error |
字典状态错误 | 字典数据状态异常 |
i18n.illegal_operation_self |
非法操作,禁止删除自己 | 用户尝试删除自身账号 |
i18n.new_password_same_as_old |
新密码不能与旧密码相同 | 修改密码时新旧密码一致 |
i18n.parent_node_cannot_be_button |
父节点不能为按钮类型 | 创建菜单时选择了按钮类型作为父节点 |
i18n.password_error |
旧密码输入错误 | 修改密码时旧密码校验失败 |
i18n.permission_id_format_empty |
权限标识格式为空 | 按钮权限标识为空字符串 |
i18n.permission_id_format_error |
权限标识格式错误 | 按钮权限标识不符合 xxx:xxx 格式 |
i18n.prohibit_delete_admin |
禁止删除系统管理员角色 | 尝试删除系统管理员角色 |
i18n.prohibit_delete_super_admin |
禁止删除超级管理员 | 尝试删除超级管理员账号 |
i18n.prohibit_modify_self_status |
禁止修改本人账号状态 | 尝试启用/停用自己的账号 |
i18n.prohibit_modify_super_admin_status |
禁止修改超级管理员状态 | 尝试修改超级管理员的启用状态 |
i18n.prohibit_same_node_as_parent |
禁止本节点与父节点相同 | 菜单编辑时将自身设为父节点 |
i18n.record_not_exists |
记录不存在 | 根据 ID 查询/更新时记录缺失 |
i18n.role_has_accounts |
此角色下面存在账号禁止删除 | 删除角色时仍有用户关联 |
i18n.route_name_duplicate |
路由名称重复 | 创建菜单时路由名称已存在 |
ErrorEnum.D4000 |
通用业务错误 | 参数校验失败等通用错误 |
异常处理示例
// <summary>
// 安全创建用户 —— 捕获参数、业务和数据库异常。
// 异步写法。
// </summary>
public async Task<(bool Success, string Message)> SafeCreateUserAsync(
SysUserService userService, AddUserInput dto)
{
try
{
await userService.AddUser(dto);
return (true, "用户创建成功");
}
catch (Exception ex) when (ex.Message.Contains("account_exists"))
{
// 账号已存在
return (false, $"账号 '{dto.Account}' 已存在,请更换账号名");
}
catch (ArgumentException ex)
{
// 参数校验失败(必填字段缺失、格式错误)
return (false, $"参数校验失败: {ex.Message}");
}
catch (Exception ex) when (ex.Message.Contains("transaction", StringComparison.OrdinalIgnoreCase))
{
// 事务执行失败(数据库层面错误)
return (false, $"数据操作失败,已自动回滚: {ex.Message}");
}
}
// <summary>
// 安全删除角色 —— 含关联数据防护检查。
// 异步写法。
// </summary>
public async Task<(bool Success, string Message)> SafeDeleteRoleAsync(
SysRoleService roleService, long roleId)
{
try
{
await roleService.DeleteRole(new DeleteRoleInput { Id = roleId });
return (true, "角色删除成功");
}
catch (Exception ex) when (ex.Message.Contains("prohibit_delete_admin"))
{
return (false, "系统管理员角色禁止删除");
}
catch (Exception ex) when (ex.Message.Contains("role_has_accounts"))
{
return (false, "该角色下仍有用户关联,请先解除用户角色关系");
}
catch (Exception ex) when (ex.Message.Contains("record_not_exists"))
{
return (false, "角色不存在或已被删除");
}
}
// <summary>
// 安全删除角色 —— 同步写法(仅在非 ASP.NET 上下文使用)
// </summary>
public (bool Success, string Message) SafeDeleteRole(SysRoleService roleService, long roleId)
{
try
{
roleService.DeleteRole(new DeleteRoleInput { Id = roleId }).GetAwaiter().GetResult();
return (true, "角色删除成功");
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
日志集成
组件不直接输出日志,依赖调用方集成的日志框架。推荐在 Program.cs 中配置 Serilog 或 NLog 来捕获:
// SqlSugar 的 SQL 日志可通过 AOP 事件捕获
builder.Services.AddSqlSugarSetup(); // 内部配置了 SQL 执行日志
版本迁移指南
从 8.0.x 升级到 10.0.x
| 变更项 | 说明 |
|---|---|
| TFM 升级 | net8.0 → net10.0,需同步升级所有依赖包到 10.0.* 版本 |
| 包版本对齐 | RuoVea.ExFilter、RuoVea.ExJwtBearer、RuoVea.ExPws、RuoVea.OmiApi.Config、RuoVea.DynamicWebApi、RuoVea.ExSugar 均需升至对应 10.0.* |
| API 兼容 | 所有公开 API 向后兼容,无需修改业务代码 |
| 数据库 | 表结构无变更,无需执行迁移脚本 |
API 变更历史
| 版本 | 变更 |
|---|---|
8.0.2.19 |
修复多字段查询时 SqlSugar 因重复参数 @value 键而报错的问题 |
8.0.2.x |
组件版本升级,图表数据缓存 |
8.0.2.x |
表结构初始化处理 |
| 更早版本 | 初始发布 |
常见问题
Q: 如何切换数据库?
修改 appsettings.json 中的 DbType 和 ConnectionString,然后重新运行。AddSystemInitSetup 会自动为新数据库创建表结构并写入种子数据。
// MySql 示例
{ "DbType": "MySql", "ConnectionString": "Server=localhost;Database=ruovea;Uid=root;Pwd=123456;" }
// SqlServer 示例
{ "DbType": "SqlServer", "ConnectionString": "Server=.;Database=ruovea;Trusted_Connection=True;" }
// PostgreSQL 示例
{ "DbType": "PostgreSQL", "ConnectionString": "Host=localhost;Database=ruovea;Username=postgres;Password=123456;" }
Q: 如何自定义 API 路由前缀?
在 AddDynamicWebApi 中配置 DefaultApiPrefix:
builder.Services.AddDynamicWebApi(options =>
{
options.DefaultApiPrefix = "/openapi/api";
});
Q: Web 菜单和 API 菜单有什么区别?
AddSystemInitSetup(isWeb: true) 初始化的是前端 Web 应用的菜单种子数据(含路由路径、组件、图标等),isWeb: false 初始化的是纯 API 接口的菜单种子数据。根据您的项目类型选择合适的模式。
Q: ⚠️ 超级管理员账号是什么?有哪些防护?
种子数据中预置的超级管理员账号在组件内部有严格防护:
- 禁止删除超级管理员(
i18n.prohibit_delete_super_admin) - 禁止修改超级管理员状态(
i18n.prohibit_modify_super_admin_status) - 禁止修改本人账号状态(
i18n.prohibit_modify_self_status) - 禁止删除系统管理员角色(
i18n.prohibit_delete_admin)
这些防护确保系统始终至少有一个可用管理员。
Q: ❗ 按钮权限缓存何时失效?
按钮权限缓存在以下场景自动失效:
- 角色菜单授权变更(
GrantMenu)时,自动调用RemoveByPrefixKey清除相关缓存 - 缓存过期时间到达后自动失效
- 手动调用
SysCacheService.RemoveByPrefixKey(prefixKey)强制清除
如果需要立即刷新缓存,可通过 SysCacheService 的 RemoveByPrefixKey 方法清除按钮权限缓存前缀。
Q: 为什么删除角色时提示"此角色下面存在账号"?
该角色仍有用户关联时,为防止权限失控,组件会抛出 i18n.role_has_accounts 错误。解决方案:通过 SysUserService.GrantRole() 先解除所有用户与该角色的关联,再执行删除。
Q: 菜单的三种类型(目录/菜单/按钮)各自用途是什么?
| 类型 | 说明 | 典型场景 |
|---|---|---|
| 目录 | 分组节点,无路由组件,仅用于组织菜单树 | 系统管理、内容管理等顶级目录 |
| 菜单 | 有路由和组件的页面节点,显示在左侧导航 | 用户管理、角色管理等具体页面 |
| 按钮 | 权限控制节点,不显示在菜单树中,仅用于权限标识 | user:add、role:delete 等操作按钮 |
创建菜单时,父节点不能为按钮类型(
i18n.parent_node_cannot_be_button),且路由名称不允许重复(i18n.route_name_duplicate)。
Q: ⚠️ 修改密码后需要重新登录吗?
修改密码操作不会使当前 JWT Token 失效。如果业务需要修改密码后强制重新登录,请在调用方额外实现 Token 失效逻辑(如将 Token 加入黑名单缓存)。
Q: ❗ 多租户场景下数据如何隔离?
所有实体(SysUser、SysRole、SysMenu)均实现 ITenantEntity 接口。启用租户过滤后,查询和操作会自动按 TenantId 过滤。配置方式:
{
"ConnectionConfigs": [
{
"IsTenantIdFilter": true
}
]
}
许可证
本项目基于 Apache 2.0 License 开源发布。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- RuoVea.DynamicWebApi (>= 10.0.0)
- RuoVea.ExFilter (>= 10.0.0.6)
- RuoVea.ExJwtBearer (>= 10.0.0.5)
- RuoVea.ExPws (>= 10.0.0.4)
- RuoVea.ExSugar (>= 10.0.0.8)
- RuoVea.OmiApi.Config (>= 10.0.0.4)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on RuoVea.OmiApi.UserRoleMenu:
| Package | Downloads |
|---|---|
|
RuoVea.OmiUserRoleMenu
字典管理 |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.0.5 | 48 | 6/26/2026 |
| 10.0.0.4 | 93 | 6/24/2026 |
| 10.0.0.3 | 116 | 5/28/2026 |
| 10.0.0.2 | 105 | 5/28/2026 |
| 10.0.0.1 | 123 | 3/23/2026 |
| 9.0.0.3 | 128 | 5/28/2026 |
| 9.0.0.2 | 113 | 5/28/2026 |
| 9.0.0.1 | 154 | 3/23/2026 |
| 9.0.0 | 138 | 1/27/2026 |
| 8.0.2.20 | 50 | 6/26/2026 |
| 8.0.2.19 | 107 | 6/24/2026 |
| 8.0.2.18 | 126 | 5/28/2026 |
| 8.0.1.17 | 122 | 5/28/2026 |
| 8.0.1.16 | 131 | 3/23/2026 |
| 7.0.2.18 | 113 | 5/28/2026 |
| 7.0.1.17 | 118 | 5/28/2026 |
| 7.0.1.16 | 126 | 3/23/2026 |
| 6.0.2.18 | 130 | 5/28/2026 |
| 6.0.2.17 | 131 | 5/28/2026 |
| 6.0.2.16 | 144 | 3/23/2026 |