From 6c94476a8d1fe7f9b851384ffb56c414561d078e Mon Sep 17 00:00:00 2001
From: wuzongyong <13203449218@163.com>
Date: Wed, 28 Jan 2026 09:13:23 +0800
Subject: [PATCH] =?UTF-8?q?feat(permission):=20=E6=B7=BB=E5=8A=A0=E8=8F=9C?=
=?UTF-8?q?=E5=8D=95=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增菜单数据规则表和角色菜单数据规则关联表
- 实现菜单数据权限切面和处理器
- 添加数据规则条件和变量枚举
- 实现变量替换工具类和规则构建逻辑
- 在权限分配中集成菜单数据规则关联功能
- 优化部门ID解析逻辑,支持从用户信息中获取默认部门
- 添加菜单组件查询方法和公司访问上下文拦截器改进
---
docs/菜单数据权限使用文档.md | 617 ++++++++++++++++++
sql/dm/20260126菜单数据规则表.sql | 73 +++
sql/dm/ruoyi-vue-pro-dm8.sql | 71 ++
.../ZtDataPermissionAutoConfiguration.java | 19 +
.../annotation/PermissionData.java | 29 +
.../aop/MenuDataPermissionAspect.java | 89 +++
.../MenuDataPermissionConfiguration.java | 36 +
.../context/MenuDataRuleContextHolder.java | 41 ++
.../dal/mapper/MenuDataPermissionMapper.java | 51 ++
.../handler/MenuDataPermissionHandler.java | 41 ++
.../model/MenuDataRuleDTO.java | 33 +
.../service/MenuDataRuleLoader.java | 31 +
.../service/impl/MenuDataRuleLoaderImpl.java | 45 ++
.../util/DataRuleVariableUtils.java | 92 +++
.../util/MenuDataPermissionRule.java | 156 +++++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
.../web/CompanyVisitContextInterceptor.java | 22 +-
.../permission/DataRuleConditionEnum.java | 60 ++
.../permission/DataRuleVariableEnum.java | 53 ++
.../permission/MenuDataRuleController.java | 77 +++
.../permission/PermissionController.java | 7 +
.../admin/permission/RoleController.java | 2 +
.../vo/menudatarule/MenuDataRuleRespVO.java | 46 ++
.../menudatarule/MenuDataRuleSaveReqVO.java | 53 ++
.../PermissionAssignRoleMenuItemReqVO.java | 5 +
.../permission/MenuDataRuleConvert.java | 26 +
.../dataobject/permission/MenuDataRuleDO.java | 67 ++
.../permission/RoleMenuDataRuleDO.java | 42 ++
.../mysql/permission/MenuDataRuleMapper.java | 59 ++
.../dal/mysql/permission/MenuMapper.java | 4 +
.../permission/RoleMenuDataRuleMapper.java | 43 ++
.../oauth2/OAuth2TokenServiceImpl.java | 3 +
.../permission/MenuDataRuleService.java | 72 ++
.../PageComponentMappingService.java | 56 ++
.../service/permission/PermissionService.java | 17 +
.../permission/PermissionServiceImpl.java | 41 ++
.../impl/MenuDataRuleServiceImpl.java | 109 ++++
37 files changed, 2288 insertions(+), 1 deletion(-)
create mode 100644 docs/菜单数据权限使用文档.md
create mode 100644 sql/dm/20260126菜单数据规则表.sql
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/annotation/PermissionData.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/aop/MenuDataPermissionAspect.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/config/MenuDataPermissionConfiguration.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/context/MenuDataRuleContextHolder.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/dal/mapper/MenuDataPermissionMapper.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/handler/MenuDataPermissionHandler.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/model/MenuDataRuleDTO.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/MenuDataRuleLoader.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/impl/MenuDataRuleLoaderImpl.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/DataRuleVariableUtils.java
create mode 100644 zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/MenuDataPermissionRule.java
create mode 100644 zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java
create mode 100644 zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java
create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java
diff --git a/docs/菜单数据权限使用文档.md b/docs/菜单数据权限使用文档.md
new file mode 100644
index 00000000..fe539ea9
--- /dev/null
+++ b/docs/菜单数据权限使用文档.md
@@ -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
+
+ com.zt.plat
+ zt-spring-boot-starter-biz-data-permission
+
+```
+
+#### 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
+
+
+
+
+
+ com.zt.plat
+ zt-spring-boot-starter-biz-data-permission
+
+
+```
+
+#### 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> getRolePage(RolePageReqVO pageReqVO) {
+ PageResult 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> 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 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> getRolePage(RolePageReqVO pageReqVO) {
+ PageResult 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
diff --git a/sql/dm/20260126菜单数据规则表.sql b/sql/dm/20260126菜单数据规则表.sql
new file mode 100644
index 00000000..7fb4e0b3
--- /dev/null
+++ b/sql/dm/20260126菜单数据规则表.sql
@@ -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 '角色菜单数据规则关联表';
diff --git a/sql/dm/ruoyi-vue-pro-dm8.sql b/sql/dm/ruoyi-vue-pro-dm8.sql
index 1a0eba7e..83fc4b8f 100644
--- a/sql/dm/ruoyi-vue-pro-dm8.sql
+++ b/sql/dm/ruoyi-vue-pro-dm8.sql
@@ -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 '角色菜单数据规则关联表';
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java
index 1c7d9e91..6c95cc7c 100644
--- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java
@@ -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();
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/annotation/PermissionData.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/annotation/PermissionData.java
new file mode 100644
index 00000000..4eed8a18
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/annotation/PermissionData.java
@@ -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;
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/aop/MenuDataPermissionAspect.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/aop/MenuDataPermissionAspect.java
new file mode 100644
index 00000000..acb87fb8
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/aop/MenuDataPermissionAspect.java
@@ -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 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();
+ }
+ }
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/config/MenuDataPermissionConfiguration.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/config/MenuDataPermissionConfiguration.java
new file mode 100644
index 00000000..172fba3d
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/config/MenuDataPermissionConfiguration.java
@@ -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;
+ }
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/context/MenuDataRuleContextHolder.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/context/MenuDataRuleContextHolder.java
new file mode 100644
index 00000000..4f2abaa8
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/context/MenuDataRuleContextHolder.java
@@ -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> CONTEXT = new ThreadLocal<>();
+
+ /**
+ * 设置当前请求的菜单数据规则
+ *
+ * @param rules 规则列表
+ */
+ public static void setRules(List rules) {
+ CONTEXT.set(rules);
+ }
+
+ /**
+ * 获取当前请求的菜单数据规则
+ *
+ * @return 规则列表
+ */
+ public static List getRules() {
+ return CONTEXT.get();
+ }
+
+ /**
+ * 清除当前请求的菜单数据规则
+ */
+ public static void clear() {
+ CONTEXT.remove();
+ }
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/dal/mapper/MenuDataPermissionMapper.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/dal/mapper/MenuDataPermissionMapper.java
new file mode 100644
index 00000000..0c46c8b7
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/dal/mapper/MenuDataPermissionMapper.java
@@ -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("")
+ List selectUserMenuDataRules(@Param("userId") Long userId,
+ @Param("menuId") Long menuId);
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/handler/MenuDataPermissionHandler.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/handler/MenuDataPermissionHandler.java
new file mode 100644
index 00000000..c72a2b0e
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/handler/MenuDataPermissionHandler.java
@@ -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;
+ }
+ }
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/model/MenuDataRuleDTO.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/model/MenuDataRuleDTO.java
new file mode 100644
index 00000000..2e6ef64b
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/model/MenuDataRuleDTO.java
@@ -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;
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/MenuDataRuleLoader.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/MenuDataRuleLoader.java
new file mode 100644
index 00000000..2d1efe69
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/MenuDataRuleLoader.java
@@ -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 getUserMenuDataRules(Long userId, Long menuId);
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/impl/MenuDataRuleLoaderImpl.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/impl/MenuDataRuleLoaderImpl.java
new file mode 100644
index 00000000..57311c64
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/impl/MenuDataRuleLoaderImpl.java
@@ -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 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();
+ }
+ }
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/DataRuleVariableUtils.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/DataRuleVariableUtils.java
new file mode 100644
index 00000000..b368db61
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/DataRuleVariableUtils.java
@@ -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 "";
+ }
+ }
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/MenuDataPermissionRule.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/MenuDataPermissionRule.java
new file mode 100644
index 00000000..ef62d916
--- /dev/null
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/MenuDataPermissionRule.java
@@ -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 rules = MenuDataRuleContextHolder.getRules();
+ if (CollUtil.isEmpty(rules)) {
+ return null;
+ }
+
+ List 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("'", "''");
+ }
+}
diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index df6ed09f..85d1ca70 100644
--- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -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
diff --git a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java
index f4688a21..99336abf 100644
--- a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java
+++ b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java
@@ -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 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);
+ }
+ }
+ }
}
}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java
new file mode 100644
index 00000000..9064c4d6
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java
@@ -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;
+ }
+}
diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java
new file mode 100644
index 00000000..1c5881cd
--- /dev/null
+++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java
@@ -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;
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java
new file mode 100644
index 00000000..be27f6c4
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java
@@ -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 createMenuDataRule(@Valid @RequestBody MenuDataRuleSaveReqVO createReqVO) {
+ return success(menuDataRuleService.createMenuDataRule(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新菜单数据规则")
+ @PreAuthorize("@ss.hasPermission('system:menu:update')")
+ public CommonResult 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 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 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> getMenuDataRuleList(@RequestParam("menuId") Long menuId) {
+ List list = menuDataRuleService.getMenuDataRuleListByMenuId(menuId);
+ return success(MenuDataRuleConvert.INSTANCE.convertList(list));
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java
index d84bc342..cd1d2728 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java
@@ -66,6 +66,9 @@ public class PermissionController {
PermissionAssignRoleMenuItemReqVO reqVO = new PermissionAssignRoleMenuItemReqVO();
reqVO.setId(menu.getMenuId());
reqVO.setShowMenu(menu.getShowMenu());
+ // 获取该角色在该菜单下的数据规则ID列表
+ Set 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);
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java
index 74845b6c..86186e3b 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java
@@ -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> getRolePage(RolePageReqVO pageReqVO) {
PageResult pageResult = roleService.getRolePage(pageReqVO);
// 获取所有父级角色信息
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java
new file mode 100644
index 00000000..4b624dea
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java
@@ -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;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java
new file mode 100644
index 00000000..d3044877
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java
@@ -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;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java
index 0d038266..576274b9 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java
@@ -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 dataRuleIds;
+
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java
new file mode 100644
index 00000000..1841074b
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java
@@ -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 convertList(List list);
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java
new file mode 100644
index 00000000..f8843af7
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java
@@ -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;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java
new file mode 100644
index 00000000..f5d9ae2f
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java
@@ -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;
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java
new file mode 100644
index 00000000..2f8aa889
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java
@@ -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 {
+
+ /**
+ * 根据菜单ID查询规则列表
+ *
+ * @param menuId 菜单ID
+ * @return 规则列表
+ */
+ default List selectListByMenuId(Long menuId) {
+ return selectList(MenuDataRuleDO::getMenuId, menuId);
+ }
+
+ /**
+ * 根据角色和菜单查询规则ID列表
+ *
+ * @param roleIds 角色ID集合
+ * @param menuId 菜单ID
+ * @return 规则ID列表
+ */
+ @Select("")
+ List selectRuleIdsByRoleAndMenu(@Param("roleIds") Collection roleIds,
+ @Param("menuId") Long menuId);
+
+ /**
+ * 批量查询菜单的规则
+ *
+ * @param menuIds 菜单ID集合
+ * @return 规则列表
+ */
+ default List selectListByMenuIds(Collection menuIds) {
+ return selectList("menu_id", menuIds);
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java
index 60a4af8b..5f9b4a3d 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java
@@ -33,4 +33,8 @@ public interface MenuMapper extends BaseMapperX {
return selectOne(MenuDO::getComponentName, componentName);
}
+ default MenuDO selectByComponent(String component) {
+ return selectOne(MenuDO::getComponent, component);
+ }
+
}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java
new file mode 100644
index 00000000..25d7e101
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java
@@ -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 {
+
+ /**
+ * 根据角色ID和菜单ID查询规则关联
+ *
+ * @param roleId 角色ID
+ * @param menuId 菜单ID
+ * @return 规则关联列表
+ */
+ default List selectListByRoleAndMenu(Long roleId, Long menuId) {
+ return selectList(new LambdaQueryWrapper()
+ .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()
+ .eq(RoleMenuDataRuleDO::getRoleId, roleId)
+ .eq(RoleMenuDataRuleDO::getMenuId, menuId));
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java
index 33552e9f..22e27de8 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java
@@ -205,6 +205,9 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
.put(LoginUser.INFO_KEY_PHONE, user.getMobile())
.put(LoginUser.INFO_KEY_WORK_CODE, user.getWorkcode())
.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 暂时不读取,可以按需实现
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java
new file mode 100644
index 00000000..e49d10d8
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java
@@ -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 getMenuDataRuleListByMenuId(Long menuId);
+
+ /**
+ * 获取用户在指定菜单下的有效数据规则
+ *
+ * @param userId 用户ID
+ * @param menuId 菜单ID
+ * @return 规则列表
+ */
+ List getUserMenuDataRules(Long userId, Long menuId);
+
+ /**
+ * 批量获取菜单的数据规则(带缓存)
+ *
+ * @param menuIds 菜单ID列表
+ * @return 菜单ID -> 规则列表的映射
+ */
+ Map> getMenuDataRuleMap(Collection menuIds);
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java
new file mode 100644
index 00000000..f62f9bc1
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java
@@ -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;
+ }
+}
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java
index 7a23c24e..6d570ca7 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java
@@ -86,6 +86,23 @@ public interface PermissionService {
*/
Set getMenuRoleIdListByMenuIdFromCache(Long menuId);
+ /**
+ * 批量设置角色-菜单-规则关联
+ *
+ * @param roleId 角色编号
+ * @param menuDataRules 菜单和规则的映射关系
+ */
+ void assignRoleMenuDataRules(Long roleId, Collection menuDataRules);
+
+ /**
+ * 获取角色在指定菜单下已选择的数据规则ID列表
+ *
+ * @param roleId 角色编号
+ * @param menuId 菜单编号
+ * @return 数据规则ID列表
+ */
+ Set getRoleMenuDataRules(Long roleId, Long menuId);
+
// ========== 用户-角色的相关方法 ==========
/**
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java
index ce302e9e..96820ed6 100644
--- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java
@@ -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 menuDataRules) {
+ if (CollUtil.isEmpty(menuDataRules)) {
+ return;
+ }
+
+ // 遍历每个菜单,更新其数据规则关联
+ for (PermissionAssignRoleMenuItemReqVO menuDataRule : menuDataRules) {
+ Long menuId = menuDataRule.getId();
+ List dataRuleIds = menuDataRule.getDataRuleIds();
+
+ // 删除该角色在该菜单下的旧规则关联
+ roleMenuDataRuleMapper.deleteByRoleAndMenu(roleId, menuId);
+
+ // 如果有新规则,则插入
+ if (CollUtil.isNotEmpty(dataRuleIds)) {
+ List 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 getRoleMenuDataRules(Long roleId, Long menuId) {
+ List 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 = {
diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java
new file mode 100644
index 00000000..da8cbef8
--- /dev/null
+++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java
@@ -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 getMenuDataRuleListByMenuId(Long menuId) {
+ return menuDataRuleMapper.selectListByMenuId(menuId);
+ }
+
+ @Override
+ public List getUserMenuDataRules(Long userId, Long menuId) {
+ Set roleIds = permissionService.getUserRoleIdListByUserId(userId);
+ if (CollUtil.isEmpty(roleIds)) {
+ return Collections.emptyList();
+ }
+
+ List allRules = getMenuDataRuleListByMenuId(menuId);
+ if (CollUtil.isEmpty(allRules)) {
+ return Collections.emptyList();
+ }
+
+ List 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> getMenuDataRuleMap(Collection menuIds) {
+ List 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;
+ }
+}