feat(permission): 添加菜单数据权限功能
- 新增菜单数据规则表和角色菜单数据规则关联表 - 实现菜单数据权限切面和处理器 - 添加数据规则条件和变量枚举 - 实现变量替换工具类和规则构建逻辑 - 在权限分配中集成菜单数据规则关联功能 - 优化部门ID解析逻辑,支持从用户信息中获取默认部门 - 添加菜单组件查询方法和公司访问上下文拦截器改进
This commit is contained in:
@@ -4,12 +4,15 @@ import com.zt.plat.framework.datapermission.core.aop.CompanyDataPermissionIgnore
|
||||
import com.zt.plat.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
|
||||
import com.zt.plat.framework.datapermission.core.aop.DeptDataPermissionIgnoreAspect;
|
||||
import com.zt.plat.framework.datapermission.core.db.DataPermissionRuleHandler;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.aop.MenuDataPermissionAspect;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.handler.MenuDataPermissionHandler;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
|
||||
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@@ -21,6 +24,7 @@ import java.util.List;
|
||||
* @author ZT
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@MapperScan("com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper")
|
||||
public class ZtDataPermissionAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -40,6 +44,21 @@ public class ZtDataPermissionAutoConfiguration {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
||||
// 创建菜单数据权限处理器
|
||||
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
||||
DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
|
||||
// 添加到 interceptor 中,放在部门数据权限之后
|
||||
MyBatisUtils.addInterceptor(interceptor, inner, 1);
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MenuDataPermissionAspect menuDataPermissionAspect() {
|
||||
return new MenuDataPermissionAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
|
||||
return new DataPermissionAnnotationAdvisor();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限注解
|
||||
* 标注在Controller方法上,表示该方法需要应用菜单数据规则
|
||||
*
|
||||
* 参考JeecgBoot的实现方式
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface PermissionData {
|
||||
|
||||
/**
|
||||
* 页面组件路径
|
||||
* 用于匹配菜单表中的component字段
|
||||
* 例如:system/role 对应角色管理菜单
|
||||
*/
|
||||
String pageComponent();
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
boolean enable() default true;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.aop;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.context.MenuDataRuleContextHolder;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.service.MenuDataRuleLoader;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据权限切面
|
||||
* 拦截 @PermissionData 注解的方法,加载并应用菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MenuDataPermissionAspect {
|
||||
|
||||
@Resource
|
||||
private MenuDataRuleLoader menuDataRuleLoader;
|
||||
|
||||
@Around("@annotation(com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData)")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
// 获取方法签名
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
PermissionData annotation = method.getAnnotation(PermissionData.class);
|
||||
|
||||
// 如果未启用,直接执行
|
||||
if (!annotation.enable()) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (userId == null) {
|
||||
log.debug("[MenuDataPermissionAspect][未登录,跳过菜单数据权限]");
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
// 从注解获取pageComponent
|
||||
String pageComponent = annotation.pageComponent();
|
||||
if (pageComponent == null || pageComponent.isEmpty()) {
|
||||
log.warn("[MenuDataPermissionAspect][未指定pageComponent,跳过菜单数据权限]");
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
// 根据pageComponent查询菜单ID
|
||||
Long menuId = menuDataRuleLoader.getMenuIdByPageComponent(pageComponent);
|
||||
if (menuId == null) {
|
||||
log.warn("[MenuDataPermissionAspect][未找到匹配的菜单: {},跳过菜单数据权限]", pageComponent);
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
log.debug("[MenuDataPermissionAspect][pageComponent: {} 对应菜单ID: {}]", pageComponent, menuId);
|
||||
|
||||
// 加载用户的菜单数据规则
|
||||
List<MenuDataRuleDTO> rules = menuDataRuleLoader.getUserMenuDataRules(userId, menuId);
|
||||
|
||||
if (CollUtil.isEmpty(rules)) {
|
||||
log.debug("[MenuDataPermissionAspect][用户 {} 在菜单 {} 下无数据规则]", userId, menuId);
|
||||
} else {
|
||||
log.debug("[MenuDataPermissionAspect][用户 {} 在菜单 {} 下加载了 {} 条数据规则]",
|
||||
userId, menuId, rules.size());
|
||||
// 将规则存入 ThreadLocal
|
||||
MenuDataRuleContextHolder.setRules(rules);
|
||||
}
|
||||
|
||||
// 执行目标方法
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
// 清理 ThreadLocal
|
||||
MenuDataRuleContextHolder.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.config;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.handler.MenuDataPermissionHandler;
|
||||
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 菜单数据权限配置类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan("com.zt.plat.framework.datapermission.core.menudatapermission")
|
||||
public class MenuDataPermissionConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(MybatisPlusInterceptor.class)
|
||||
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
||||
// 创建菜单数据权限处理器
|
||||
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
||||
|
||||
// 创建 DataPermissionInterceptor 拦截器
|
||||
DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
|
||||
|
||||
// 添加到 interceptor 中
|
||||
// 添加在索引1的位置,在部门数据权限之后,但在分页插件之前
|
||||
MyBatisUtils.addInterceptor(interceptor, inner, 1);
|
||||
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.context;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则上下文持有者
|
||||
* 使用 ThreadLocal 存储当前请求的菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public class MenuDataRuleContextHolder {
|
||||
|
||||
private static final ThreadLocal<List<MenuDataRuleDTO>> CONTEXT = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前请求的菜单数据规则
|
||||
*
|
||||
* @param rules 规则列表
|
||||
*/
|
||||
public static void setRules(List<MenuDataRuleDTO> rules) {
|
||||
CONTEXT.set(rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的菜单数据规则
|
||||
*
|
||||
* @return 规则列表
|
||||
*/
|
||||
public static List<MenuDataRuleDTO> getRules() {
|
||||
return CONTEXT.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前请求的菜单数据规则
|
||||
*/
|
||||
public static void clear() {
|
||||
CONTEXT.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据权限 Mapper
|
||||
* 用于查询菜单和菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Mapper
|
||||
public interface MenuDataPermissionMapper {
|
||||
|
||||
/**
|
||||
* 根据页面组件路径获取菜单ID
|
||||
*
|
||||
* @param component 页面组件路径,如:system/role/index
|
||||
* @return 菜单ID,如果未找到返回null
|
||||
*/
|
||||
@Select("SELECT id FROM system_menu WHERE component = #{component} AND deleted = 0 LIMIT 1")
|
||||
Long selectMenuIdByComponent(@Param("component") String component);
|
||||
|
||||
/**
|
||||
* 获取用户在指定菜单下的有效数据规则
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param menuId 菜单ID
|
||||
* @return 数据规则列表
|
||||
*/
|
||||
@TenantIgnore
|
||||
@Select("<script>" +
|
||||
"SELECT mdr.rule_column, mdr.rule_conditions, mdr.rule_value, mdr.status " +
|
||||
"FROM system_menu_data_rule mdr " +
|
||||
"INNER JOIN system_role_menu_data_rule rmdr ON mdr.id = rmdr.data_rule_id " +
|
||||
"INNER JOIN system_user_role ur ON rmdr.role_id = ur.role_id " +
|
||||
"WHERE mdr.menu_id = #{menuId} " +
|
||||
"AND ur.user_id = #{userId} " +
|
||||
"AND mdr.status = 1 " +
|
||||
"AND mdr.deleted = 0 " +
|
||||
"AND rmdr.deleted = 0 " +
|
||||
"AND ur.deleted = 0" +
|
||||
"</script>")
|
||||
List<MenuDataRuleDTO> selectUserMenuDataRules(@Param("userId") Long userId,
|
||||
@Param("menuId") Long menuId);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.util.MenuDataPermissionRule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
|
||||
/**
|
||||
* 菜单数据权限处理器
|
||||
* 基于 MyBatis Plus 的数据权限插件,应用菜单数据规则到 SQL 查询
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Slf4j
|
||||
public class MenuDataPermissionHandler implements MultiDataPermissionHandler {
|
||||
|
||||
@Override
|
||||
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
|
||||
try {
|
||||
// 从 ThreadLocal 获取菜单数据规则并构建 SQL 条件
|
||||
String sqlCondition = MenuDataPermissionRule.buildSqlCondition();
|
||||
|
||||
if (StrUtil.isBlank(sqlCondition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将 SQL 字符串解析为 Expression 对象
|
||||
Expression expression = CCJSqlParserUtil.parseCondExpression(sqlCondition);
|
||||
log.debug("[MenuDataPermissionHandler][表: {}, 添加条件: {}]", table.getName(), sqlCondition);
|
||||
return expression;
|
||||
|
||||
} catch (JSQLParserException e) {
|
||||
log.error("[MenuDataPermissionHandler][解析 SQL 条件失败]", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 菜单数据规则 DTO
|
||||
* 用于在框架层传递菜单数据规则信息,避免依赖业务模块的数据库实体
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Data
|
||||
public class MenuDataRuleDTO {
|
||||
|
||||
/**
|
||||
* 规则字段(数据库列名)
|
||||
*/
|
||||
private String ruleColumn;
|
||||
|
||||
/**
|
||||
* 规则条件(=、>、<、IN、LIKE等)
|
||||
*/
|
||||
private String ruleConditions;
|
||||
|
||||
/**
|
||||
* 规则值(支持变量如#{userId}、#{deptId})
|
||||
*/
|
||||
private String ruleValue;
|
||||
|
||||
/**
|
||||
* 状态(0=禁用 1=启用)
|
||||
*/
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.service;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则加载器接口
|
||||
* 负责加载菜单和菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
public interface MenuDataRuleLoader {
|
||||
|
||||
/**
|
||||
* 根据页面组件路径获取菜单ID
|
||||
*
|
||||
* @param pageComponent 页面组件路径,如:system/role/index
|
||||
* @return 菜单ID,如果未找到返回null
|
||||
*/
|
||||
Long getMenuIdByPageComponent(String pageComponent);
|
||||
|
||||
/**
|
||||
* 获取用户在指定菜单下的数据规则
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param menuId 菜单ID
|
||||
* @return 数据规则列表
|
||||
*/
|
||||
List<MenuDataRuleDTO> getUserMenuDataRules(Long userId, Long menuId);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.service.impl;
|
||||
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper.MenuDataPermissionMapper;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.service.MenuDataRuleLoader;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单数据规则加载器默认实现
|
||||
* 直接从数据库加载菜单数据规则
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MenuDataRuleLoaderImpl implements MenuDataRuleLoader {
|
||||
|
||||
@Resource
|
||||
private MenuDataPermissionMapper menuDataPermissionMapper;
|
||||
|
||||
@Override
|
||||
public Long getMenuIdByPageComponent(String pageComponent) {
|
||||
try {
|
||||
return menuDataPermissionMapper.selectMenuIdByComponent(pageComponent);
|
||||
} catch (Exception e) {
|
||||
log.error("[MenuDataRuleLoaderImpl][根据pageComponent查询菜单ID失败: {}]", pageComponent, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MenuDataRuleDTO> getUserMenuDataRules(Long userId, Long menuId) {
|
||||
try {
|
||||
return menuDataPermissionMapper.selectUserMenuDataRules(userId, menuId);
|
||||
} catch (Exception e) {
|
||||
log.error("[MenuDataRuleLoaderImpl][查询用户菜单数据规则失败: userId={}, menuId={}]",
|
||||
userId, menuId, e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.security.core.LoginUser;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 数据规则变量替换工具类
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Slf4j
|
||||
public class DataRuleVariableUtils {
|
||||
|
||||
private static final Pattern VARIABLE_PATTERN = Pattern.compile("#\\{([^}]+)}");
|
||||
|
||||
/**
|
||||
* 替换规则值中的变量
|
||||
*
|
||||
* @param ruleValue 规则值,如 "#{userId}" 或 "#{deptId}"
|
||||
* @return 替换后的值
|
||||
*/
|
||||
public static String replaceVariables(String ruleValue) {
|
||||
if (StrUtil.isBlank(ruleValue)) {
|
||||
return ruleValue;
|
||||
}
|
||||
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return ruleValue;
|
||||
}
|
||||
|
||||
Matcher matcher = VARIABLE_PATTERN.matcher(ruleValue);
|
||||
StringBuffer result = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String variable = matcher.group(1);
|
||||
String replacement = getVariableValue(variable, loginUser);
|
||||
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变量对应的值
|
||||
*
|
||||
* @param variable 变量名,如 "userId", "deptId"
|
||||
* @param loginUser 当前登录用户
|
||||
* @return 变量值
|
||||
*/
|
||||
private static String getVariableValue(String variable, LoginUser loginUser) {
|
||||
if (loginUser == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (variable) {
|
||||
case "userId":
|
||||
return loginUser.getId() != null ? loginUser.getId().toString() : "";
|
||||
case "username":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_USERNAME, "") : "";
|
||||
case "deptId":
|
||||
return loginUser.getVisitDeptId() != null ?
|
||||
loginUser.getVisitDeptId().toString() : "";
|
||||
case "companyId":
|
||||
return loginUser.getVisitCompanyId() != null ?
|
||||
loginUser.getVisitCompanyId().toString() : "";
|
||||
case "tenantId":
|
||||
return loginUser.getTenantId() != null ?
|
||||
loginUser.getTenantId().toString() : "";
|
||||
case "deptIds":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_DEPT_IDS, "") : "";
|
||||
case "companyIds":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_COMPANY_IDS, "") : "";
|
||||
case "postIds":
|
||||
return loginUser.getInfo() != null ?
|
||||
loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_POST_IDS, "") : "";
|
||||
default:
|
||||
// 未知变量,记录警告并返回空字符串
|
||||
log.warn("[DataRuleVariableUtils][未知的变量: {},请检查数据规则配置]", variable);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.zt.plat.framework.datapermission.core.menudatapermission.util;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.context.MenuDataRuleContextHolder;
|
||||
import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 菜单数据权限规则
|
||||
* 用于构建 SQL WHERE 条件
|
||||
*
|
||||
* @author ZT
|
||||
*/
|
||||
@Slf4j
|
||||
public class MenuDataPermissionRule {
|
||||
|
||||
/**
|
||||
* 构建 SQL WHERE 条件
|
||||
*
|
||||
* @return SQL 条件字符串,如 "dept_id = 1 AND status = 1"
|
||||
*/
|
||||
public static String buildSqlCondition() {
|
||||
List<MenuDataRuleDTO> rules = MenuDataRuleContextHolder.getRules();
|
||||
if (CollUtil.isEmpty(rules)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> conditions = rules.stream()
|
||||
.filter(rule -> rule.getStatus() == 1) // 只处理启用的规则
|
||||
.map(MenuDataPermissionRule::buildSingleCondition)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (CollUtil.isEmpty(conditions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 多个规则用 AND 连接
|
||||
return "(" + String.join(" AND ", conditions) + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建单个规则的 SQL 条件
|
||||
*
|
||||
* @param rule 规则
|
||||
* @return SQL 条件字符串
|
||||
*/
|
||||
private static String buildSingleCondition(MenuDataRuleDTO rule) {
|
||||
String ruleColumn = rule.getRuleColumn();
|
||||
String ruleConditions = rule.getRuleConditions();
|
||||
String ruleValue = rule.getRuleValue();
|
||||
|
||||
// 替换变量
|
||||
String actualValue = DataRuleVariableUtils.replaceVariables(ruleValue);
|
||||
|
||||
if (StrUtil.isBlank(ruleColumn) || StrUtil.isBlank(ruleConditions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理 SQL_RULE 类型(自定义 SQL)
|
||||
if ("SQL_RULE".equals(ruleConditions)) {
|
||||
return actualValue;
|
||||
}
|
||||
|
||||
// 处理 IS_NULL 和 IS_NOT_NULL
|
||||
if ("IS_NULL".equals(ruleConditions)) {
|
||||
return ruleColumn + " IS NULL";
|
||||
}
|
||||
if ("IS_NOT_NULL".equals(ruleConditions)) {
|
||||
return ruleColumn + " IS NOT NULL";
|
||||
}
|
||||
|
||||
// 其他条件需要有值
|
||||
if (StrUtil.isBlank(actualValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建条件
|
||||
switch (ruleConditions) {
|
||||
case "=":
|
||||
return ruleColumn + " = " + formatValue(actualValue);
|
||||
case "!=":
|
||||
return ruleColumn + " != " + formatValue(actualValue);
|
||||
case ">":
|
||||
return ruleColumn + " > " + formatValue(actualValue);
|
||||
case "<":
|
||||
return ruleColumn + " < " + formatValue(actualValue);
|
||||
case ">=":
|
||||
return ruleColumn + " >= " + formatValue(actualValue);
|
||||
case "<=":
|
||||
return ruleColumn + " <= " + formatValue(actualValue);
|
||||
case "IN":
|
||||
return ruleColumn + " IN (" + formatInValues(actualValue) + ")";
|
||||
case "NOT_IN":
|
||||
return ruleColumn + " NOT IN (" + formatInValues(actualValue) + ")";
|
||||
case "LIKE":
|
||||
return ruleColumn + " LIKE '%" + escapeSql(actualValue) + "%'";
|
||||
case "NOT_LIKE":
|
||||
return ruleColumn + " NOT LIKE '%" + escapeSql(actualValue) + "%'";
|
||||
case "BETWEEN":
|
||||
return buildBetweenCondition(ruleColumn, actualValue);
|
||||
case "NOT_BETWEEN":
|
||||
return "NOT " + buildBetweenCondition(ruleColumn, actualValue);
|
||||
default:
|
||||
log.warn("[buildSingleCondition][未知的规则条件: {}]", ruleConditions);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化值(添加引号)
|
||||
* 统一给所有值加引号,数据库会自动处理类型转换
|
||||
*/
|
||||
private static String formatValue(String value) {
|
||||
// 统一添加单引号,避免达梦数据库等对字符串类型字段的类型转换错误
|
||||
return "'" + escapeSql(value) + "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 IN 条件的值
|
||||
*/
|
||||
private static String formatInValues(String value) {
|
||||
String[] values = value.split(",");
|
||||
return java.util.Arrays.stream(values)
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(MenuDataPermissionRule::formatValue)
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 BETWEEN 条件
|
||||
*/
|
||||
private static String buildBetweenCondition(String column, String value) {
|
||||
String[] values = value.split(",");
|
||||
if (values.length != 2) {
|
||||
log.warn("[buildBetweenCondition][BETWEEN 条件需要两个值,用逗号分隔: {}]", value);
|
||||
return null;
|
||||
}
|
||||
return column + " BETWEEN " + formatValue(values[0].trim()) + " AND " + formatValue(values[1].trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 转义,防止 SQL 注入
|
||||
*/
|
||||
private static String escapeSql(String value) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
return value.replace("'", "''");
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@ com.zt.plat.framework.datapermission.config.ZtDataPermissionAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.config.ZtDeptDataPermissionAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.config.ZtBusinessDataPermissionAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.config.ZtDataPermissionRpcAutoConfiguration
|
||||
com.zt.plat.framework.datapermission.core.menudatapermission.config.MenuDataPermissionConfiguration
|
||||
|
||||
@@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
@@ -45,13 +47,31 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor {
|
||||
}
|
||||
|
||||
Long deptId = WebFrameworkUtils.getDeptId(request);
|
||||
// 部门信息同样遵循“请求头 -> 请求属性 -> 登录缓存”的回退顺序
|
||||
// 部门信息同样遵循"请求头 -> 请求属性 -> 登录缓存"的回退顺序
|
||||
if (deptId == null || deptId <= 0L) {
|
||||
Long attrDeptId = resolveLong(request.getAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_ID));
|
||||
if (attrDeptId != null && attrDeptId > 0L) {
|
||||
deptId = attrDeptId;
|
||||
} else if (loginUser != null && loginUser.getVisitDeptId() != null && loginUser.getVisitDeptId() > 0L) {
|
||||
deptId = loginUser.getVisitDeptId();
|
||||
} else if (loginUser != null) {
|
||||
// 如果以上都没有,尝试从用户info中获取第一个部门作为默认值
|
||||
Map<String, String> info = loginUser.getInfo();
|
||||
if (info != null) {
|
||||
String deptIdsStr = info.get(LoginUser.INFO_KEY_DEPT_IDS);
|
||||
if (deptIdsStr != null && !deptIdsStr.isEmpty() && !"[]".equals(deptIdsStr)) {
|
||||
try {
|
||||
// 解析JSON数组,取第一个部门ID
|
||||
deptIdsStr = deptIdsStr.trim();
|
||||
if (deptIdsStr.startsWith("[") && deptIdsStr.length() > 2) {
|
||||
String firstId = deptIdsStr.substring(1, deptIdsStr.indexOf(']')).split(",")[0].trim();
|
||||
deptId = Long.parseLong(firstId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[CompanyVisitContextInterceptor][解析用户默认部门失败: {}]", deptIdsStr, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user