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

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

View File

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

View File

@@ -0,0 +1,33 @@
package com.zt.plat.framework.datapermission.core.menudatapermission.model;
import lombok.Data;
/**
* 菜单数据规则 DTO
* 用于在框架层传递菜单数据规则信息,避免依赖业务模块的数据库实体
*
* @author ZT
*/
@Data
public class MenuDataRuleDTO {
/**
* 规则字段(数据库列名)
*/
private String ruleColumn;
/**
* 规则条件(=、>、<、IN、LIKE等
*/
private String ruleConditions;
/**
* 规则值(支持变量如#{userId}、#{deptId}
*/
private String ruleValue;
/**
* 状态0=禁用 1=启用)
*/
private Integer status;
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.framework.datapermission.core.menudatapermission.service;
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
import java.util.List;
/**
* 菜单数据规则加载器接口
* 负责加载菜单和菜单数据规则
*
* @author ZT
*/
public interface MenuDataRuleLoader {
/**
* 根据页面组件路径获取菜单ID
*
* @param pageComponent 页面组件路径system/role/index
* @return 菜单ID如果未找到返回null
*/
Long getMenuIdByPageComponent(String pageComponent);
/**
* 获取用户在指定菜单下的数据规则
*
* @param userId 用户ID
* @param menuId 菜单ID
* @return 数据规则列表
*/
List<MenuDataRuleDTO> getUserMenuDataRules(Long userId, Long menuId);
}

View File

@@ -0,0 +1,45 @@
package com.zt.plat.framework.datapermission.core.menudatapermission.service.impl;
import com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper.MenuDataPermissionMapper;
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
import com.zt.plat.framework.datapermission.core.menudatapermission.service.MenuDataRuleLoader;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 菜单数据规则加载器默认实现
* 直接从数据库加载菜单数据规则
*
* @author ZT
*/
@Component
@Slf4j
public class MenuDataRuleLoaderImpl implements MenuDataRuleLoader {
@Resource
private MenuDataPermissionMapper menuDataPermissionMapper;
@Override
public Long getMenuIdByPageComponent(String pageComponent) {
try {
return menuDataPermissionMapper.selectMenuIdByComponent(pageComponent);
} catch (Exception e) {
log.error("[MenuDataRuleLoaderImpl][根据pageComponent查询菜单ID失败: {}]", pageComponent, e);
return null;
}
}
@Override
public List<MenuDataRuleDTO> getUserMenuDataRules(Long userId, Long menuId) {
try {
return menuDataPermissionMapper.selectUserMenuDataRules(userId, menuId);
} catch (Exception e) {
log.error("[MenuDataRuleLoaderImpl][查询用户菜单数据规则失败: userId={}, menuId={}]",
userId, menuId, e);
return List.of();
}
}
}

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