基于若依(RuoYi-Vue)自建“车间设备”模块与行级数据权限控制全栈实战
一、前言
在企业级系统开发中,控制按钮看得见、菜单点得开只是权限管理的第一步。当面对“同一张业务表,不同部门的用户登录后只能看到属于自己部门的数据”这种行级过滤需求时,就需要引入数据权限控制。
本文将基于若依(RuoYi-Vue)前后端分离版开源框架,手把手教你如何从零开始进行数据库建表、编写后端 CSMD(Controller, Service, Model/Mapper, DAO/XML) 全链路代码、配置前端菜单路由,并最终完美实现“车间设备数据”的行级数据权限隔离。
二、第一阶段:环境准备与数据库设计 (Database)
业务背景:系统需要管理车间的设备资产,且数据权限必须紧密挂钩在部门(车间)这个最小单位上。因此,设备表中必须包含部门 ID(dept_id)字段。
2.1 执行建表与数据插入 SQL
打开数据库的连接工具(如 Navicat),连接到若依对应的数据库,新建查询并执行以下 SQL 语句:
-- 创建车间设备表
CREATE TABLE `sys_equipment` (
`equip_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录编号',
`equip_no` varchar(50) NOT NULL COMMENT '设备号',
`temperature` int(11) DEFAULT '0' COMMENT '温度',
`dept_id` bigint(20) DEFAULT NULL COMMENT '部门[车间]编号',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`equip_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='车间设备表';
-- 插入实验测试数据(105为测试部门,106为财务部门)
INSERT INTO `sys_equipment` VALUES (1, 'guotianxin_026', 45, 105, 'admin', '2026-05-27 09:21:04');
INSERT INTO `sys_equipment` VALUES (2, 'guotianxin_027', 46, 105, 'admin', '2026-05-27 09:21:04');
INSERT INTO `sys_equipment` VALUES (3, 'guotianxin_028', 36, 106, 'admin', '2026-05-27 09:21:04');
INSERT INTO `sys_equipment` VALUES (4, 'guotianxin_029', 37, 106, 'admin', '2026-05-27 09:21:04');
执行成功后,我们在数据库中核对数据。

图1:表内的4条初始化测试数据,包含设备号、温度以及对应的部门编号(105和106)
三、第二阶段:后端全链路开发 (CSMD 架构落地)
接下来我们需要在后端的 ruoyi-system 和 ruoyi-admin 模块中手动建立 Java 类与 XML 映射文件,打通业务查询链路。
3.1 实体类 (Model / Domain)
在 ruoyi-system 模块的 com.ruoyi.system.domain 包下新建 SysEquipment.java 类。 ⚠️ 核心要点:必须继承 BaseEntity!因为若依的数据权限切面类会将动态组装好的 SQL 语句塞入 BaseEntity 的 params 属性中。
package com.ruoyi.system.domain;
import com.ruoyi.common.core.domain.BaseEntity;
public class SysEquipment extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long equipId;
private String equipNo;
private Integer temperature;
private Long deptId;
public Long getEquipId() { return equipId; }
public void setEquipId(Long equipId) { this.equipId = equipId; }
public String getEquipNo() { return equipNo; }
public void setEquipNo(String equipNo) { this.equipNo = equipNo; }
public Integer getTemperature() { return temperature; }
public void setTemperature(Integer temperature) { this.temperature = temperature; }
public Long getDeptId() { return deptId; }
public void setDeptId(Long deptId) { this.deptId = deptId; }
}

图2:在 IDEA 中编写的 SysEquipment 实体类,继承自 BaseEntity
3.2 数据访问接口 (Mapper / DAO)
在 com.ruoyi.system.mapper 包下新建接口 SysEquipmentMapper.java:
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysEquipment;
public interface SysEquipmentMapper {
public List<SysEquipment> selectEquipmentList(SysEquipment equipment);
}

