feat(permission): 添加菜单数据权限功能

- 新增菜单数据规则表和角色菜单数据规则关联表
- 实现菜单数据权限切面和处理器
- 添加数据规则条件和变量枚举
- 实现变量替换工具类和规则构建逻辑
- 在权限分配中集成菜单数据规则关联功能
- 优化部门ID解析逻辑,支持从用户信息中获取默认部门
- 添加菜单组件查询方法和公司访问上下文拦截器改进
This commit is contained in:
wuzongyong
2026-01-28 09:13:23 +08:00
parent 6ea653ca43
commit 2227271d08
37 changed files with 2288 additions and 1 deletions

View 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
-- 规则1creator = #{userId}
-- 规则2status = 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
# 方式1MyBatis Plus SQL 日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 方式2Logback 日志
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

View 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 '角色菜单数据规则关联表';

View File

@@ -4313,3 +4313,74 @@ VALUES
(5022, 2, '日期格式', 'DATE', 'system_sequence_detail_rule_type', 0, 'success', '', '日期格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0), (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), (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); (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 '角色菜单数据规则关联表';

View File

@@ -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.DataPermissionAnnotationAdvisor;
import com.zt.plat.framework.datapermission.core.aop.DeptDataPermissionIgnoreAspect; 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.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.DataPermissionRule;
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactory; import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactory;
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils; import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -21,6 +24,7 @@ import java.util.List;
* @author ZT * @author ZT
*/ */
@AutoConfiguration @AutoConfiguration
@MapperScan("com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper")
public class ZtDataPermissionAutoConfiguration { public class ZtDataPermissionAutoConfiguration {
@Bean @Bean
@@ -40,6 +44,21 @@ public class ZtDataPermissionAutoConfiguration {
return handler; 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 @Bean
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
return new DataPermissionAnnotationAdvisor(); return new DataPermissionAnnotationAdvisor();

View File

@@ -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;
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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 "";
}
}
}

View File

@@ -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("'", "''");
}
}

View File

@@ -2,3 +2,4 @@ com.zt.plat.framework.datapermission.config.ZtDataPermissionAutoConfiguration
com.zt.plat.framework.datapermission.config.ZtDeptDataPermissionAutoConfiguration com.zt.plat.framework.datapermission.config.ZtDeptDataPermissionAutoConfiguration
com.zt.plat.framework.datapermission.config.ZtBusinessDataPermissionAutoConfiguration com.zt.plat.framework.datapermission.config.ZtBusinessDataPermissionAutoConfiguration
com.zt.plat.framework.datapermission.config.ZtDataPermissionRpcAutoConfiguration com.zt.plat.framework.datapermission.config.ZtDataPermissionRpcAutoConfiguration
com.zt.plat.framework.datapermission.core.menudatapermission.config.MenuDataPermissionConfiguration

View File

@@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
/** /**
* @author chenbowen * @author chenbowen
*/ */
@@ -45,13 +47,31 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor {
} }
Long deptId = WebFrameworkUtils.getDeptId(request); Long deptId = WebFrameworkUtils.getDeptId(request);
// 部门信息同样遵循请求头 -> 请求属性 -> 登录缓存的回退顺序 // 部门信息同样遵循"请求头 -> 请求属性 -> 登录缓存"的回退顺序
if (deptId == null || deptId <= 0L) { if (deptId == null || deptId <= 0L) {
Long attrDeptId = resolveLong(request.getAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_ID)); Long attrDeptId = resolveLong(request.getAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_ID));
if (attrDeptId != null && attrDeptId > 0L) { if (attrDeptId != null && attrDeptId > 0L) {
deptId = attrDeptId; deptId = attrDeptId;
} else if (loginUser != null && loginUser.getVisitDeptId() != null && loginUser.getVisitDeptId() > 0L) { } else if (loginUser != null && loginUser.getVisitDeptId() != null && loginUser.getVisitDeptId() > 0L) {
deptId = loginUser.getVisitDeptId(); 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);
}
}
}
} }
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -66,6 +66,9 @@ public class PermissionController {
PermissionAssignRoleMenuItemReqVO reqVO = new PermissionAssignRoleMenuItemReqVO(); PermissionAssignRoleMenuItemReqVO reqVO = new PermissionAssignRoleMenuItemReqVO();
reqVO.setId(menu.getMenuId()); reqVO.setId(menu.getMenuId());
reqVO.setShowMenu(menu.getShowMenu()); reqVO.setShowMenu(menu.getShowMenu());
// 获取该角色在该菜单下的数据规则ID列表
Set<Long> dataRuleIds = permissionService.getRoleMenuDataRules(roleId, menu.getMenuId());
reqVO.setDataRuleIds(dataRuleIds != null ? new ArrayList<>(dataRuleIds) : null);
return reqVO; return reqVO;
}).collect(Collectors.toSet()); }).collect(Collectors.toSet());
return success(result); return success(result);
@@ -83,6 +86,10 @@ public class PermissionController {
// 更新菜单的显示状态 // 更新菜单的显示状态
permissionService.updateMenuDisplay(reqVO.getRoleId(), reqVO.getMenus()); permissionService.updateMenuDisplay(reqVO.getRoleId(), reqVO.getMenus());
// 保存菜单数据规则关联
permissionService.assignRoleMenuDataRules(reqVO.getRoleId(), reqVO.getMenus());
return success(true); return success(true);
} }

