feat(permission): 添加菜单数据权限功能
- 新增菜单数据规则表和角色菜单数据规则关联表 - 实现菜单数据权限切面和处理器 - 添加数据规则条件和变量枚举 - 实现变量替换工具类和规则构建逻辑 - 在权限分配中集成菜单数据规则关联功能 - 优化部门ID解析逻辑,支持从用户信息中获取默认部门 - 添加菜单组件查询方法和公司访问上下文拦截器改进
This commit is contained in:
617
docs/菜单数据权限使用文档.md
Normal file
617
docs/菜单数据权限使用文档.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# 菜单数据权限使用文档
|
||||
|
||||
## 📖 目录
|
||||
|
||||
1. [功能介绍](#功能介绍)
|
||||
2. [架构说明](#架构说明)
|
||||
3. [开发指南](#开发指南)
|
||||
4. [配置指南](#配置指南)
|
||||
5. [注意事项](#注意事项)
|
||||
6. [完整示例](#完整示例)
|
||||
7. [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 功能介绍
|
||||
|
||||
### 什么是菜单数据权限?
|
||||
|
||||
菜单数据权限是一种**基于菜单的动态数据过滤机制**,允许管理员为不同的角色配置不同的数据查询规则,实现细粒度的数据权限控制。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- ✅ **动态配置**:无需修改代码,通过页面配置即可实现数据过滤
|
||||
- ✅ **基于角色**:不同角色可以看到不同的数据
|
||||
- ✅ **灵活规则**:支持多种条件(等于、大于、IN、LIKE等)
|
||||
- ✅ **变量支持**:支持动态变量(如当前用户ID、部门ID等)
|
||||
- ✅ **SQL级别**:在SQL层面过滤,性能高效
|
||||
|
||||
### 使用场景
|
||||
|
||||
1. **部门数据隔离**:用户只能查看自己部门的数据
|
||||
2. **创建人过滤**:用户只能查看自己创建的数据
|
||||
3. **状态过滤**:某些角色只能查看特定状态的数据
|
||||
4. **自定义规则**:根据业务需求配置任意过滤条件
|
||||
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 模块结构
|
||||
|
||||
```
|
||||
zt-framework/
|
||||
└── zt-spring-boot-starter-biz-data-permission/ # 框架模块
|
||||
└── menudatapermission/
|
||||
├── annotation/ # @PermissionData 注解
|
||||
├── aop/ # AOP 切面
|
||||
├── context/ # ThreadLocal 上下文
|
||||
├── handler/ # MyBatis 拦截器
|
||||
├── model/ # DTO 模型
|
||||
└── util/ # 工具类
|
||||
|
||||
zt-module-system/
|
||||
└── zt-module-system-server/ # 业务模块
|
||||
└── framework/permission/
|
||||
└── MenuDataRuleLoaderImpl.java # 规则加载器实现
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
1. 用户访问页面(如:角色管理)
|
||||
↓
|
||||
2. Controller 方法上有 @PermissionData 注解
|
||||
↓
|
||||
3. AOP 切面拦截,根据 pageComponent 查询菜单ID
|
||||
↓
|
||||
4. 加载该菜单下用户角色关联的数据规则
|
||||
↓
|
||||
5. 将规则存入 ThreadLocal
|
||||
↓
|
||||
6. MyBatis 执行查询时,拦截器读取规则
|
||||
↓
|
||||
7. 构建 SQL WHERE 条件并添加到查询中
|
||||
↓
|
||||
8. 返回过滤后的数据
|
||||
↓
|
||||
9. finally 块清理 ThreadLocal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
### ✅ 检查 Maven 依赖
|
||||
|
||||
在使用菜单数据权限功能前,**必须确保业务模块已引入框架依赖**。
|
||||
|
||||
#### 1. 检查依赖是否存在
|
||||
|
||||
打开业务模块的 `pom.xml` 文件(如 `zt-module-xxx-server/pom.xml`),检查是否包含以下依赖:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### 2. 如何检查?
|
||||
|
||||
**方法1:查看 pom.xml**
|
||||
```bash
|
||||
# 在项目根目录执行
|
||||
grep -r "zt-spring-boot-starter-biz-data-permission" zt-module-xxx/zt-module-xxx-server/pom.xml
|
||||
```
|
||||
|
||||
**方法2:在 IDE 中查看**
|
||||
- IDEA:打开 `pom.xml`,搜索 `zt-spring-boot-starter-biz-data-permission`
|
||||
- 或者查看 Maven 依赖树
|
||||
|
||||
#### 3. 如果没有依赖,如何添加?
|
||||
|
||||
在业务模块的 `pom.xml` 中添加:
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- 其他依赖 -->
|
||||
|
||||
<!-- 菜单数据权限框架 -->
|
||||
<dependency>
|
||||
<groupId>com.zt.plat</groupId>
|
||||
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
#### 4. 已包含该依赖的模块
|
||||
|
||||
以下模块已默认包含该依赖,可直接使用:
|
||||
- ✅ `zt-module-system-server`
|
||||
- ⚠️ 其他业务模块需自行检查
|
||||
|
||||
---
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 步骤1:在 Controller 方法上添加注解
|
||||
|
||||
在需要数据过滤的查询方法上添加 `@PermissionData` 注解:
|
||||
|
||||
```java
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/system/role")
|
||||
public class RoleController {
|
||||
|
||||
@GetMapping("/page")
|
||||
@PermissionData(pageComponent = "system/role/index") // 指定页面组件路径
|
||||
public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) {
|
||||
PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RoleRespVO.class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤2:确定 pageComponent 值
|
||||
|
||||
`pageComponent` 是前端页面组件的路径,用于关联菜单:
|
||||
|
||||
- **格式**:`模块名/功能名/页面名`
|
||||
- **示例**:
|
||||
- `system/role/index` - 角色管理页面
|
||||
- `system/user/index` - 用户管理页面
|
||||
- `system/dept/index` - 部门管理页面
|
||||
|
||||
**如何查找 pageComponent?**
|
||||
|
||||
1. 打开前端项目,找到对应的 Vue 文件路径
|
||||
2. 例如:`src/views/system/role/index.vue`
|
||||
3. pageComponent 就是:`system/role/index`
|
||||
|
||||
### 步骤3:在菜单表中配置 component 字段
|
||||
|
||||
确保数据库 `system_menu` 表中,该菜单的 `component` 字段值与 `pageComponent` 一致:
|
||||
|
||||
```sql
|
||||
-- 示例:角色管理菜单
|
||||
UPDATE system_menu
|
||||
SET component = 'system/role/index'
|
||||
WHERE id = 角色管理菜单ID;
|
||||
```
|
||||
|
||||
### 注解参数说明
|
||||
|
||||
```java
|
||||
@PermissionData(
|
||||
pageComponent = "system/role/index", // 必填:页面组件路径
|
||||
enable = true // 可选:是否启用,默认 true
|
||||
)
|
||||
```
|
||||
|
||||
**何时设置 `enable = false`?**
|
||||
|
||||
当某个方法不需要数据权限过滤时(如管理员查看所有数据):
|
||||
|
||||
```java
|
||||
@GetMapping("/all")
|
||||
@PermissionData(pageComponent = "system/role/index", enable = false)
|
||||
public CommonResult<List<RoleRespVO>> getAllRoles() {
|
||||
// 返回所有角色,不受数据权限限制
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置指南
|
||||
|
||||
### 步骤1:配置菜单
|
||||
|
||||
1. 登录系统,进入 **系统管理 > 菜单管理**
|
||||
2. 找到需要配置数据权限的菜单(如:角色管理)
|
||||
3. 确认该菜单的 **组件路径** 字段与代码中的 `pageComponent` 一致
|
||||
|
||||
### 步骤2:配置数据规则
|
||||
|
||||
1. 在菜单列表中,点击对应菜单的 **"数据规则"** 按钮
|
||||
2. 点击 **"新增规则"** 按钮
|
||||
3. 填写规则信息:
|
||||
|
||||
#### 规则字段说明
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **规则名称** | 规则的描述性名称 | `只看自己创建的角色` |
|
||||
| **规则字段** | 数据库表的字段名 | `creator`、`dept_id`、`status` |
|
||||
| **规则条件** | 比较条件 | `=`、`IN`、`LIKE` 等 |
|
||||
| **规则值** | 比较的值,支持变量 | `#{userId}`、`#{deptId}` |
|
||||
| **状态** | 启用/禁用 | 启用 |
|
||||
| **排序** | 规则执行顺序 | 0 |
|
||||
|
||||
#### 支持的规则条件
|
||||
|
||||
| 条件 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `=` | 等于 | `creator = #{userId}` |
|
||||
| `!=` | 不等于 | `status != 0` |
|
||||
| `>` | 大于 | `create_time > '2024-01-01'` |
|
||||
| `<` | 小于 | `sort < 100` |
|
||||
| `>=` | 大于等于 | `age >= 18` |
|
||||
| `<=` | 小于等于 | `price <= 1000` |
|
||||
| `IN` | 包含 | `dept_id IN (#{deptIds})` |
|
||||
| `NOT_IN` | 不包含 | `status NOT_IN (0,1)` |
|
||||
| `LIKE` | 模糊匹配 | `name LIKE '张'` |
|
||||
| `NOT_LIKE` | 不匹配 | `name NOT_LIKE '测试'` |
|
||||
| `IS_NULL` | 为空 | `deleted_time IS_NULL` |
|
||||
| `IS_NOT_NULL` | 不为空 | `phone IS_NOT_NULL` |
|
||||
| `BETWEEN` | 区间 | `age BETWEEN 18,60` |
|
||||
| `NOT_BETWEEN` | 不在区间 | `score NOT_BETWEEN 0,60` |
|
||||
| `SQL_RULE` | 自定义SQL | `(dept_id = 1 OR creator = 2)` |
|
||||
|
||||
#### 支持的变量
|
||||
|
||||
| 变量 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `#{userId}` | 当前用户ID | `creator = #{userId}` |
|
||||
| `#{username}` | 当前用户名 | `create_by = #{username}` |
|
||||
| `#{deptId}` | 当前部门ID | `dept_id = #{deptId}` |
|
||||
| `#{companyId}` | 当前公司ID | `company_id = #{companyId}` |
|
||||
| `#{tenantId}` | 当前租户ID | `tenant_id = #{tenantId}` |
|
||||
| `#{deptIds}` | 用户所有部门ID | `dept_id IN (#{deptIds})` |
|
||||
| `#{companyIds}` | 用户所有公司ID | `company_id IN (#{companyIds})` |
|
||||
| `#{postIds}` | 用户所有岗位ID | `post_id IN (#{postIds})` |
|
||||
|
||||
#### SQL_RULE 类型详解
|
||||
|
||||
`SQL_RULE` 是一种特殊的规则类型,允许你直接编写自定义 SQL 表达式,实现更复杂的数据过滤逻辑。
|
||||
|
||||
**什么时候使用 SQL_RULE?**
|
||||
|
||||
- ✅ 需要 `OR` 逻辑连接多个条件
|
||||
- ✅ 需要组合多个字段的复杂判断
|
||||
- ✅ 需要嵌套条件或括号分组
|
||||
- ✅ 标准规则条件无法满足业务需求
|
||||
|
||||
**配置方法**
|
||||
|
||||
| 字段 | 配置值 |
|
||||
|------|--------|
|
||||
| 规则条件 | 选择 `SQL_RULE` |
|
||||
| 规则值 | 直接填写 SQL WHERE 条件表达式 |
|
||||
| 规则字段 | 可以留空(不使用) |
|
||||
|
||||
**配置示例**
|
||||
|
||||
1. **OR 逻辑 - 查看自己创建的或自己部门的数据**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(creator = #{userId} OR dept_id = #{deptId})
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (creator = '123' OR dept_id = '456')
|
||||
```
|
||||
|
||||
2. **多字段组合 - 特定部门的已启用数据**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(dept_id IN (#{deptIds}) AND status = 1)
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (dept_id IN ('100', '101', '102') AND status = 1)
|
||||
```
|
||||
|
||||
3. **复杂嵌套条件 - 管理员或本部门负责人**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(role_type = 'admin' OR (dept_id = #{deptId} AND is_leader = 1))
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (role_type = 'admin' OR (dept_id = '456' AND is_leader = 1))
|
||||
```
|
||||
|
||||
4. **时间范围 + 状态过滤**
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(create_time >= '2024-01-01' AND status IN (1, 2))
|
||||
```
|
||||
|
||||
生成的 SQL:
|
||||
```sql
|
||||
WHERE (create_time >= '2024-01-01' AND status IN (1, 2))
|
||||
```
|
||||
|
||||
**工作原理**
|
||||
|
||||
当规则条件为 `SQL_RULE` 时:
|
||||
|
||||
1. 系统会忽略"规则字段"和"规则条件"
|
||||
2. 直接使用"规则值"中的 SQL 表达式
|
||||
3. 先替换表达式中的变量(如 `#{userId}`)
|
||||
4. 将替换后的表达式直接添加到 SQL WHERE 子句中
|
||||
|
||||
代码实现(MenuDataPermissionRule.java:64-67):
|
||||
```java
|
||||
// 处理 SQL_RULE 类型(自定义 SQL)
|
||||
if ("SQL_RULE".equals(ruleConditions)) {
|
||||
return actualValue; // 直接返回替换变量后的 SQL 表达式
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ 重要警告**
|
||||
|
||||
1. **SQL 注入风险**
|
||||
- SQL_RULE 直接拼接到 SQL 中,存在注入风险
|
||||
- ✅ **安全做法**:只使用预定义变量(`#{userId}` 等)
|
||||
- ❌ **危险做法**:不要在规则值中拼接用户输入的内容
|
||||
|
||||
2. **字段名必须正确**
|
||||
- SQL_RULE 中的字段名必须与数据库表字段完全一致
|
||||
- 错误的字段名会导致 SQL 查询报错
|
||||
- 建议先在数据库中测试 SQL 语句
|
||||
|
||||
3. **括号很重要**
|
||||
- 建议始终用括号包裹整个表达式:`(condition1 OR condition2)`
|
||||
- 避免与其他规则或系统条件产生优先级问题
|
||||
|
||||
4. **变量替换**
|
||||
- 变量会被替换为带引号的字符串值
|
||||
- 例如:`#{userId}` → `'123'`
|
||||
- 数据库会自动处理类型转换
|
||||
|
||||
**SQL_RULE vs 普通规则对比**
|
||||
|
||||
| 特性 | 普通规则 | SQL_RULE |
|
||||
|------|---------|----------|
|
||||
| 配置难度 | 简单,选择即可 | 需要 SQL 知识 |
|
||||
| 灵活性 | 有限,单一条件 | 非常灵活,任意表达式 |
|
||||
| 安全性 | 高,系统控制 | 需要注意 SQL 注入 |
|
||||
| OR 逻辑 | ❌ 不支持 | ✅ 支持 |
|
||||
| 嵌套条件 | ❌ 不支持 | ✅ 支持 |
|
||||
| 多规则组合 | AND 连接 | 单个规则内实现 |
|
||||
| 错误提示 | 友好 | SQL 错误信息 |
|
||||
|
||||
**最佳实践**
|
||||
|
||||
1. **优先使用普通规则**:能用普通规则解决的,不要用 SQL_RULE
|
||||
2. **测试后再上线**:在测试环境验证 SQL 语句正确性
|
||||
3. **添加注释**:在规则名称中说明 SQL_RULE 的用途
|
||||
4. **定期审查**:定期检查 SQL_RULE 规则,删除不再使用的
|
||||
5. **权限控制**:限制能配置 SQL_RULE 的管理员权限
|
||||
|
||||
### 步骤3:关联角色
|
||||
|
||||
1. 配置完规则后,进入 **系统管理 > 角色管理**
|
||||
2. 编辑需要应用规则的角色
|
||||
3. 在 **"数据权限"** 标签页中,勾选对应的菜单数据规则
|
||||
4. 保存角色配置
|
||||
|
||||
### 步骤4:测试验证
|
||||
|
||||
1. 使用该角色的用户登录系统
|
||||
2. 访问配置了数据规则的页面
|
||||
3. 验证数据是否按规则过滤
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### ⚠️ 重要提醒
|
||||
|
||||
#### 1. 规则字段必须与数据库表字段一致
|
||||
|
||||
**错误示例**:
|
||||
```
|
||||
规则字段:dept_id_xxx
|
||||
数据库字段:dept_id
|
||||
结果:SQL 查询报错!
|
||||
```
|
||||
|
||||
**正确做法**:
|
||||
- 在配置规则前,先确认数据库表结构
|
||||
- 字段名必须完全一致(包括大小写)
|
||||
- 前端已添加警告提示,请仔细阅读
|
||||
|
||||
#### 2. 只在菜单/页面级别配置规则
|
||||
|
||||
- ✅ **菜单/页面**(type=2):需要配置数据规则
|
||||
- ❌ **目录**(type=1):不需要配置
|
||||
- ❌ **按钮**(type=3):不需要配置
|
||||
|
||||
前端已自动隐藏目录和按钮的"数据规则"按钮。
|
||||
|
||||
#### 3. 多个规则使用 AND 连接
|
||||
|
||||
如果为同一个菜单配置了多个规则,它们会用 `AND` 连接:
|
||||
|
||||
```sql
|
||||
-- 规则1:creator = #{userId}
|
||||
-- 规则2:status = 1
|
||||
-- 最终SQL:
|
||||
WHERE creator = '当前用户ID' AND status = 1
|
||||
```
|
||||
|
||||
#### 4. 变量不存在时的处理
|
||||
|
||||
如果配置了不存在的变量(如 `#{unknownVar}`),系统会:
|
||||
- 记录警告日志
|
||||
- 将变量替换为空字符串
|
||||
- 可能导致查询结果为空
|
||||
|
||||
**建议**:使用前端下拉框选择变量,避免手动输入错误。
|
||||
|
||||
#### 5. 性能考虑
|
||||
|
||||
- 数据规则在 SQL 层面过滤,性能较好
|
||||
- 但过多的规则会增加 SQL 复杂度
|
||||
- 建议每个菜单不超过 5 条规则
|
||||
|
||||
#### 6. 禁用数据权限的场景
|
||||
|
||||
某些查询不应该受数据权限限制,需要添加 `@DataPermission(enable = false)`:
|
||||
|
||||
```java
|
||||
// 示例:查询所有根级部门(不受数据权限限制)
|
||||
@Override
|
||||
@DataPermission(enable = false)
|
||||
public List<DeptDO> getTopLevelDeptList() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 场景:角色管理 - 只看自己创建的角色
|
||||
|
||||
#### 1. 后端代码
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/system/role")
|
||||
public class RoleController {
|
||||
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得角色分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:role:query')")
|
||||
@PermissionData(pageComponent = "system/role/index") // 添加数据权限注解
|
||||
public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) {
|
||||
PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, RoleRespVO.class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 菜单配置
|
||||
|
||||
确保 `system_menu` 表中角色管理菜单的配置:
|
||||
|
||||
```sql
|
||||
SELECT id, name, component
|
||||
FROM system_menu
|
||||
WHERE name = '角色管理';
|
||||
|
||||
-- 结果:
|
||||
-- id: 101
|
||||
-- name: 角色管理
|
||||
-- component: system/role/index ✅ 与代码中的 pageComponent 一致
|
||||
```
|
||||
|
||||
#### 3. 数据规则配置
|
||||
|
||||
在页面上配置规则:
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|------|
|
||||
| 规则名称 | 只看自己创建的角色 |
|
||||
| 规则字段 | `creator` |
|
||||
| 规则条件 | `=` |
|
||||
| 规则值 | `#{userId}` |
|
||||
| 状态 | 启用 |
|
||||
| 排序 | 0 |
|
||||
|
||||
#### 4. 角色关联
|
||||
|
||||
1. 进入角色管理,编辑"普通用户"角色
|
||||
2. 在"数据权限"标签页,勾选"只看自己创建的角色"规则
|
||||
3. 保存
|
||||
|
||||
#### 5. 生成的 SQL
|
||||
|
||||
当普通用户(ID=123)查询角色列表时,实际执行的 SQL:
|
||||
|
||||
```sql
|
||||
SELECT * FROM system_role
|
||||
WHERE deleted = 0
|
||||
AND tenant_id = 1
|
||||
AND creator = '123' -- 自动添加的数据权限条件
|
||||
ORDER BY sort ASC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么配置了规则但不生效?
|
||||
|
||||
**可能原因**:
|
||||
|
||||
1. ✅ 检查 Controller 方法是否添加了 `@PermissionData` 注解
|
||||
2. ✅ 检查 `pageComponent` 是否与菜单的 `component` 字段一致
|
||||
3. ✅ 检查规则是否启用(状态=启用)
|
||||
4. ✅ 检查角色是否关联了该规则
|
||||
5. ✅ 检查用户是否拥有该角色
|
||||
|
||||
### Q2: 如何查看实际执行的 SQL?
|
||||
|
||||
在 `application.yaml` 中开启 SQL 日志:
|
||||
|
||||
```yaml
|
||||
# 方式1:MyBatis Plus SQL 日志
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# 方式2:Logback 日志
|
||||
logging:
|
||||
level:
|
||||
com.zt.plat.module.system.dal.mysql: debug
|
||||
```
|
||||
|
||||
### Q3: 多个规则如何组合?
|
||||
|
||||
多个规则使用 `AND` 连接,如果需要 `OR` 逻辑,使用 `SQL_RULE` 类型:
|
||||
|
||||
```
|
||||
规则条件:SQL_RULE
|
||||
规则值:(dept_id = #{deptId} OR creator = #{userId})
|
||||
```
|
||||
|
||||
### Q4: 如何配置"查看本部门及下级部门"的规则?
|
||||
|
||||
这需要在业务层实现,菜单数据权限只支持简单的字段过滤。建议使用原有的部门数据权限功能。
|
||||
|
||||
### Q5: 规则字段配置错误会怎样?
|
||||
|
||||
会导致 SQL 查询报错,因为数据库找不到该字段。前端已添加警告提示,请仔细检查。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
菜单数据权限提供了一种灵活、高效的数据过滤机制:
|
||||
|
||||
✅ **开发简单**:只需添加一个注解
|
||||
✅ **配置灵活**:通过页面配置,无需修改代码
|
||||
✅ **性能高效**:SQL 层面过滤,不影响性能
|
||||
✅ **易于维护**:规则集中管理,便于调整
|
||||
|
||||
**最佳实践**:
|
||||
1. 优先使用预定义变量,避免手动输入
|
||||
2. 规则字段必须与数据库表字段一致
|
||||
3. 合理使用规则,避免过度复杂
|
||||
4. 定期review规则配置,删除无用规则
|
||||
|
||||
---
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请联系:
|
||||
- 开发团队:ZT
|
||||
- 文档版本:v1.0
|
||||
- 更新日期:2026-01-27
|
||||
73
sql/dm/20260126菜单数据规则表.sql
Normal file
73
sql/dm/20260126菜单数据规则表.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- ----------------------------
|
||||
-- Table structure for system_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
menu_id bigint NOT NULL,
|
||||
rule_name varchar(100) NOT NULL,
|
||||
rule_column varchar(100) DEFAULT NULL NULL,
|
||||
rule_conditions varchar(20) NOT NULL,
|
||||
rule_value varchar(500) NOT NULL,
|
||||
status smallint DEFAULT 1 NOT NULL,
|
||||
sort int DEFAULT 0 NOT NULL,
|
||||
remark varchar(500) DEFAULT NULL NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
-- CREATE INDEX idx_menu_data_rule_menu ON system_menu_data_rule (menu_id);
|
||||
-- CREATE INDEX idx_menu_data_rule_tenant ON system_menu_data_rule (tenant_id);
|
||||
|
||||
COMMENT ON COLUMN system_menu_data_rule.id IS '规则ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_name IS '规则名称';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_column IS '规则字段(数据库列名)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_conditions IS '规则条件(=、>、<、IN、LIKE等)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_value IS '规则值(支持变量如#{userId}、#{deptId})';
|
||||
COMMENT ON COLUMN system_menu_data_rule.status IS '状态(0=禁用 1=启用)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.sort IS '排序';
|
||||
COMMENT ON COLUMN system_menu_data_rule.remark IS '备注';
|
||||
COMMENT ON COLUMN system_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_menu_data_rule IS '菜单数据规则表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for system_role_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_role_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
role_id bigint NOT NULL,
|
||||
menu_id bigint NOT NULL,
|
||||
data_rule_id bigint NOT NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
-- CREATE INDEX idx_rmdr_role ON system_role_menu_data_rule (role_id);
|
||||
-- CREATE INDEX idx_rmdr_menu ON system_role_menu_data_rule (menu_id);
|
||||
-- CREATE INDEX idx_rmdr_tenant ON system_role_menu_data_rule (tenant_id);
|
||||
-- CREATE INDEX idx_rmdr_role_menu_rule ON system_role_menu_data_rule (role_id, menu_id, data_rule_id);
|
||||
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.id IS '自增主键';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.role_id IS '角色ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.data_rule_id IS '数据规则ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_role_menu_data_rule IS '角色菜单数据规则关联表';
|
||||
@@ -4313,3 +4313,74 @@ VALUES
|
||||
(5022, 2, '日期格式', 'DATE', 'system_sequence_detail_rule_type', 0, 'success', '', '日期格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
||||
(5023, 3, '数字格式', 'NUMBER', 'system_sequence_detail_rule_type', 0, 'info', '', '数字格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
||||
(5024, 4, '自定义格式', 'CUSTOM', 'system_sequence_detail_rule_type', 0, 'warning', '', '自定义格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0);
|
||||
|
||||
/*
|
||||
增加菜单规则(system_menu_data_rule)和规则角色关联表(system_role_menu_data_rule),同增量脚本:sql/dm/20260126菜单数据规则表.sql
|
||||
*/
|
||||
-- ----------------------------
|
||||
-- Table structure for system_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
menu_id bigint NOT NULL,
|
||||
rule_name varchar(100) NOT NULL,
|
||||
rule_column varchar(100) DEFAULT NULL NULL,
|
||||
rule_conditions varchar(20) NOT NULL,
|
||||
rule_value varchar(500) NOT NULL,
|
||||
status smallint DEFAULT 1 NOT NULL,
|
||||
sort int DEFAULT 0 NOT NULL,
|
||||
remark varchar(500) DEFAULT NULL NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
COMMENT ON COLUMN system_menu_data_rule.id IS '规则ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_name IS '规则名称';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_column IS '规则字段(数据库列名)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_conditions IS '规则条件(=、>、<、IN、LIKE等)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.rule_value IS '规则值(支持变量如#{userId}、#{deptId})';
|
||||
COMMENT ON COLUMN system_menu_data_rule.status IS '状态(0=禁用 1=启用)';
|
||||
COMMENT ON COLUMN system_menu_data_rule.sort IS '排序';
|
||||
COMMENT ON COLUMN system_menu_data_rule.remark IS '备注';
|
||||
COMMENT ON COLUMN system_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_menu_data_rule IS '菜单数据规则表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for system_role_menu_data_rule
|
||||
-- ----------------------------
|
||||
CREATE TABLE system_role_menu_data_rule (
|
||||
id bigint NOT NULL PRIMARY KEY,
|
||||
role_id bigint NOT NULL,
|
||||
menu_id bigint NOT NULL,
|
||||
data_rule_id bigint NOT NULL,
|
||||
creator varchar(64) DEFAULT '' NULL,
|
||||
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updater varchar(64) DEFAULT '' NULL,
|
||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted bit DEFAULT '0' NOT NULL,
|
||||
tenant_id bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.id IS '自增主键';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.role_id IS '角色ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.menu_id IS '菜单ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.data_rule_id IS '数据规则ID';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.creator IS '创建者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.create_time IS '创建时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.updater IS '更新者';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.update_time IS '更新时间';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.deleted IS '是否删除';
|
||||
COMMENT ON COLUMN system_role_menu_data_rule.tenant_id IS '租户编号';
|
||||
COMMENT ON TABLE system_role_menu_data_rule IS '角色菜单数据规则关联表';
|
||||
|
||||
@@ -4,12 +4,15 @@ import com.zt.plat.framework.datapermission.core.aop.CompanyDataPermissionIgnore
|
||||
import com.zt.plat.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
|
||||
import com.zt.plat.framework.datapermission.core.aop.DeptDataPermissionIgnoreAspect;
|
||||
import com.zt.plat.framework.datapermission.core.db.DataPermissionRuleHandler;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.aop.MenuDataPermissionAspect;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.handler.MenuDataPermissionHandler;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
|
||||
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@@ -21,6 +24,7 @@ import java.util.List;
|
||||
* @author ZT
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@MapperScan("com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper")
|
||||
public class ZtDataPermissionAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -40,6 +44,21 @@ public class ZtDataPermissionAutoConfiguration {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
||||
// 创建菜单数据权限处理器
|
||||
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
||||
DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
|
||||
// 添加到 interceptor 中,放在部门数据权限之后
|
||||
MyBatisUtils.addInterceptor(interceptor, inner, 1);
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MenuDataPermissionAspect menuDataPermissionAspect() {
|
||||
return new MenuDataPermissionAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
|
||||
return new DataPermissionAnnotationAdvisor();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限注解
|
||||
* 标注在Controller方法上,表示该方法需要应用菜单数据规则
|
||||
*
|
||||
* 参考JeecgBoot的实现方式
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface PermissionData {
|
||||
|
||||
/**
|
||||
* 页面组件路径
|
||||
* 用于匹配菜单表中的component字段
|
||||
* 例如:system/role 对应角色管理菜单
|
||||
*/
|
||||
String pageComponent();
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
boolean enable() default true;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.aop;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.context.MenuDataRuleContextHolder;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.service.MenuDataRuleLoader;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据权限切面
|
||||
* 拦截 @PermissionData 注解的方法,加载并应用菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MenuDataPermissionAspect {
|
||||
|
||||
@Resource
|
||||
private MenuDataRuleLoader menuDataRuleLoader;
|
||||
|
||||
@Around("@annotation(com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData)")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
// 获取方法签名
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
PermissionData annotation = method.getAnnotation(PermissionData.class);
|
||||
|
||||
// 如果未启用,直接执行
|
||||
if (!annotation.enable()) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (userId == null) {
|
||||
log.debug("[MenuDataPermissionAspect][未登录,跳过菜单数据权限]");
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
// 从注解获取pageComponent
|
||||
String pageComponent = annotation.pageComponent();
|
||||
if (pageComponent == null || pageComponent.isEmpty()) {
|
||||
log.warn("[MenuDataPermissionAspect][未指定pageComponent,跳过菜单数据权限]");
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
// 根据pageComponent查询菜单ID
|
||||
Long menuId = menuDataRuleLoader.getMenuIdByPageComponent(pageComponent);
|
||||
if (menuId == null) {
|
||||
log.warn("[MenuDataPermissionAspect][未找到匹配的菜单: {},跳过菜单数据权限]", pageComponent);
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
log.debug("[MenuDataPermissionAspect][pageComponent: {} 对应菜单ID: {}]", pageComponent, menuId);
|
||||
|
||||
// 加载用户的菜单数据规则
|
||||
List<MenuDataRuleDTO> rules = menuDataRuleLoader.getUserMenuDataRules(userId, menuId);
|
||||
|
||||
if (CollUtil.isEmpty(rules)) {
|
||||
log.debug("[MenuDataPermissionAspect][用户 {} 在菜单 {} 下无数据规则]", userId, menuId);
|
||||
} else {
|
||||
log.debug("[MenuDataPermissionAspect][用户 {} 在菜单 {} 下加载了 {} 条数据规则]",
|
||||
userId, menuId, rules.size());
|
||||
// 将规则存入 ThreadLocal
|
||||
MenuDataRuleContextHolder.setRules(rules);
|
||||
}
|
||||
|
||||
// 执行目标方法
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
// 清理 ThreadLocal
|
||||
MenuDataRuleContextHolder.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.config;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.handler.MenuDataPermissionHandler;
|
||||
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 菜单数据权限配置类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan("com.zt.plat.framework.datapermission.core.menudatapermission")
|
||||
public class MenuDataPermissionConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(MybatisPlusInterceptor.class)
|
||||
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
||||
// 创建菜单数据权限处理器
|
||||
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
||||
|
||||
// 创建 DataPermissionInterceptor 拦截器
|
||||
DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
|
||||
|
||||
// 添加到 interceptor 中
|
||||
// 添加在索引1的位置,在部门数据权限之后,但在分页插件之前
|
||||
MyBatisUtils.addInterceptor(interceptor, inner, 1);
|
||||
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.context;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则上下文持有者
|
||||
* 使用 ThreadLocal 存储当前请求的菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class MenuDataRuleContextHolder {
|
||||
|
||||
private static final ThreadLocal<List<MenuDataRuleDTO>> CONTEXT = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前请求的菜单数据规则
|
||||
*
|
||||
* @param rules 规则列表
|
||||
*/
|
||||
public static void setRules(List<MenuDataRuleDTO> rules) {
|
||||
CONTEXT.set(rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的菜单数据规则
|
||||
*
|
||||
* @return 规则列表
|
||||
*/
|
||||
public static List<MenuDataRuleDTO> getRules() {
|
||||
return CONTEXT.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前请求的菜单数据规则
|
||||
*/
|
||||
public static void clear() {
|
||||
CONTEXT.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据权限 Mapper
|
||||
* 用于查询菜单和菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Mapper
|
||||
public interface MenuDataPermissionMapper {
|
||||
|
||||
/**
|
||||
* 根据页面组件路径获取菜单ID
|
||||
*
|
||||
* @param component 页面组件路径,如:system/role/index
|
||||
* @return 菜单ID,如果未找到返回null
|
||||
*/
|
||||
@Select("SELECT id FROM system_menu WHERE component = #{component} AND deleted = 0 LIMIT 1")
|
||||
Long selectMenuIdByComponent(@Param("component") String component);
|
||||
|
||||
/**
|
||||
* 获取用户在指定菜单下的有效数据规则
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param menuId 菜单ID
|
||||
* @return 数据规则列表
|
||||
*/
|
||||
@TenantIgnore
|
||||
@Select("<script>" +
|
||||
"SELECT mdr.rule_column, mdr.rule_conditions, mdr.rule_value, mdr.status " +
|
||||
"FROM system_menu_data_rule mdr " +
|
||||
"INNER JOIN system_role_menu_data_rule rmdr ON mdr.id = rmdr.data_rule_id " +
|
||||
"INNER JOIN system_user_role ur ON rmdr.role_id = ur.role_id " +
|
||||
"WHERE mdr.menu_id = #{menuId} " +
|
||||
"AND ur.user_id = #{userId} " +
|
||||
"AND mdr.status = 1 " +
|
||||
"AND mdr.deleted = 0 " +
|
||||
"AND rmdr.deleted = 0 " +
|
||||
"AND ur.deleted = 0" +
|
||||
"</script>")
|
||||
List<MenuDataRuleDTO> selectUserMenuDataRules(@Param("userId") Long userId,
|
||||
@Param("menuId") Long menuId);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.util.MenuDataPermissionRule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
|
||||
/**
|
||||
* 菜单数据权限处理器
|
||||
* 基于 MyBatis Plus 的数据权限插件,应用菜单数据规则到 SQL 查询
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Slf4j
|
||||
public class MenuDataPermissionHandler implements MultiDataPermissionHandler {
|
||||
|
||||
@Override
|
||||
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
|
||||
try {
|
||||
// 从 ThreadLocal 获取菜单数据规则并构建 SQL 条件
|
||||
String sqlCondition = MenuDataPermissionRule.buildSqlCondition();
|
||||
|
||||
if (StrUtil.isBlank(sqlCondition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将 SQL 字符串解析为 Expression 对象
|
||||
Expression expression = CCJSqlParserUtil.parseCondExpression(sqlCondition);
|
||||
log.debug("[MenuDataPermissionHandler][表: {}, 添加条件: {}]", table.getName(), sqlCondition);
|
||||
return expression;
|
||||
|
||||
} catch (JSQLParserException e) {
|
||||
log.error("[MenuDataPermissionHandler][解析 SQL 条件失败]", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 DTO
|
||||
* 用于在框架层传递菜单数据规则信息,避免依赖业务模块的数据库实体
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Data
|
||||
public class MenuDataRuleDTO {
|
||||
|
||||
/**
|
||||
* 规则字段(数据库列名)
|
||||
*/
|
||||
private String ruleColumn;
|
||||
|
||||
/**
|
||||
* 规则条件(=、>、<、IN、LIKE等)
|
||||
*/
|
||||
private String ruleConditions;
|
||||
|
||||
/**
|
||||
* 规则值(支持变量如#{userId}、#{deptId})
|
||||
*/
|
||||
private String ruleValue;
|
||||
|
||||
/**
|
||||
* 状态(0=禁用 1=启用)
|
||||
*/
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.service;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则加载器接口
|
||||
* 负责加载菜单和菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public interface MenuDataRuleLoader {
|
||||
|
||||
/**
|
||||
* 根据页面组件路径获取菜单ID
|
||||
*
|
||||
* @param pageComponent 页面组件路径,如:system/role/index
|
||||
* @return 菜单ID,如果未找到返回null
|
||||
*/
|
||||
Long getMenuIdByPageComponent(String pageComponent);
|
||||
|
||||
/**
|
||||
* 获取用户在指定菜单下的数据规则
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param menuId 菜单ID
|
||||
* @return 数据规则列表
|
||||
*/
|
||||
List<MenuDataRuleDTO> getUserMenuDataRules(Long userId, Long menuId);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.service.impl;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper.MenuDataPermissionMapper;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.service.MenuDataRuleLoader;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则加载器默认实现
|
||||
* 直接从数据库加载菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MenuDataRuleLoaderImpl implements MenuDataRuleLoader {
|
||||
|
||||
@Resource
|
||||
private MenuDataPermissionMapper menuDataPermissionMapper;
|
||||
|
||||
@Override
|
||||
public Long getMenuIdByPageComponent(String pageComponent) {
|
||||
try {
|
||||
return menuDataPermissionMapper.selectMenuIdByComponent(pageComponent);
|
||||
} catch (Exception e) {
|
||||
log.error("[MenuDataRuleLoaderImpl][根据pageComponent查询菜单ID失败: {}]", pageComponent, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MenuDataRuleDTO> getUserMenuDataRules(Long userId, Long menuId) {
|
||||
try {
|
||||
return menuDataPermissionMapper.selectUserMenuDataRules(userId, menuId);
|
||||
} catch (Exception e) {
|
||||
log.error("[MenuDataRuleLoaderImpl][查询用户菜单数据规则失败: userId={}, menuId={}]",
|
||||
userId, menuId, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.security.core.LoginUser;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 数据规则变量替换工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Slf4j
|
||||
public class DataRuleVariableUtils {
|
||||
|
||||
private static final Pattern VARIABLE_PATTERN = Pattern.compile("#\\{([^}]+)}");
|
||||
|
||||
/**
|
||||
* 替换规则值中的变量
|
||||
*
|
||||
* @param ruleValue 规则值,如 "#{userId}" 或 "#{deptId}"
|
||||
* @return 替换后的值
|
||||
*/
|
||||
public static String replaceVariables(String ruleValue) {
|
||||
if (StrUtil.isBlank(ruleValue)) {
|
||||
return ruleValue;
|
||||
}
|
||||
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return ruleValue;
|
||||
}
|
||||
|
||||
Matcher matcher = VARIABLE_PATTERN.matcher(ruleValue);
|
||||
StringBuffer result = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String variable = matcher.group(1);
|
||||
String replacement = getVariableValue(variable, loginUser);
|
||||
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变量对应的值
|
||||
*
|
||||
* @param variable 变量名,如 "userId", "deptId"
|
||||
* @param loginUser 当前登录用户
|
||||
* @return 变量值
|
||||
*/
|
||||
private static String getVariableValue(String variable, LoginUser loginUser) {
|
||||
if (loginUser == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (variable) {
|
||||
case "userId":
|
||||
return loginUser.getId() != null ? loginUser.getId().toString() : "";
|
||||
case "username":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_USERNAME, "") : "";
|
||||
case "deptId":
|
||||
return loginUser.getVisitDeptId() != null ?
|
||||
loginUser.getVisitDeptId().toString() : "";
|
||||
case "companyId":
|
||||
return loginUser.getVisitCompanyId() != null ?
|
||||
loginUser.getVisitCompanyId().toString() : "";
|
||||
case "tenantId":
|
||||
return loginUser.getTenantId() != null ?
|
||||
loginUser.getTenantId().toString() : "";
|
||||
case "deptIds":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_DEPT_IDS, "") : "";
|
||||
case "companyIds":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_COMPANY_IDS, "") : "";
|
||||
case "postIds":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_POST_IDS, "") : "";
|
||||
default:
|
||||
// 未知变量,记录警告并返回空字符串
|
||||
log.warn("[DataRuleVariableUtils][未知的变量: {},请检查数据规则配置]", variable);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.context.MenuDataRuleContextHolder;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 菜单数据权限规则
|
||||
* 用于构建 SQL WHERE 条件
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Slf4j
|
||||
public class MenuDataPermissionRule {
|
||||
|
||||
/**
|
||||
* 构建 SQL WHERE 条件
|
||||
*
|
||||
* @return SQL 条件字符串,如 "dept_id = 1 AND status = 1"
|
||||
*/
|
||||
public static String buildSqlCondition() {
|
||||
List<MenuDataRuleDTO> rules = MenuDataRuleContextHolder.getRules();
|
||||
if (CollUtil.isEmpty(rules)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> conditions = rules.stream()
|
||||
.filter(rule -> rule.getStatus() == 1) // 只处理启用的规则
|
||||
.map(MenuDataPermissionRule::buildSingleCondition)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (CollUtil.isEmpty(conditions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 多个规则用 AND 连接
|
||||
return "(" + String.join(" AND ", conditions) + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建单个规则的 SQL 条件
|
||||
*
|
||||
* @param rule 规则
|
||||
* @return SQL 条件字符串
|
||||
*/
|
||||
private static String buildSingleCondition(MenuDataRuleDTO rule) {
|
||||
String ruleColumn = rule.getRuleColumn();
|
||||
String ruleConditions = rule.getRuleConditions();
|
||||
String ruleValue = rule.getRuleValue();
|
||||
|
||||
// 替换变量
|
||||
String actualValue = DataRuleVariableUtils.replaceVariables(ruleValue);
|
||||
|
||||
if (StrUtil.isBlank(ruleColumn) || StrUtil.isBlank(ruleConditions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理 SQL_RULE 类型(自定义 SQL)
|
||||
if ("SQL_RULE".equals(ruleConditions)) {
|
||||
return actualValue;
|
||||
}
|
||||
|
||||
// 处理 IS_NULL 和 IS_NOT_NULL
|
||||
if ("IS_NULL".equals(ruleConditions)) {
|
||||
return ruleColumn + " IS NULL";
|
||||
}
|
||||
if ("IS_NOT_NULL".equals(ruleConditions)) {
|
||||
return ruleColumn + " IS NOT NULL";
|
||||
}
|
||||
|
||||
// 其他条件需要有值
|
||||
if (StrUtil.isBlank(actualValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建条件
|
||||
switch (ruleConditions) {
|
||||
case "=":
|
||||
return ruleColumn + " = " + formatValue(actualValue);
|
||||
case "!=":
|
||||
return ruleColumn + " != " + formatValue(actualValue);
|
||||
case ">":
|
||||
return ruleColumn + " > " + formatValue(actualValue);
|
||||
case "<":
|
||||
return ruleColumn + " < " + formatValue(actualValue);
|
||||
case ">=":
|
||||
return ruleColumn + " >= " + formatValue(actualValue);
|
||||
case "<=":
|
||||
return ruleColumn + " <= " + formatValue(actualValue);
|
||||
case "IN":
|
||||
return ruleColumn + " IN (" + formatInValues(actualValue) + ")";
|
||||
case "NOT_IN":
|
||||
return ruleColumn + " NOT IN (" + formatInValues(actualValue) + ")";
|
||||
case "LIKE":
|
||||
return ruleColumn + " LIKE '%" + escapeSql(actualValue) + "%'";
|
||||
case "NOT_LIKE":
|
||||
return ruleColumn + " NOT LIKE '%" + escapeSql(actualValue) + "%'";
|
||||
case "BETWEEN":
|
||||
return buildBetweenCondition(ruleColumn, actualValue);
|
||||
case "NOT_BETWEEN":
|
||||
return "NOT " + buildBetweenCondition(ruleColumn, actualValue);
|
||||
default:
|
||||
log.warn("[buildSingleCondition][未知的规则条件: {}]", ruleConditions);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化值(添加引号)
|
||||
* 统一给所有值加引号,数据库会自动处理类型转换
|
||||
*/
|
||||
private static String formatValue(String value) {
|
||||
// 统一添加单引号,避免达梦数据库等对字符串类型字段的类型转换错误
|
||||
return "'" + escapeSql(value) + "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 IN 条件的值
|
||||
*/
|
||||
private static String formatInValues(String value) {
|
||||
String[] values = value.split(",");
|
||||
return java.util.Arrays.stream(values)
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(MenuDataPermissionRule::formatValue)
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 BETWEEN 条件
|
||||
*/
|
||||
private static String buildBetweenCondition(String column, String value) {
|
||||
String[] values = value.split(",");
|
||||
if (values.length != 2) {
|
||||
log.warn("[buildBetweenCondition][BETWEEN 条件需要两个值,用逗号分隔: {}]", value);
|
||||
return null;
|
||||
}
|
||||
return column + " BETWEEN " + formatValue(values[0].trim()) + " AND " + formatValue(values[1].trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 转义,防止 SQL 注入
|
||||
*/
|
||||
private static String escapeSql(String value) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
return value.replace("'", "''");
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@ com.zt.plat.framework.datapermission.config.ZtDataPermissionAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.config.ZtDeptDataPermissionAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.config.ZtBusinessDataPermissionAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.config.ZtDataPermissionRpcAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.core.menudatapermission.config.MenuDataPermissionConfiguration
|
||||
|
||||
@@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
@@ -45,13 +47,31 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor {
|
||||
}
|
||||
|
||||
Long deptId = WebFrameworkUtils.getDeptId(request);
|
||||
// 部门信息同样遵循“请求头 -> 请求属性 -> 登录缓存”的回退顺序
|
||||
// 部门信息同样遵循"请求头 -> 请求属性 -> 登录缓存"的回退顺序
|
||||
if (deptId == null || deptId <= 0L) {
|
||||
Long attrDeptId = resolveLong(request.getAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_ID));
|
||||
if (attrDeptId != null && attrDeptId > 0L) {
|
||||
deptId = attrDeptId;
|
||||
} else if (loginUser != null && loginUser.getVisitDeptId() != null && loginUser.getVisitDeptId() > 0L) {
|
||||
deptId = loginUser.getVisitDeptId();
|
||||
} else if (loginUser != null) {
|
||||
// 如果以上都没有,尝试从用户info中获取第一个部门作为默认值
|
||||
Map<String, String> info = loginUser.getInfo();
|
||||
if (info != null) {
|
||||
String deptIdsStr = info.get(LoginUser.INFO_KEY_DEPT_IDS);
|
||||
if (deptIdsStr != null && !deptIdsStr.isEmpty() && !"[]".equals(deptIdsStr)) {
|
||||
try {
|
||||
// 解析JSON数组,取第一个部门ID
|
||||
deptIdsStr = deptIdsStr.trim();
|
||||
if (deptIdsStr.startsWith("[") && deptIdsStr.length() > 2) {
|
||||
String firstId = deptIdsStr.substring(1, deptIdsStr.indexOf(']')).split(",")[0].trim();
|
||||
deptId = Long.parseLong(firstId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[CompanyVisitContextInterceptor][解析用户默认部门失败: {}]", deptIdsStr, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.zt.plat.module.system.enums.permission;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据规则条件枚举
|
||||
*
|
||||
* 用于菜单数据规则的条件类型
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataRuleConditionEnum {
|
||||
|
||||
EQ("=", "等于"),
|
||||
NE("!=", "不等于"),
|
||||
GT(">", "大于"),
|
||||
GE(">=", "大于等于"),
|
||||
LT("<", "小于"),
|
||||
LE("<=", "小于等于"),
|
||||
IN("IN", "包含"),
|
||||
NOT_IN("NOT_IN", "不包含"),
|
||||
LIKE("LIKE", "模糊匹配"),
|
||||
LEFT_LIKE("LEFT_LIKE", "左模糊"),
|
||||
RIGHT_LIKE("RIGHT_LIKE", "右模糊"),
|
||||
NOT_LIKE("NOT_LIKE", "不匹配"),
|
||||
IS_NULL("IS_NULL", "为空"),
|
||||
IS_NOT_NULL("IS_NOT_NULL", "不为空"),
|
||||
SQL_RULE("SQL_RULE", "自定义SQL");
|
||||
|
||||
/**
|
||||
* 条件符号
|
||||
*/
|
||||
private final String condition;
|
||||
|
||||
/**
|
||||
* 条件描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据条件符号查找枚举
|
||||
*
|
||||
* @param condition 条件符号
|
||||
* @return 枚举值
|
||||
*/
|
||||
public static DataRuleConditionEnum findByCondition(String condition) {
|
||||
if (condition == null) {
|
||||
return null;
|
||||
}
|
||||
for (DataRuleConditionEnum value : values()) {
|
||||
if (value.condition.equals(condition)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.zt.plat.module.system.enums.permission;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据规则变量枚举
|
||||
*
|
||||
* 用于菜单数据规则的变量替换
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataRuleVariableEnum {
|
||||
|
||||
USER_ID("#{userId}", "当前用户ID"),
|
||||
USERNAME("#{username}", "当前用户名"),
|
||||
DEPT_ID("#{deptId}", "当前用户部门ID"),
|
||||
DEPT_IDS("#{deptIds}", "当前用户所有部门ID"),
|
||||
ORG_CODE("#{orgCode}", "当前用户组织编码"),
|
||||
TENANT_ID("#{tenantId}", "当前租户ID"),
|
||||
CURRENT_DATE("#{currentDate}", "当前日期"),
|
||||
CURRENT_TIME("#{currentTime}", "当前时间");
|
||||
|
||||
/**
|
||||
* 变量名
|
||||
*/
|
||||
private final String variable;
|
||||
|
||||
/**
|
||||
* 变量描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 根据变量名查找枚举
|
||||
*
|
||||
* @param variable 变量名
|
||||
* @return 枚举值
|
||||
*/
|
||||
public static DataRuleVariableEnum findByVariable(String variable) {
|
||||
if (variable == null) {
|
||||
return null;
|
||||
}
|
||||
for (DataRuleVariableEnum value : values()) {
|
||||
if (value.variable.equals(variable)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.zt.plat.module.system.controller.admin.permission;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleRespVO;
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
|
||||
import com.zt.plat.module.system.convert.permission.MenuDataRuleConvert;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
|
||||
import com.zt.plat.module.system.service.permission.MenuDataRuleService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.zt.plat.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 Controller
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Tag(name = "管理后台 - 菜单数据规则")
|
||||
@RestController
|
||||
@RequestMapping("/system/menu-data-rule")
|
||||
@Validated
|
||||
public class MenuDataRuleController {
|
||||
|
||||
@Resource
|
||||
private MenuDataRuleService menuDataRuleService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建菜单数据规则")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu:update')")
|
||||
public CommonResult<Long> createMenuDataRule(@Valid @RequestBody MenuDataRuleSaveReqVO createReqVO) {
|
||||
return success(menuDataRuleService.createMenuDataRule(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新菜单数据规则")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu:update')")
|
||||
public CommonResult<Boolean> updateMenuDataRule(@Valid @RequestBody MenuDataRuleSaveReqVO updateReqVO) {
|
||||
menuDataRuleService.updateMenuDataRule(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除菜单数据规则")
|
||||
@Parameter(name = "id", description = "规则ID", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu:update')")
|
||||
public CommonResult<Boolean> deleteMenuDataRule(@RequestParam("id") Long id) {
|
||||
menuDataRuleService.deleteMenuDataRule(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得菜单数据规则")
|
||||
@Parameter(name = "id", description = "规则ID", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu:query')")
|
||||
public CommonResult<MenuDataRuleRespVO> getMenuDataRule(@RequestParam("id") Long id) {
|
||||
MenuDataRuleDO rule = menuDataRuleService.getMenuDataRule(id);
|
||||
return success(MenuDataRuleConvert.INSTANCE.convert(rule));
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获得菜单的所有数据规则")
|
||||
@Parameter(name = "menuId", description = "菜单ID", required = true, example = "1")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu:query')")
|
||||
public CommonResult<List<MenuDataRuleRespVO>> getMenuDataRuleList(@RequestParam("menuId") Long menuId) {
|
||||
List<MenuDataRuleDO> list = menuDataRuleService.getMenuDataRuleListByMenuId(menuId);
|
||||
return success(MenuDataRuleConvert.INSTANCE.convertList(list));
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,9 @@ public class PermissionController {
|
||||
PermissionAssignRoleMenuItemReqVO reqVO = new PermissionAssignRoleMenuItemReqVO();
|
||||
reqVO.setId(menu.getMenuId());
|
||||
reqVO.setShowMenu(menu.getShowMenu());
|
||||
// 获取该角色在该菜单下的数据规则ID列表
|
||||
Set<Long> dataRuleIds = permissionService.getRoleMenuDataRules(roleId, menu.getMenuId());
|
||||
reqVO.setDataRuleIds(dataRuleIds != null ? new ArrayList<>(dataRuleIds) : null);
|
||||
return reqVO;
|
||||
}).collect(Collectors.toSet());
|
||||
return success(result);
|
||||
@@ -83,6 +86,10 @@ public class PermissionController {
|
||||
|
||||
// 更新菜单的显示状态
|
||||
permissionService.updateMenuDisplay(reqVO.getRoleId(), reqVO.getMenus());
|
||||
|
||||
// 保存菜单数据规则关联
|
||||
permissionService.assignRoleMenuDataRules(reqVO.getRoleId(), reqVO.getMenus());
|
||||
|
||||
return success(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.zt.plat.module.system.controller.admin.permission.vo.role.RolePageReq
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleRespVO;
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.RoleDO;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData;
|
||||
import com.zt.plat.module.system.service.permission.RoleService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -78,6 +79,7 @@ public class RoleController {
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得角色分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:role:query')")
|
||||
@PermissionData(pageComponent = "system/role/index")
|
||||
public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) {
|
||||
PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO);
|
||||
// 获取所有父级角色信息
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.zt.plat.module.system.controller.admin.permission.vo.menudatarule;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 Response VO
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Schema(description = "管理后台 - 菜单数据规则 Response VO")
|
||||
@Data
|
||||
public class MenuDataRuleRespVO {
|
||||
|
||||
@Schema(description = "规则ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "菜单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "仅看本部门数据")
|
||||
private String ruleName;
|
||||
|
||||
@Schema(description = "规则字段", example = "dept_id")
|
||||
private String ruleColumn;
|
||||
|
||||
@Schema(description = "规则条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "=")
|
||||
private String ruleConditions;
|
||||
|
||||
@Schema(description = "规则值", requiredMode = Schema.RequiredMode.REQUIRED, example = "#{deptId}")
|
||||
private String ruleValue;
|
||||
|
||||
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "排序", example = "1")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "限制只能查看本部门数据")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.zt.plat.module.system.controller.admin.permission.vo.menudatarule;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 菜单数据规则创建/修改 Request VO
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Schema(description = "管理后台 - 菜单数据规则创建/修改 Request VO")
|
||||
@Data
|
||||
public class MenuDataRuleSaveReqVO {
|
||||
|
||||
@Schema(description = "规则ID", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "菜单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "菜单ID不能为空")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "仅看本部门数据")
|
||||
@NotBlank(message = "规则名称不能为空")
|
||||
@Size(max = 100, message = "规则名称长度不能超过 100 个字符")
|
||||
private String ruleName;
|
||||
|
||||
@Schema(description = "规则字段", example = "dept_id")
|
||||
@Size(max = 100, message = "规则字段长度不能超过 100 个字符")
|
||||
private String ruleColumn;
|
||||
|
||||
@Schema(description = "规则条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "=")
|
||||
@NotBlank(message = "规则条件不能为空")
|
||||
@Size(max = 20, message = "规则条件长度不能超过 20 个字符")
|
||||
private String ruleConditions;
|
||||
|
||||
@Schema(description = "规则值", requiredMode = Schema.RequiredMode.REQUIRED, example = "#{deptId}")
|
||||
@NotBlank(message = "规则值不能为空")
|
||||
@Size(max = 500, message = "规则值长度不能超过 500 个字符")
|
||||
private String ruleValue;
|
||||
|
||||
@Schema(description = "状态", example = "1")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "排序", example = "1")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "限制只能查看本部门数据")
|
||||
@Size(max = 500, message = "备注长度不能超过 500 个字符")
|
||||
private String remark;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Schema(description = "管理后台 - 赋予角色菜单--菜单列表 Request VO")
|
||||
@Data
|
||||
@@ -19,4 +21,7 @@ public class PermissionAssignRoleMenuItemReqVO {
|
||||
@Schema(description = "是否显示菜单按钮是否点击过(避免大量更新数据,只更新点击过的)")
|
||||
private Boolean showMenuChanged = false;
|
||||
|
||||
@Schema(description = "菜单数据规则ID列表", example = "[1, 2, 3]")
|
||||
private List<Long> dataRuleIds;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.zt.plat.module.system.convert.permission;
|
||||
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleRespVO;
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 Convert
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Mapper
|
||||
public interface MenuDataRuleConvert {
|
||||
|
||||
MenuDataRuleConvert INSTANCE = Mappers.getMapper(MenuDataRuleConvert.class);
|
||||
|
||||
MenuDataRuleDO convert(MenuDataRuleSaveReqVO bean);
|
||||
|
||||
MenuDataRuleRespVO convert(MenuDataRuleDO bean);
|
||||
|
||||
List<MenuDataRuleRespVO> convertList(List<MenuDataRuleDO> list);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.zt.plat.module.system.dal.dataobject.permission;
|
||||
|
||||
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 DO
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@TableName("system_menu_data_rule")
|
||||
@KeySequence("system_menu_data_rule_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MenuDataRuleDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 规则ID
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 菜单ID
|
||||
*/
|
||||
private Long menuId;
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String ruleName;
|
||||
|
||||
/**
|
||||
* 规则字段(数据库列名)
|
||||
*/
|
||||
private String ruleColumn;
|
||||
|
||||
/**
|
||||
* 规则条件(=、>、<、IN、LIKE等)
|
||||
*/
|
||||
private String ruleConditions;
|
||||
|
||||
/**
|
||||
* 规则值(支持变量如#{userId}、#{deptId})
|
||||
*/
|
||||
private String ruleValue;
|
||||
|
||||
/**
|
||||
* 状态(0=禁用 1=启用)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.zt.plat.module.system.dal.dataobject.permission;
|
||||
|
||||
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色菜单数据规则关联 DO
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@TableName("system_role_menu_data_rule")
|
||||
@KeySequence("system_role_menu_data_rule_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class RoleMenuDataRuleDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 自增主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 菜单ID
|
||||
*/
|
||||
private Long menuId;
|
||||
|
||||
/**
|
||||
* 数据规则ID
|
||||
*/
|
||||
private Long dataRuleId;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.zt.plat.module.system.dal.mysql.permission;
|
||||
|
||||
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 Mapper
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Mapper
|
||||
public interface MenuDataRuleMapper extends BaseMapperX<MenuDataRuleDO> {
|
||||
|
||||
/**
|
||||
* 根据菜单ID查询规则列表
|
||||
*
|
||||
* @param menuId 菜单ID
|
||||
* @return 规则列表
|
||||
*/
|
||||
default List<MenuDataRuleDO> selectListByMenuId(Long menuId) {
|
||||
return selectList(MenuDataRuleDO::getMenuId, menuId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色和菜单查询规则ID列表
|
||||
*
|
||||
* @param roleIds 角色ID集合
|
||||
* @param menuId 菜单ID
|
||||
* @return 规则ID列表
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT DISTINCT rmdr.data_rule_id " +
|
||||
"FROM system_role_menu_data_rule rmdr " +
|
||||
"WHERE rmdr.role_id IN " +
|
||||
"<foreach collection='roleIds' item='roleId' open='(' separator=',' close=')'>" +
|
||||
"#{roleId}" +
|
||||
"</foreach>" +
|
||||
"AND rmdr.menu_id = #{menuId} " +
|
||||
"AND rmdr.deleted = 0" +
|
||||
"</script>")
|
||||
List<Long> selectRuleIdsByRoleAndMenu(@Param("roleIds") Collection<Long> roleIds,
|
||||
@Param("menuId") Long menuId);
|
||||
|
||||
/**
|
||||
* 批量查询菜单的规则
|
||||
*
|
||||
* @param menuIds 菜单ID集合
|
||||
* @return 规则列表
|
||||
*/
|
||||
default List<MenuDataRuleDO> selectListByMenuIds(Collection<Long> menuIds) {
|
||||
return selectList("menu_id", menuIds);
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,8 @@ public interface MenuMapper extends BaseMapperX<MenuDO> {
|
||||
return selectOne(MenuDO::getComponentName, componentName);
|
||||
}
|
||||
|
||||
default MenuDO selectByComponent(String component) {
|
||||
return selectOne(MenuDO::getComponent, component);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.zt.plat.module.system.dal.mysql.permission;
|
||||
|
||||
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色菜单数据规则关联 Mapper
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMenuDataRuleMapper extends BaseMapperX<RoleMenuDataRuleDO> {
|
||||
|
||||
/**
|
||||
* 根据角色ID和菜单ID查询规则关联
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param menuId 菜单ID
|
||||
* @return 规则关联列表
|
||||
*/
|
||||
default List<RoleMenuDataRuleDO> selectListByRoleAndMenu(Long roleId, Long menuId) {
|
||||
return selectList(new LambdaQueryWrapper<RoleMenuDataRuleDO>()
|
||||
.eq(RoleMenuDataRuleDO::getRoleId, roleId)
|
||||
.eq(RoleMenuDataRuleDO::getMenuId, menuId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色ID和菜单ID删除规则关联
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param menuId 菜单ID
|
||||
*/
|
||||
default void deleteByRoleAndMenu(Long roleId, Long menuId) {
|
||||
delete(new LambdaQueryWrapper<RoleMenuDataRuleDO>()
|
||||
.eq(RoleMenuDataRuleDO::getRoleId, roleId)
|
||||
.eq(RoleMenuDataRuleDO::getMenuId, menuId));
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,9 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
|
||||
.put(LoginUser.INFO_KEY_USERNAME, user.getUsername())
|
||||
.put(LoginUser.INFO_KEY_PHONE, user.getMobile())
|
||||
.put(LoginUser.INFO_KEY_POST_IDS, CollUtil.isEmpty(user.getPostIds()) ? "[]" : JsonUtils.toJsonString(user.getPostIds()))
|
||||
.put(LoginUser.INFO_KEY_DEPT_IDS, CollUtil.isEmpty(user.getDeptIds()) ? "[]" : JsonUtils.toJsonString(user.getDeptIds()))
|
||||
.put(LoginUser.INFO_KEY_COMPANY_IDS, CollUtil.isEmpty(user.getCompanyIds()) ? "[]" : JsonUtils.toJsonString(user.getCompanyIds()))
|
||||
.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, CollUtil.isEmpty(user.getCompanyDeptInfos()) ? "[]" : JsonUtils.toJsonString(user.getCompanyDeptInfos()))
|
||||
.build();
|
||||
} else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
|
||||
// 注意:目前 Member 暂时不读取,可以按需实现
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.zt.plat.module.system.service.permission;
|
||||
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 Service 接口
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public interface MenuDataRuleService {
|
||||
|
||||
/**
|
||||
* 创建菜单数据规则
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 规则ID
|
||||
*/
|
||||
Long createMenuDataRule(@Valid MenuDataRuleSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新菜单数据规则
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateMenuDataRule(@Valid MenuDataRuleSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除菜单数据规则
|
||||
*
|
||||
* @param id 规则ID
|
||||
*/
|
||||
void deleteMenuDataRule(Long id);
|
||||
|
||||
/**
|
||||
* 获取菜单数据规则
|
||||
*
|
||||
* @param id 规则ID
|
||||
* @return 规则信息
|
||||
*/
|
||||
MenuDataRuleDO getMenuDataRule(Long id);
|
||||
|
||||
/**
|
||||
* 获取菜单的所有数据规则
|
||||
*
|
||||
* @param menuId 菜单ID
|
||||
* @return 规则列表
|
||||
*/
|
||||
List<MenuDataRuleDO> getMenuDataRuleListByMenuId(Long menuId);
|
||||
|
||||
/**
|
||||
* 获取用户在指定菜单下的有效数据规则
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param menuId 菜单ID
|
||||
* @return 规则列表
|
||||
*/
|
||||
List<MenuDataRuleDO> getUserMenuDataRules(Long userId, Long menuId);
|
||||
|
||||
/**
|
||||
* 批量获取菜单的数据规则(带缓存)
|
||||
*
|
||||
* @param menuIds 菜单ID列表
|
||||
* @return 菜单ID -> 规则列表的映射
|
||||
*/
|
||||
Map<Long, List<MenuDataRuleDO>> getMenuDataRuleMap(Collection<Long> menuIds);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.zt.plat.module.system.service.permission;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.MenuDO;
|
||||
import com.zt.plat.module.system.dal.mysql.permission.MenuMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 页面组件映射服务
|
||||
* 根据pageComponent查询对应的菜单ID
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PageComponentMappingService {
|
||||
|
||||
@Resource
|
||||
private MenuMapper menuMapper;
|
||||
|
||||
/**
|
||||
* 根据页面组件路径获取菜单ID
|
||||
*
|
||||
* @param pageComponent 页面组件路径,如:system/role/index
|
||||
* @return 菜单ID,如果未找到返回null
|
||||
*/
|
||||
public Long getMenuIdByPageComponent(String pageComponent) {
|
||||
if (StrUtil.isBlank(pageComponent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("[getMenuIdByPageComponent][查询pageComponent: {}]", pageComponent);
|
||||
|
||||
// 使用精确匹配查询菜单
|
||||
MenuDO menu = menuMapper.selectByComponent(pageComponent);
|
||||
|
||||
if (menu != null) {
|
||||
log.debug("[getMenuIdByPageComponent][找到匹配菜单: ID={}, Name={}, Component={}]",
|
||||
menu.getId(), menu.getName(), menu.getComponent());
|
||||
// 兼容达梦数据库,ID可能是Integer类型,需要转换为Long
|
||||
Object id = menu.getId();
|
||||
if (id instanceof Number) {
|
||||
return ((Number) id).longValue();
|
||||
}
|
||||
return (Long) id;
|
||||
}
|
||||
|
||||
log.warn("[getMenuIdByPageComponent][未找到匹配的菜单: {}]", pageComponent);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,23 @@ public interface PermissionService {
|
||||
*/
|
||||
Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId);
|
||||
|
||||
/**
|
||||
* 批量设置角色-菜单-规则关联
|
||||
*
|
||||
* @param roleId 角色编号
|
||||
* @param menuDataRules 菜单和规则的映射关系
|
||||
*/
|
||||
void assignRoleMenuDataRules(Long roleId, Collection<PermissionAssignRoleMenuItemReqVO> menuDataRules);
|
||||
|
||||
/**
|
||||
* 获取角色在指定菜单下已选择的数据规则ID列表
|
||||
*
|
||||
* @param roleId 角色编号
|
||||
* @param menuId 菜单编号
|
||||
* @return 数据规则ID列表
|
||||
*/
|
||||
Set<Long> getRoleMenuDataRules(Long roleId, Long menuId);
|
||||
|
||||
// ========== 用户-角色的相关方法 ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,6 +76,8 @@ public class PermissionServiceImpl implements PermissionService {
|
||||
private RoleMenuMapper roleMenuMapper;
|
||||
@Resource
|
||||
private UserRoleMapper userRoleMapper;
|
||||
@Resource
|
||||
private com.zt.plat.module.system.dal.mysql.permission.RoleMenuDataRuleMapper roleMenuDataRuleMapper;
|
||||
|
||||
private RoleService roleService;
|
||||
@Resource
|
||||
@@ -221,6 +223,45 @@ public class PermissionServiceImpl implements PermissionService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assignRoleMenuDataRules(Long roleId, Collection<PermissionAssignRoleMenuItemReqVO> menuDataRules) {
|
||||
if (CollUtil.isEmpty(menuDataRules)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历每个菜单,更新其数据规则关联
|
||||
for (PermissionAssignRoleMenuItemReqVO menuDataRule : menuDataRules) {
|
||||
Long menuId = menuDataRule.getId();
|
||||
List<Long> dataRuleIds = menuDataRule.getDataRuleIds();
|
||||
|
||||
// 删除该角色在该菜单下的旧规则关联
|
||||
roleMenuDataRuleMapper.deleteByRoleAndMenu(roleId, menuId);
|
||||
|
||||
// 如果有新规则,则插入
|
||||
if (CollUtil.isNotEmpty(dataRuleIds)) {
|
||||
List<com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO> entities =
|
||||
dataRuleIds.stream().map(ruleId -> {
|
||||
com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO entity =
|
||||
new com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO();
|
||||
entity.setRoleId(roleId);
|
||||
entity.setMenuId(menuId);
|
||||
entity.setDataRuleId(ruleId);
|
||||
return entity;
|
||||
}).collect(Collectors.toList());
|
||||
roleMenuDataRuleMapper.insertBatch(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getRoleMenuDataRules(Long roleId, Long menuId) {
|
||||
List<com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO> list =
|
||||
roleMenuDataRuleMapper.selectListByRoleAndMenu(roleId, menuId);
|
||||
return CollectionUtils.convertSet(list,
|
||||
com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO::getDataRuleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Caching(evict = {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.zt.plat.module.system.service.permission.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO;
|
||||
import com.zt.plat.module.system.convert.permission.MenuDataRuleConvert;
|
||||
import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO;
|
||||
import com.zt.plat.module.system.dal.mysql.permission.MenuDataRuleMapper;
|
||||
import com.zt.plat.module.system.service.permission.MenuDataRuleService;
|
||||
import com.zt.plat.module.system.service.permission.PermissionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 Service 实现类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class MenuDataRuleServiceImpl implements MenuDataRuleService {
|
||||
|
||||
@Resource
|
||||
private MenuDataRuleMapper menuDataRuleMapper;
|
||||
|
||||
@Resource
|
||||
private PermissionService permissionService;
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = "menuDataRule", key = "#createReqVO.menuId")
|
||||
public Long createMenuDataRule(MenuDataRuleSaveReqVO createReqVO) {
|
||||
MenuDataRuleDO rule = MenuDataRuleConvert.INSTANCE.convert(createReqVO);
|
||||
menuDataRuleMapper.insert(rule);
|
||||
return rule.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CacheEvict(value = "menuDataRule", key = "#updateReqVO.menuId")
|
||||
public void updateMenuDataRule(MenuDataRuleSaveReqVO updateReqVO) {
|
||||
validateMenuDataRuleExists(updateReqVO.getId());
|
||||
MenuDataRuleDO updateObj = MenuDataRuleConvert.INSTANCE.convert(updateReqVO);
|
||||
menuDataRuleMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMenuDataRule(Long id) {
|
||||
MenuDataRuleDO rule = validateMenuDataRuleExists(id);
|
||||
menuDataRuleMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MenuDataRuleDO getMenuDataRule(Long id) {
|
||||
return menuDataRuleMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "menuDataRule", key = "#menuId")
|
||||
public List<MenuDataRuleDO> getMenuDataRuleListByMenuId(Long menuId) {
|
||||
return menuDataRuleMapper.selectListByMenuId(menuId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MenuDataRuleDO> getUserMenuDataRules(Long userId, Long menuId) {
|
||||
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(userId);
|
||||
if (CollUtil.isEmpty(roleIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<MenuDataRuleDO> allRules = getMenuDataRuleListByMenuId(menuId);
|
||||
if (CollUtil.isEmpty(allRules)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Long> ruleIds = menuDataRuleMapper.selectRuleIdsByRoleAndMenu(roleIds, menuId);
|
||||
|
||||
// 如果角色没有关联任何规则,返回空列表(不应用任何过滤)
|
||||
if (CollUtil.isEmpty(ruleIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return allRules.stream()
|
||||
.filter(rule -> ruleIds.contains(rule.getId()) && rule.getStatus() == 1)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, List<MenuDataRuleDO>> getMenuDataRuleMap(Collection<Long> menuIds) {
|
||||
List<MenuDataRuleDO> rules = menuDataRuleMapper.selectListByMenuIds(menuIds);
|
||||
return rules.stream().collect(Collectors.groupingBy(MenuDataRuleDO::getMenuId));
|
||||
}
|
||||
|
||||
private MenuDataRuleDO validateMenuDataRuleExists(Long id) {
|
||||
MenuDataRuleDO rule = menuDataRuleMapper.selectById(id);
|
||||
if (rule == null) {
|
||||
throw exception(MENU_NOT_EXISTS);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user