图3:在 IDEA 中定义的 SysEquipmentMapper 接口,包含 selectEquipmentList 查询方法
3.3 XML 映射文件 (MyBatis XML)
在 ruoyi-system 模块的 src/main/resources/mapper/system 路径下,新建 SysEquipmentMapper.xml。 ⚠️ 核心要点:必须使用 left join sys_dept d 关联部门表,并在 <where> 标签的末尾追加 ${params.dataScope} 占位符,注意最后的 ${params.dataScope} 是动态拼接 SQL 的核心。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysEquipmentMapper">
<resultMap type="SysEquipment" id="SysEquipmentResult">
<result property="equipId" column="equip_id" />
<result property="equipNo" column="equip_no" />
<result property="temperature" column="temperature" />
<result property="deptId" column="dept_id" />
</resultMap>
<select id="selectEquipmentList" parameterType="SysEquipment" resultMap="SysEquipmentResult">
select e.equip_id, e.equip_no, e.temperature, e.dept_id
from sys_equipment e
left join sys_dept d on e.dept_id = d.dept_id
<where>
<if test="equipNo != null and equipNo != ''"> and e.equip_no = #{equipNo}</if>
<if test="deptId != null "> and e.dept_id = #{deptId}</if>
${params.dataScope}
</where>
</select>
</mapper>

图4:在 IDEA 中配置的 XML 文件
3.4 业务逻辑层接口与实现类 (Service)
在 com.ruoyi.system.service 下创建接口 ISysEquipmentService.java:
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.SysEquipment;
public interface ISysEquipmentService {
public List<SysEquipment> selectEquipmentList(SysEquipment equipment);
}

图5:在 IDEA 中定义的 ISysEquipmentService 服务层接口
在 com.ruoyi.system.service.impl 包下新建核心实现类 SysEquipmentServiceImpl.java。 🛡️ 核心要点:在实现方法上必须添加 @DataScope(deptAlias = "d") 注解。这句注解会告诉若依的数据权限切面类(DataScopeAspect),当前业务中部门表的别名是 d。
package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.system.domain.SysEquipment;
import com.ruoyi.system.mapper.SysEquipmentMapper;
import com.ruoyi.system.service.ISysEquipmentService;
@Service
public class SysEquipmentServiceImpl implements ISysEquipmentService {
@Autowired
private SysEquipmentMapper equipmentMapper;
@Override
@DataScope(deptAlias = "d") // AOP切面拦截点,声明部门表别名为 d
public List<SysEquipment> selectEquipmentList(SysEquipment equipment) {
return equipmentMapper.selectEquipmentList(equipment);
}
}

图6:在 IDEA 中编写的 Service 实现类
3.5 控制层编写 (Controller)
在前端发起 HTTP 请求时,需要有对应的控制器接收。我们在 ruoyi-admin 模块的 com.ruoyi.web.controller.system 包下建立 SysEquipmentController.java。在 Controller 中暴露接口。这一步和普通的数据分页查询完全一致,权限过滤的黑盒工作已经在 Service 和 AOP 层面完成了
package com.ruoyi.web.controller.system;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.SysEquipment;
import com.ruoyi.system.service.ISysEquipmentService;
@RestController
@RequestMapping("/system/equipment")
public class SysEquipmentController extends BaseController {
@Autowired
private ISysEquipmentService equipmentService;
@PreAuthorize("@ss.hasPermi('system:equipment:list')")
@GetMapping("/list")
public TableDataInfo list(SysEquipment equipment) {
startPage();
List<SysEquipment> list = equipmentService.selectEquipmentList(equipment);
return getDataTable(list);
}
}

图7:在 IDEA 中编写的控制层 Controller
四、 第三阶段:前端菜单配置与系统基础数据搭建
后端基础就绪后,我们登录系统进行界面可视化配置。
4.1 配置前端文件
1. 新建前端接口文件 (equipment.js)
我们需要在前端定义一个请求,用来调用你的 Java 后端 /system/equipment/list 接口。
① 在 IDEA 左侧的项目树中,依次展开:ruoyi-ui ➡️ src ➡️ api ➡️ system
② 右键点击 system 文件夹,选择 New ➡️ File。
③ 输入文件名:equipment.js,点击回车。
④把下面的代码复制进去并保存:
import request from '@/utils/request'
// 查询车间设备列表
export function listEquipment(query) {
return request({
url: '/system/equipment/list',
method: 'get',
params: query
})
}
如下图所示:

