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

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

File diff suppressed because it is too large Load Diff

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

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.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();

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

Some files were not shown because too many files have changed in this diff Show More