View File

@@ -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.RoleRespVO;
import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; 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.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 com.zt.plat.module.system.service.permission.RoleService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -78,6 +79,7 @@ public class RoleController {
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获得角色分页") @Operation(summary = "获得角色分页")
@PreAuthorize("@ss.hasPermission('system:role:query')") @PreAuthorize("@ss.hasPermission('system:role:query')")
@PermissionData(pageComponent = "system/role/index")
public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) { public CommonResult<PageResult<RoleRespVO>> getRolePage(RolePageReqVO pageReqVO) {
PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO); PageResult<RoleDO> pageResult = roleService.getRolePage(pageReqVO);
// 获取所有父级角色信息 // 获取所有父级角色信息

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 赋予角色菜单--菜单列表 Request VO") @Schema(description = "管理后台 - 赋予角色菜单--菜单列表 Request VO")
@Data @Data
@@ -19,4 +21,7 @@ public class PermissionAssignRoleMenuItemReqVO {
@Schema(description = "是否显示菜单按钮是否点击过(避免大量更新数据,只更新点击过的)") @Schema(description = "是否显示菜单按钮是否点击过(避免大量更新数据,只更新点击过的)")
private Boolean showMenuChanged = false; private Boolean showMenuChanged = false;
@Schema(description = "菜单数据规则ID列表", example = "[1, 2, 3]")
private List<Long> dataRuleIds;
} }

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -33,4 +33,8 @@ public interface MenuMapper extends BaseMapperX<MenuDO> {
return selectOne(MenuDO::getComponentName, componentName); return selectOne(MenuDO::getComponentName, componentName);
} }
default MenuDO selectByComponent(String component) {
return selectOne(MenuDO::getComponent, component);
}
} }

View File

@@ -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));
}
}

View File

@@ -204,6 +204,9 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
.put(LoginUser.INFO_KEY_USERNAME, user.getUsername()) .put(LoginUser.INFO_KEY_USERNAME, user.getUsername())
.put(LoginUser.INFO_KEY_PHONE, user.getMobile()) .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_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(); .build();
} else if (userType.equals(UserTypeEnum.MEMBER.getValue())) { } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
// 注意:目前 Member 暂时不读取,可以按需实现 // 注意:目前 Member 暂时不读取,可以按需实现

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -86,6 +86,23 @@ public interface PermissionService {
*/ */
Set<Long> getMenuRoleIdListByMenuIdFromCache(Long menuId); 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);
// ========== 用户-角色的相关方法 ========== // ========== 用户-角色的相关方法 ==========
/** /**

View File

@@ -76,6 +76,8 @@ public class PermissionServiceImpl implements PermissionService {
private RoleMenuMapper roleMenuMapper; private RoleMenuMapper roleMenuMapper;
@Resource @Resource
private UserRoleMapper userRoleMapper; private UserRoleMapper userRoleMapper;
@Resource
private com.zt.plat.module.system.dal.mysql.permission.RoleMenuDataRuleMapper roleMenuDataRuleMapper;
private RoleService roleService; private RoleService roleService;
@Resource @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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Caching(evict = { @Caching(evict = {

View File

@@ -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;
}
}