图8:新建并编写equipment.js
2. 新建前端页面文件 (index.vue)
① 在 IDEA 左侧项目树中,找到:ruoyi-ui ➡️ src ➡️ views ➡️ system,右键点击 system 文件夹,选择 New ➡️ Directory(新建文件夹),输入文件夹名称:equipment,点击回车,右键点击刚刚新建的 equipment 文件夹,选择 New ➡️ File,输入文件名:index.vue,点击回车。
② 把下面的完整页面代码复制进去并保存:
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
<el-form-item label="设备号" prop="equipNo">
<el-input
v-model="queryParams.equipNo"
placeholder="请输入设备号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="equipmentList" style="width: 100%">
<el-table-column label="记录编号" align="center" prop="equipId" />
<el-table-column label="设备号" align="center" prop="equipNo" />
<el-table-column label="温度" align="center" prop="temperature" />
<el-table-column label="部门[车间]编号" align="center" prop="deptId" />
</el-table>
</div>
</template>
<script>
// 引入我们在第一步中写的接口方法
import { listEquipment } from "@/api/system/equipment";
export default {
name: "Equipment",
data() {
return {
// 遮罩层
loading: true,
// 设备表格数据
equipmentList: [],
// 查询参数
queryParams: {
equipNo: null,
}
};
},
created() {
this.getList();
},
methods: {
/** 查询车间设备列表 */
getList() {
this.loading = true;
listEquipment(this.queryParams).then(response => {
this.equipmentList = response.rows;
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams.equipNo = null;
this.handleQuery();
}
}
};
</script>
如下图所示:

图9:新建并编写index.vue
4.2 增加系统菜单配置
1. 使用最高权限账户 admin 登录若依系统后台,进入 系统管理 -> 菜单管理,点击 + 新增 按钮:

图10:在菜单管理页面点击新增,准备挂载新的子菜单
2. 按照下图参数配置填写。组件路径填写 system/equipment/index,权限字符填写 system:equipment:list:,显示排序和图标任意选择,完成后点击确定提交。

图11:核对填写的表单
3. 提交成功后,刷新一下浏览器,左侧菜单栏的“系统管理”下方就会多出一个 “设备信息” 菜单:

图12:左侧导航栏成功渲染出“设备信息”菜单
4.3 检查与完善组织架构
进入 系统管理 -> 部门管理。确保系统中存在我们要演练的两个平行部门:测试部门(ID 105)和财务部门(ID 106)。

图13:部门管理列表中清晰展示的“测试部门”和“财务部门”
4.4 创建并关联不同车间的员工用户
进入 系统管理 -> 用户管理。我们需要配置两个不同部门的普通员工:
1. 测试车间员工 ry:将用户 ry 的归属部门选择为 “测试部门”,角色勾选为 “普通角色”。

图14:用户管理中编辑 ry 用户,将其归属于测试部门并赋予普通角色
2. 财务车间员工 Three Stones:新增Three Stones用户,将用户 Three Stones 的归属部门选择为 “财务部门”,角色同样勾选为 “普通角色”。

图15:编辑 Three Stones用户,将其归属于财务部门,同样赋予普通角色

图16:用户管理列表新增了Three Stones用户
五、 第四阶段:核心操作——分配角色数据权限
这是实现行级隔离的最关键步骤。我们将为 common(普通角色)配置数据权限范围。
1. 进入 系统管理 -> 角色管理。
2. 找到权限字符为 common 的 普通角色,点击其操作栏最右侧的 “数据权限” 按钮:

图17:在角色管理列表中,点击普通角色的数据权限操作
3. 在弹出的“分配数据权限”浮窗中,将权限范围的下拉菜单由默认选项更改为 【本部门数据权限】,修改完毕后点击右下角 确定 保存。
图18:关键配置——将普通角色的数据权限变更为“本部门数据权限”
六、第五阶段:多用户登录切换效果验证与底层 SQL 日志逆向分析
现在,所有的前后端配置和权限划分已经全部完成。我们通过切换三个不同的账号登录系统,观察前端数据展现以及后端 IDE 控制台打印出来的 SQL 语句。
6.1 场景一:超级管理员账户 (admin) 效果与日志
🍉 前端展示:
使用 admin 登录,点击进入“设备信息”。由于管理员不受数据权限限制,页面能够拉取到全表的 全部 4 条 设备记录(包含测试车间和财务车间)

图19:admin账号登录,前端表格无过滤,完整展示4条数据
🍉 控制台底层日志分析:
在控制台日志中查看 MyBatis 的 Preparing 执行语句,发现由于是系统管理员,DataScopeAspect 判定不追加过滤 SQL,因此查询语句直接结束,没有 WHERE 限制条件:

图20:MyBatis 打印的输出结果,Total 为 4,展示完整的 4 条查询结果
6.2 场景二:测试车间员工账户 (ry) 效果与日志
🍉前端展示:
登出管理员,使用账号 ry 登录系统。再次点击“设备信息”,发现此时前端表格发生了明显的行级截断,只能查看到 dept_id 为 105 的测试车间设备数据(共 2 条)。

图21:ry 账户登录,前端只筛选出了 2 条属于测试部门的设备
🍉 控制台底层日志分析:
在 ry 触发请求时,后端的 AOP 切面开始介入。切面捕获到该用户的角色具有“本部门数据权限”限制,并且其部门 ID 为 105,于是在运行时动态地将 AND (d.dept_id = 105) 追加到了 SQL 的末尾:

图22:ry 请求对应的日志
6.3 场景三:财务车间员工账户 (Three Stones) 效果与日志
🍉 前端展示:
登出账号,切换为 Three Stones账号登录。再次查看“设备信息”页面,此时前端视图再次发生精准改变,页面上只呈现出了财务车间(ID 106)所持有的 2 条 资产:

图23:ry 账户登录,前端只筛选出了 2 条属于测试部门的设备
🍉 控制台底层日志分析:
后台执行链发生同样的动态变换。由于 Three Stones 属于财务部门,切面动态拼接和注入的 SQL 条件在运行时自动替换为了 AND (d.dept_id = 106):
图24:ry 请求对应的日志
6.4 核心原理:在若依的 AOP 切面类中查看
数据权限的判断核心逻辑全部在 ruoyi-common 模块的 DataScopeAspect.java(数据权限切面处理类)中。
当登录用户 ry 并访问设备列表时,这个切面类会利用 Spring Security 拦截到当前登录用户:
// 1. 获取当前登录用户信息
LoginUser loginUser = SecurityUtils.getLoginUser();
SysUser user = loginUser.getUser();
// 2. 获取该用户所属的部门ID(此时user.getDeptId()拿到的就是数据库里的 105)
Long userDeptId = user.getDeptId();
随后,它会遍历该用户所拥有的角色数据权限。当识别到你的权限范围是 【本部门数据权限】(对应常量 DATA_SCOPE_DEPT)时,就会执行类似下面的代码:
// 3. 动态拼接SQL,把 105 转化为字符串拼进 SQL String
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
// 最终组装出来的片段就是: OR d.dept_id = 105



图25-27:AOP 切面类中查看
七、总结与反思
回顾这节数据权限控制课,它的本质原理与我们常打交道的数据分页查询高度一致 :两者的底层核心思想都是通过在运行时动态拦截并修改/组装底层 SQL 语句来实现数据控制 。
通过 CSMD 的链条分析和 AOP 切面对 BaseEntity 内部私有属性的拼装 ,我们不必在每个查询方法里去人肉写 where dept_id = ... 这种脏代码,而是优雅地通过一个注解解耦了复杂的业务隔离逻辑 。这也是在开发大型企业级系统时,大家应该要灵活举一反三和掌握的底层思维 。
更多推荐



所有评论(0)