模块深度解析 · 系统业务层(ruoyi-system)
📍 核心源码在
ruoyi-system/src/main/java/com/ruoyi/system/下的service/、mapper/、domain/。这是业务规则真正所在之地——Controller(04 章)是薄外壳,ruoyi-system的 Service 承担了所有「用户/角色/菜单/部门/字典/配置/日志」的具体玩法。
1. 模块概述
ruoyi-system 提供后台管理系统的全部业务实体与操作逻辑,不暴露任何 HTTP,纯粹供给上层 Service 接口。结构清晰为「接口 + 实现 + Mapper」三层:
| 层 | 内容 | 示例 |
|---|---|---|
接口 service/ISysXxxService |
业务契约 | ISysUserService、ISysMenuService |
实现 service/impl/SysXxxServiceImpl |
具体逻辑 + @DataScope 数据权限 |
SysUserServiceImpl |
mapper/SysXxxMapper + XML |
MyBatis 数据访问 | SysMenuMapper |
domain/SysXxx |
领域实体(关联表/中间表) | SysUserPost、SysRoleMenu |
domain/vo/RouterVo/MetaVo/TreeSelect |
前端视图对象 | 路由、下拉树 |
graph TB
subgraph ruoyi-system
ISVC[service 接口 ISysXxxService]
SVC[service/impl 实现]
MAP[Mapper 接口]
XML[XML 映射文件]
DOM[domain 实体+VO]
end
ISVC <--> SVC
SVC --> MAP
MAP --> XML
SVC --> DOM
MAP --> DOM
SVC --> FR[framework 数据权限切面
@DataScope]
📌 模块数据全景:用户
sys_user、角色sys_role、菜单sys_menu、部门sys_dept、岗位sys_post、字典sys_dict_type/data、参数sys_config、通知sys_notice、5 类日志(操作/登录/在线/任务)、及关联表sys_user_role/sys_role_menu/sys_role_dept/sys_user_post。
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 对象 | 操作 | 含义 |
|---|---|---|---|
SysXxxService. |
select + Xxx |
List/ById/ByUserName/BuildTree |
查询(组合查询/按ID/按名/建树) |
SysXxxService. |
insert/update/delete |
增删改 | 写操作 |
SysXxxService. |
check + XxxUnique |
唯一性校验 | 防重复 |
SysMenuService. |
build + Menus/Tree |
构建路由/树 | 前端数据组装 |
SysRoleService. |
auth/insertAuth/insertRole |
授权 | 角色授权与分配 |
命名规律:查询统一 select 前缀、写操作 insert/update/delete、唯一性 check...Unique、树/路由构建 build...——一眼即知每个方法职责。
2.2 易混淆函数对比
对比一:selectMenuList(查列表)vs selectMenuTreeByUserId(查菜单树)
| 对比维度 | selectMenuList(Long) |
selectMenuTreeByUserId(Long) |
|---|---|---|
| 返回 | 扁平 List<SysMenu>(可选过滤) |
层级树 List<SysMenu> |
| 是否 admin 分支 | 是(admin 全量) | 是(selectMenuTreeAll) |
| 用途 | 管理菜单表格 | 构建前端路由 / 权限树 |
| 一句话区分 | 拿到”一维列表” | 拿到”多维树”,供 buildMenus 用 |
对比二:insertUserRole(用户服务内)vs insertAuthUsers(角色服务内)
| 对比维度 | SysUserServiceImpl.insertUserRole |
SysRoleServiceImpl.insertAuthUsers |
|---|---|---|
| 入口角度 | 「给用户绑角色」 | 「给角色分配用户」 |
| 参数 | Long userId, Long[] roleIds |
Long roleId, Long[] userIds |
| 落表 | sys_user_role |
sys_user_role |
| 一句话区分 | 同一张关联表,不同入口方向 | 用户派 vs 角色派 |
3. API Signatures(关键 Service)
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
4. 核心机制深度解析
4.1 菜单 → 前端路由(buildMenus)
这是”数据库菜单怎么变成页面”的秘密。selectMenuTreeByUserId 拿到层级菜单后,buildMenus 把每个 SysMenu 转成 RouterVo(对 src 前端可识别)。逐行精讲(SysMenuServiceImpl.java:173-222):
1 | public List<RouterVo> buildMenus(List<SysMenu> menus) |
关键点:
- **
getRouteName**:重名时加Dup后缀,避免 Vue 路由 name 冲突。 - **
getRouterPath**:目录父路由自动加/,子路由拼接{父path}/{子path}。 - **
getComponent**:映射成实际.vue文件地址(如system/user/index→ 编译后 chunk 路径)。 - 前端拿到
RouterVo数组后,用component动态注册路由(views/**/index.vue),实现「加菜单即加页面、无需改前端代码」——这是若依代码生成器落地的关键。
4.2 树构建:buildMenuTree / buildDeptTree(扁平 → 树)
通用算法(无数据库递归,纯内存组装):
1 | public List<SysMenu> buildMenuTree(List<SysMenu> menus) |
📍 源码:SysMenuServiceImpl.java:231-250
recursionFn遍历剩余菜单,凡parentId == 当前menu.id就挂为 child 并递归。此算法 O(n²),但菜单数据量小可接受。
4.3 用户权限集合:selectMenuPermsByUserId
1 | public Set<String> selectMenuPermsByUserId(Long userId) |
普通用户路径走 SysMenuMapper.xml 的 selectMenuPermsByUserId:三表 LEFT JOIN(sys_user_role / sys_role_menu / sys_menu)+ DISTINCT,把该用户所有菜单的 perms 字段去重取出,最后 filter(perms 非空) 返回 Set<String>。这组权限最终灌进 LoginUser.permissions,供 02 章 @ss.hasPermi / SecurityUtils.hasPermi 匹配。
4.4 数据权限的落点:SysUserServiceImpl.selectUserList
1 |
|
数据权限在实现中靠
@DataScope(deptAlias = "d", userAlias = "u")注解标记,真正 SQL 注入发生在 DataScopeAspect(framework),它读出LoginUser的dataScope值拼成(d.dept_id IN ... OR d.dept_id = u.dept_id ...)追加到查询语句。这也解释了为什么SysUserServiceImpl的 service 方法本身看不到任何权限逻辑——它被切面透明增强。
4.5 部门:祖链 ancestors 维护
SysDept 用 ancestors(祖链,如 0,1,100)快速限缩子树,插入部门时由 SysDeptServiceImpl.insertDept 计算:
1 | 子部门 ancestors = 父部门 ancestors + "," + 父部门 deptId |
移动部门时用 updateDeptChildren 递归更新所有子孙的祖先串(SysDeptServiceImpl.java:271-291),替代”一层层向上找父节点”——这是典型的物化路径(Materialized Path)做法。
5. 业务规则精选
5.1 用户 CRUD 的隐藏规则
| 规则 | 实现 | 原因 |
|---|---|---|
| admin 不可改/不可删 | checkUserAllowed:user.isAdmin() 抛异常 |
保留初始超管 |
| 越权操作防护 | checkUserDataScope |
非超管不能操作他人 |
| 密码 BCrypt | SecurityUtils.encryptPassword 加密入库 |
明文不落库 |
| 用户-角色绑定 | insertUserRole 拆 sys_user_role |
多对多 |
| 导入 | importUser 失败逐行记错 |
Excel 批量友好 |
5.2 角色数据范围(dataScope)
SysRole.dataScope 取值:1 全部数据权限 / 2 自定数据权限(配合 sys_role_dept) / 3 本部门 / 4 本部门及以下 / 5 仅本人。@DataScope(permission="system:user:list") 依据它拼最终 SQL(详见 数据权限中心)。
6. 学习检查点
📝 本章小结
ruoyi-system= 业务实体 + Service(计谋)+ Mapper(数据),不含 HTTP;Controller 在 admin。- 菜单驱动一切:
buildMenus把 DB 菜单转成前端路由,是实现「加页面不改前端」的枢纽。 - 树构建用内存递归 + 物化路径(ancestors),部门/菜单都靠它。
- 数据权限不在 system 里写,而是靠
@DataScope注解 + framework 切面在 service 方法上透明注入 SQL。 - 权限集合经
sys_user_role → sys_role_menu → sys_menu三表 JOIN 产出,灌入LoginUser.permissions。
🤔 思考题
buildMenuTree如何判断「顶级节点」?若所有节点都不满足会怎样?参考答案
若某节点
parentId不在全量menuId集合tempList中,即为顶级(SysMenuServiceImpl.java:239-243)。若全在(如所有节点 parentId 都指向有效子孙),returnList为空,兜底returnList = menus直接返回原始列表,避免空树导致前端白屏。selectMenuPermsByUserId为何要WHERE perms != NULL且用DISTINCT?参考答案
菜单里大量是目录/按钮节点,
perms字段为空,若不过滤会灌入null破坏权限集合;三表 JOIN 一个角色多菜单、一个用户多角色,会产生笛卡尔重复,DISTINCT去重保证LoginUser.permissions是干净的权限字符串集。源码见 SysMenuMapper.xml 的selectMenuPermsByUserId与其where段。部门「移动父级」时,为何必须重算祖链而不是让它自然失效?
参考答案
若只改父节点
parentId,其所有子孙的ancestors仍是旧链,按ancestors LIKE '0,老,..%'查询的数据权限/子树会漏/错。updateDeptChildren(SysDeptServiceImpl.java:271)把旧前缀替换成新前缀批量更新,保证物化路径始终一致。