keys, boolean fragment) {
+ UriComponentsBuilder template = UriComponentsBuilder.newInstance();
+ UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
+ URI redirectUri;
+ try {
+ // assume it's encoded to start with (if it came in over the wire)
+ redirectUri = builder.build(true).toUri();
+ } catch (Exception e) {
+ // ... but allow client registrations to contain hard-coded non-encoded values
+ redirectUri = builder.build().toUri();
+ builder = UriComponentsBuilder.fromUri(redirectUri);
+ }
+ template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
+ .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
+
+ if (fragment) {
+ StringBuilder values = new StringBuilder();
+ if (redirectUri.getFragment() != null) {
+ String append = redirectUri.getFragment();
+ values.append(append);
+ }
+ for (String key : query.keySet()) {
+ if (values.length() > 0) {
+ values.append("&");
+ }
+ String name = key;
+ if (keys != null && keys.containsKey(key)) {
+ name = keys.get(key);
+ }
+ values.append(name).append("={").append(key).append("}");
+ }
+ if (values.length() > 0) {
+ template.fragment(values.toString());
+ }
+ UriComponents encoded = template.build().expand(query).encode();
+ builder.fragment(encoded.getFragment());
+ } else {
+ for (String key : query.keySet()) {
+ String name = key;
+ if (keys != null && keys.containsKey(key)) {
+ name = keys.get(key);
+ }
+ template.queryParam(name, "{" + key + "}");
+ }
+ template.fragment(redirectUri.getFragment());
+ UriComponents encoded = template.build().expand(query).encode();
+ builder.query(encoded.getQuery());
+ }
+ return builder.build().toUriString();
+ }
+
+ public static String[] obtainBasicAuthorization(HttpServletRequest request) {
+ String clientId;
+ String clientSecret;
+ // 先从 Header 中获取
+ String authorization = request.getHeader("Authorization");
+ authorization = StrUtil.subAfter(authorization, "Basic ", true);
+ if (StringUtils.hasText(authorization)) {
+ authorization = Base64.decodeStr(authorization);
+ clientId = StrUtil.subBefore(authorization, ":", false);
+ clientSecret = StrUtil.subAfter(authorization, ":", false);
+ // 再从 Param 中获取
+ } else {
+ clientId = request.getParameter("client_id");
+ clientSecret = request.getParameter("client_secret");
+ }
+
+ // 如果两者非空,则返回
+ if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
+ return new String[]{clientId, clientSecret};
+ }
+ return null;
+ }
+
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
new file mode 100644
index 000000000..63732f1b3
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.framework.common.util.io;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import lombok.SneakyThrows;
+
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author 芋道源码
+ */
+public class FileUtils {
+
+ /**
+ * 创建临时文件
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @param data 文件内容
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile(String data) {
+ File file = createTempFile();
+ // 写入内容
+ FileUtil.writeUtf8String(data, file);
+ return file;
+ }
+
+ /**
+ * 创建临时文件
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @param data 文件内容
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile(byte[] data) {
+ File file = createTempFile();
+ // 写入内容
+ FileUtil.writeBytes(data, file);
+ return file;
+ }
+
+ /**
+ * 创建临时文件,无内容
+ * 该文件会在 JVM 退出时,进行删除
+ *
+ * @return 文件
+ */
+ @SneakyThrows
+ public static File createTempFile() {
+ // 创建文件,通过 UUID 保证唯一
+ File file = File.createTempFile(IdUtil.simpleUUID(), null);
+ // 标记 JVM 退出时,自动删除
+ file.deleteOnExit();
+ return file;
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java
new file mode 100644
index 000000000..4a19f479d
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/IoUtils.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.util.io;
+
+import cn.hutool.core.io.IORuntimeException;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.io.InputStream;
+
+/**
+ * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法
+ *
+ * @author 芋道源码
+ */
+public class IoUtils {
+
+ /**
+ * 从流中读取 UTF8 编码的内容
+ *
+ * @param in 输入流
+ * @param isClose 是否关闭
+ * @return 内容
+ * @throws IORuntimeException IO 异常
+ */
+ public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException {
+ return StrUtil.utf8Str(IoUtil.read(in, isClose));
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
new file mode 100644
index 000000000..2a46edf6a
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
@@ -0,0 +1,138 @@
+package cn.iocoder.yudao.framework.common.util.json;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * @author 芋道源码
+ */
+@UtilityClass
+@Slf4j
+public class JsonUtils {
+
+ private static ObjectMapper objectMapper = new ObjectMapper();
+
+ static {
+ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ }
+
+ /**
+ * 初始化 objectMapper 属性
+ *
+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+ *
+ * @param objectMapper ObjectMapper 对象
+ */
+ public static void init(ObjectMapper objectMapper) {
+ JsonUtils.objectMapper = objectMapper;
+ }
+
+ @SneakyThrows
+ public static String toJsonString(Object object) {
+ return objectMapper.writeValueAsString(object);
+ }
+
+ @SneakyThrows
+ public static byte[] toJsonByte(Object object) {
+ return objectMapper.writeValueAsBytes(object);
+ }
+
+
+ public static T parseObject(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(text, clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 将字符串解析成指定类型的对象
+ * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
+ * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
+ *
+ * @param text 字符串
+ * @param clazz 类型
+ * @return 对象
+ */
+ public static T parseObject2(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return null;
+ }
+ return JSONUtil.toBean(text, clazz);
+ }
+
+ public static T parseObject(byte[] bytes, Class clazz) {
+ if (ArrayUtil.isEmpty(bytes)) {
+ return null;
+ }
+ try {
+ return objectMapper.readValue(bytes, clazz);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", bytes, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T parseObject(String text, TypeReference typeReference) {
+ try {
+ return objectMapper.readValue(text, typeReference);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List parseArray(String text, Class clazz) {
+ if (StrUtil.isEmpty(text)) {
+ return new ArrayList<>();
+ }
+ try {
+ return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static JsonNode parseTree(String text) {
+ try {
+ return objectMapper.readTree(text);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static JsonNode parseTree(byte[] text) {
+ try {
+ return objectMapper.readTree(text);
+ } catch (IOException e) {
+ log.error("json parse err,json:{}", text, e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static boolean isJson(String text) {
+ return JSONUtil.isJson(text);
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java
new file mode 100644
index 000000000..81092b656
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/monitor/TracerUtils.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.framework.common.util.monitor;
+
+import org.apache.skywalking.apm.toolkit.trace.TraceContext;
+
+/**
+ * 链路追踪工具类
+ *
+ * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
+ *
+ * @author 芋道源码
+ */
+public class TracerUtils {
+
+ /**
+ * 私有化构造方法
+ */
+ private TracerUtils() {
+ }
+
+ /**
+ * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。
+ * 如果不存在的话为空字符串!!!
+ *
+ * @return 链路追踪编号
+ */
+ public static String getTraceId() {
+ return TraceContext.traceId();
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java
new file mode 100644
index 000000000..822510096
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.framework.common.util.number;
+
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
+ *
+ * @author 芋道源码
+ */
+public class NumberUtils {
+
+ public static Long parseLong(String str) {
+ return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null;
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java
new file mode 100644
index 000000000..a2f241d4c
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.framework.common.util.object;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Object 工具类
+ *
+ * @author 芋道源码
+ */
+public class ObjectUtils {
+
+ /**
+ * 复制对象,并忽略 Id 编号
+ *
+ * @param object 被复制对象
+ * @param consumer 消费者,可以二次编辑被复制对象
+ * @return 复制后的对象
+ */
+ public static T cloneIgnoreId(T object, Consumer consumer) {
+ T result = ObjectUtil.clone(object);
+ // 忽略 id 编号
+ Field field = ReflectUtil.getField(object.getClass(), "id");
+ if (field != null) {
+ ReflectUtil.setFieldValue(result, field, null);
+ }
+ // 二次编辑
+ if (result != null) {
+ consumer.accept(result);
+ }
+ return result;
+ }
+
+ public static > T max(T obj1, T obj2) {
+ if (obj1 == null) {
+ return obj2;
+ }
+ if (obj2 == null) {
+ return obj1;
+ }
+ return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+ }
+
+ public static T defaultIfNull(T... array) {
+ for (T item : array) {
+ if (item != null) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ public static boolean equalsAny(T obj, T... array) {
+ return Arrays.asList(array).contains(obj);
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java
new file mode 100644
index 000000000..72403a9bd
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/PageUtils.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.framework.common.util.object;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * {@link cn.iocoder.yudao.framework.common.pojo.PageParam} 工具类
+ *
+ * @author 芋道源码
+ */
+public class PageUtils {
+
+ public static int getStart(PageParam pageParam) {
+ return (pageParam.getPageNo() - 1) * pageParam.getPageSize();
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java
new file mode 100644
index 000000000..cd8ae6b36
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * 对于工具类的选择,优先查找 Hutool 中有没对应的方法
+ * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分
+ *
+ * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。
+ */
+package cn.iocoder.yudao.framework.common.util;
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java
new file mode 100644
index 000000000..eec306fd6
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.framework.common.util.servlet;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import org.springframework.http.MediaType;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+
+/**
+ * 客户端工具类
+ *
+ * @author 芋道源码
+ */
+public class ServletUtils {
+
+ /**
+ * 返回 JSON 字符串
+ *
+ * @param response 响应
+ * @param object 对象,会序列化成 JSON 字符串
+ */
+ @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
+ public static void writeJSON(HttpServletResponse response, Object object) {
+ String content = JsonUtils.toJsonString(object);
+ ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
+ }
+
+ /**
+ * 返回附件
+ *
+ * @param response 响应
+ * @param filename 文件名
+ * @param content 附件内容
+ * @throws IOException
+ */
+ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
+ // 设置 header 和 contentType
+ response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ // 输出附件
+ IoUtil.write(response.getOutputStream(), false, content);
+ }
+
+ /**
+ * @param request 请求
+ * @return ua
+ */
+ public static String getUserAgent(HttpServletRequest request) {
+ String ua = request.getHeader("User-Agent");
+ return ua != null ? ua : "";
+ }
+
+ /**
+ * 获得请求
+ *
+ * @return HttpServletRequest
+ */
+ public static HttpServletRequest getRequest() {
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ if (!(requestAttributes instanceof ServletRequestAttributes)) {
+ return null;
+ }
+ return ((ServletRequestAttributes) requestAttributes).getRequest();
+ }
+
+ public static String getUserAgent() {
+ HttpServletRequest request = getRequest();
+ if (request == null) {
+ return null;
+ }
+ return getUserAgent(request);
+ }
+
+ public static String getClientIP() {
+ HttpServletRequest request = getRequest();
+ if (request == null) {
+ return null;
+ }
+ return ServletUtil.getClientIP(request);
+ }
+
+ public static boolean isJsonRequest(ServletRequest request) {
+ return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java
new file mode 100644
index 000000000..b71342cb3
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.framework.common.util.spring;
+
+import cn.hutool.core.bean.BeanUtil;
+import org.springframework.aop.framework.AdvisedSupport;
+import org.springframework.aop.framework.AopProxy;
+import org.springframework.aop.support.AopUtils;
+
+/**
+ * Spring AOP 工具类
+ *
+ * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现
+ */
+public class SpringAopUtils {
+
+ /**
+ * 获取代理的目标对象
+ *
+ * @param proxy 代理对象
+ * @return 目标对象
+ */
+ public static Object getTarget(Object proxy) throws Exception {
+ // 不是代理对象
+ if (!AopUtils.isAopProxy(proxy)) {
+ return proxy;
+ }
+ // Jdk 代理
+ if (AopUtils.isJdkDynamicProxy(proxy)) {
+ return getJdkDynamicProxyTargetObject(proxy);
+ }
+ // Cglib 代理
+ return getCglibProxyTargetObject(proxy);
+ }
+
+ private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
+ Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0");
+ AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised");
+ return advisedSupport.getTargetSource().getTarget();
+ }
+
+ private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
+ AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h");
+ AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised");
+ return advisedSupport.getTargetSource().getTarget();
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java
new file mode 100644
index 000000000..659053da7
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java
@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.framework.common.util.spring;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Spring EL 表达式的工具类
+ *
+ * @author mashu
+ */
+public class SpringExpressionUtils {
+
+ private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
+ private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+ private SpringExpressionUtils() {
+ }
+
+ /**
+ * 从切面中,单个解析 EL 表达式的结果
+ *
+ * @param joinPoint 切面点
+ * @param expressionString EL 表达式数组
+ * @return 执行界面
+ */
+ public static Object parseExpression(ProceedingJoinPoint joinPoint, String expressionString) {
+ Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString));
+ return result.get(expressionString);
+ }
+
+ /**
+ * 从切面中,批量解析 EL 表达式的结果
+ *
+ * @param joinPoint 切面点
+ * @param expressionStrings EL 表达式数组
+ * @return 结果,key 为表达式,value 为对应值
+ */
+ public static Map parseExpressions(ProceedingJoinPoint joinPoint, List expressionStrings) {
+ // 如果为空,则不进行解析
+ if (CollUtil.isEmpty(expressionStrings)) {
+ return MapUtil.newHashMap();
+ }
+
+ // 第一步,构建解析的上下文 EvaluationContext
+ // 通过 joinPoint 获取被注解方法
+ MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+ Method method = methodSignature.getMethod();
+ // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
+ String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
+ // Spring 的表达式上下文对象
+ EvaluationContext context = new StandardEvaluationContext();
+ // 给上下文赋值
+ if (ArrayUtil.isNotEmpty(paramNames)) {
+ Object[] args = joinPoint.getArgs();
+ for (int i = 0; i < paramNames.length; i++) {
+ context.setVariable(paramNames[i], args[i]);
+ }
+ }
+
+ // 第二步,逐个参数解析
+ Map result = MapUtil.newHashMap(expressionStrings.size(), true);
+ expressionStrings.forEach(key -> {
+ Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context);
+ result.put(key, value);
+ });
+ return result;
+ }
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java
new file mode 100644
index 000000000..3f79ceea6
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.framework.common.util.string;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.util.Collection;
+
+/**
+ * 字符串工具类
+ *
+ * @author 芋道源码
+ */
+public class StrUtils {
+
+ public static String maxLength(CharSequence str, int maxLength) {
+ return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
+ }
+
+ /**
+ * 给定字符串是否以任何一个字符串开始
+ * 给定字符串和数组为空都返回 false
+ *
+ * @param str 给定字符串
+ * @param prefixes 需要检测的开始字符串
+ * @since 3.0.6
+ */
+ public static boolean startWithAny(String str, Collection prefixes) {
+ if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
+ return false;
+ }
+
+ for (CharSequence suffix : prefixes) {
+ if (StrUtil.startWith(str, suffix, false)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
new file mode 100644
index 000000000..d9a01747d
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.framework.common.util.validation;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.util.StringUtils;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 校验工具类
+ *
+ * @author 芋道源码
+ */
+public class ValidationUtils {
+
+ private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
+
+ private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
+
+ public static boolean isMobile(String mobile) {
+ if (StrUtil.length(mobile) != 11) {
+ return false;
+ }
+ // TODO 芋艿,后面完善手机校验
+ return true;
+ }
+
+ public static boolean isURL(String url) {
+ return StringUtils.hasText(url)
+ && PATTERN_URL.matcher(url).matches();
+ }
+
+ public static boolean isXmlNCName(String str) {
+ return StringUtils.hasText(str)
+ && PATTERN_XML_NCNAME.matcher(str).matches();
+ }
+
+ public static void validate(Validator validator, Object object, Class>... groups) {
+ Set> constraintViolations = validator.validate(object, groups);
+ if (CollUtil.isNotEmpty(constraintViolations)) {
+ throw new ConstraintViolationException(constraintViolations);
+ }
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java
new file mode 100644
index 000000000..c2d56b0c5
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnum.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+ ElementType.METHOD,
+ ElementType.FIELD,
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.PARAMETER,
+ ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = InEnumValidator.class
+)
+public @interface InEnum {
+
+ /**
+ * @return 实现 EnumValuable 接口的
+ */
+ Class extends IntArrayValuable> value();
+
+ String message() default "必须在指定范围 {value}";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java
new file mode 100644
index 000000000..6cd08caa2
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/InEnumValidator.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class InEnumValidator implements ConstraintValidator {
+
+ private List values;
+
+ @Override
+ public void initialize(InEnum annotation) {
+ IntArrayValuable[] values = annotation.value().getEnumConstants();
+ if (values.length == 0) {
+ this.values = Collections.emptyList();
+ } else {
+ this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList());
+ }
+ }
+
+ @Override
+ public boolean isValid(Integer value, ConstraintValidatorContext context) {
+ // 为空时,默认不校验,即认为通过
+ if (value == null) {
+ return true;
+ }
+ // 校验通过
+ if (values.contains(value)) {
+ return true;
+ }
+ // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
+ context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
+ context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
+ .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
+ return false;
+ }
+
+}
+
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java
new file mode 100644
index 000000000..4c7cb10ba
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+ ElementType.METHOD,
+ ElementType.FIELD,
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.PARAMETER,
+ ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = MobileValidator.class
+)
+public @interface Mobile {
+
+ String message() default "手机号格式不正确";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java
new file mode 100644
index 000000000..0bddfcb21
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class MobileValidator implements ConstraintValidator {
+
+ @Override
+ public void initialize(Mobile annotation) {
+ }
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ // 如果手机号为空,默认不校验,即校验通过
+ if (StrUtil.isEmpty(value)) {
+ return true;
+ }
+ // 校验手机
+ return ValidationUtils.isMobile(value);
+ }
+
+}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java
new file mode 100644
index 000000000..aa95c691a
--- /dev/null
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 使用 Hibernate Validator 实现参数校验
+ */
+package cn.iocoder.yudao.framework.common.validation;
diff --git a/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md b/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md
new file mode 100644
index 000000000..b2c2cf8f5
--- /dev/null
+++ b/yudao-framework/yudao-common/《芋道 Spring Boot 参数校验 Validation 入门》.md
@@ -0,0 +1 @@
+
diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/pom.xml b/yudao-framework/yudao-spring-boot-starter-activiti/pom.xml
new file mode 100644
index 000000000..3d5cf0919
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-activiti/pom.xml
@@ -0,0 +1,48 @@
+
+
+
+ cn.iocoder.cloud
+ yudao-framework
+ ${revision}
+
+ 4.0.0
+ yudao-spring-boot-starter-activiti
+ jar
+
+ ${project.artifactId}
+ Activiti 拓展
+ https://github.com/YunaiV/ruoyi-vue-pro
+
+
+
+ cn.iocoder.cloud
+ yudao-common
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-security
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mybatis
+
+
+
+
+ org.activiti
+ activiti-spring-boot-starter
+
+
+ org.activiti
+ activiti-image-generator
+
+
+
+
+
diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java
new file mode 100644
index 000000000..ad9780a1f
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/config/YudaoActivitiConfiguration.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.framework.activiti.config;
+
+import cn.iocoder.yudao.framework.activiti.core.web.ActivitiWebFilter;
+import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
+import org.activiti.image.ProcessDiagramGenerator;
+import org.activiti.image.impl.DefaultProcessDiagramGenerator;
+import org.activiti.spring.SpringProcessEngineConfiguration;
+import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.transaction.TransactionFactory;
+import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.PlatformTransactionManager;
+
+@Configuration
+public class YudaoActivitiConfiguration {
+
+ /**
+ * Activiti 流程图的生成器。目前管理后台的流程图 svg,通过它绘制生成。
+ */
+ @Bean
+ public ProcessDiagramGenerator processDiagramGenerator() {
+ return new DefaultProcessDiagramGenerator();
+ }
+
+ @Bean
+ public FilterRegistrationBean activitiWebFilter() {
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
+ registrationBean.setFilter(new ActivitiWebFilter());
+ registrationBean.setOrder(WebFilterOrderEnum.ACTIVITI_FILTER);
+ return registrationBean;
+ }
+
+ /**
+ * ProcessEngineConfigurationConfigurer 实现类,设置事务管理器,保证 ACT_ 表和自己的表的事务一致性
+ */
+ @Bean
+ public ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer(
+ PlatformTransactionManager platformTransactionManager) {
+ return processEngineConfiguration -> processEngineConfiguration.setTransactionManager(platformTransactionManager);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/util/ActivitiUtils.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/util/ActivitiUtils.java
new file mode 100644
index 000000000..13ecf7957
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/util/ActivitiUtils.java
@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.framework.activiti.core.util;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import com.alibaba.ttl.TransmittableThreadLocal;
+import org.activiti.bpmn.converter.BpmnXMLConverter;
+import org.activiti.bpmn.model.BpmnModel;
+import org.activiti.bpmn.model.FlowElement;
+import org.activiti.bpmn.model.Process;
+import org.activiti.engine.impl.identity.Authentication;
+import org.activiti.engine.impl.util.io.BytesStreamSource;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Activiti 工具类
+ *
+ * @author 芋道源码
+ */
+public class ActivitiUtils {
+
+ static {
+ setAuthenticationThreadLocal();
+ }
+
+ // ========== Authentication 相关 ==========
+
+ /**
+ * 反射修改 Authentication 的 authenticatedUserIdThreadLocal 静态变量,使用 TTL 线程变量
+ * 目的:保证 @Async 等异步执行时,变量丢失的问题
+ */
+ private static void setAuthenticationThreadLocal() {
+ ReflectUtil.setFieldValue(Authentication.class, "authenticatedUserIdThreadLocal",
+ new TransmittableThreadLocal());
+ }
+
+ public static void setAuthenticatedUserId(Long userId) {
+ Authentication.setAuthenticatedUserId(String.valueOf(userId));
+ }
+
+ public static void clearAuthenticatedUserId() {
+ Authentication.setAuthenticatedUserId(null);
+ }
+
+ public static boolean equals(String userIdStr, Long userId) {
+ return Objects.equals(userId, NumberUtils.parseLong(userIdStr));
+ }
+
+ // ========== BPMN XML 相关 ==========
+
+ /**
+ * 构建对应的 BPMN Model
+ *
+ * @param bpmnBytes 原始的 BPMN XML 字节数组
+ * @return BPMN Model
+ */
+ public static BpmnModel buildBpmnModel(byte[] bpmnBytes) {
+ // 转换成 BpmnModel 对象
+ BpmnXMLConverter converter = new BpmnXMLConverter();
+ return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true);
+ }
+
+ /**
+ * 获得 BPMN 流程中,指定的元素们
+ *
+ * @param model
+ * @param clazz 指定元素。例如说,{@link org.activiti.bpmn.model.UserTask}、{@link org.activiti.bpmn.model.Gateway} 等等
+ * @return 元素们
+ */
+ public static List getBpmnModelElements(BpmnModel model, Class clazz) {
+ List result = new ArrayList<>();
+ model.getProcesses().forEach(process -> {
+ process.getFlowElements().forEach(flowElement -> {
+ if (flowElement.getClass().isAssignableFrom(clazz)) {
+ result.add((T) flowElement);
+ }
+ });
+ });
+ return result;
+ }
+
+ public static String getBpmnXml(BpmnModel model) {
+ if (model == null) {
+ return null;
+ }
+ return StrUtil.utf8Str(getBpmnBytes(model));
+ }
+
+ public static byte[] getBpmnBytes(BpmnModel model) {
+ if (model == null) {
+ return new byte[0];
+ }
+ BpmnXMLConverter converter = new BpmnXMLConverter();
+ return converter.convertToXML(model);
+ }
+
+ public static boolean equals(BpmnModel oldModel, BpmnModel newModel) {
+ // 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较
+ return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel));
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/web/ActivitiWebFilter.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/web/ActivitiWebFilter.java
new file mode 100644
index 000000000..cb190f1fa
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/core/web/ActivitiWebFilter.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.framework.activiti.core.web;
+
+import cn.iocoder.yudao.framework.activiti.core.util.ActivitiUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Activiti Web 过滤器,将 userId 设置到 {@link org.activiti.engine.impl.identity.Authentication} 中
+ *
+ * @author 芋道源码
+ */
+public class ActivitiWebFilter extends OncePerRequestFilter {
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException {
+ try {
+ // 设置工作流的用户
+ Long userId = SecurityFrameworkUtils.getLoginUserId();
+ if (userId != null) {
+ ActivitiUtils.setAuthenticatedUserId(userId);
+ }
+ // 过滤
+ chain.doFilter(request, response);
+ } finally {
+ // 清理
+ ActivitiUtils.clearAuthenticatedUserId();
+ }
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java
new file mode 100644
index 000000000..c49d90f93
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/java/cn/iocoder/yudao/framework/activiti/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.yudao.framework.activiti;
diff --git a/yudao-framework/yudao-spring-boot-starter-activiti/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..2f091cd70
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-activiti/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ cn.iocoder.yudao.framework.activiti.config.YudaoActivitiConfiguration
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml
new file mode 100644
index 000000000..c80191e55
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml
@@ -0,0 +1,52 @@
+
+
+
+ yudao-framework
+ cn.iocoder.cloud
+ ${revision}
+
+ 4.0.0
+ yudao-spring-boot-starter-biz-data-permission
+ jar
+
+ ${project.artifactId}
+ 数据权限
+ https://github.com/YunaiV/ruoyi-vue-pro
+
+
+
+ cn.iocoder.cloud
+ yudao-common
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-security
+ true
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mybatis
+
+
+
+
+ cn.iocoder.cloud
+ yudao-module-system-api
+ ${revision}
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-test
+ test
+
+
+
+
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java
new file mode 100644
index 000000000..609e8a34b
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.framework.datapermission.config;
+
+import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
+import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionDatabaseInterceptor;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * 数据全新啊的自动配置类
+ *
+ * @author 芋道源码
+ */
+@Configuration
+public class YudaoDataPermissionAutoConfiguration {
+
+ @Bean
+ public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) {
+ return new DataPermissionRuleFactoryImpl(rules);
+ }
+
+ @Bean
+ public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor,
+ List rules) {
+ // 创建 DataPermissionDatabaseInterceptor 拦截器
+ DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules);
+ DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory);
+ // 添加到 interceptor 中
+ // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
+ MyBatisUtils.addInterceptor(interceptor, inner, 0);
+ return inner;
+ }
+
+ @Bean
+ public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
+ return new DataPermissionAnnotationAdvisor();
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java
new file mode 100644
index 000000000..92da456ee
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.datapermission.config;
+
+import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * 基于部门的数据权限 AutoConfiguration
+ *
+ * @author 芋道源码
+ */
+@Configuration
+@ConditionalOnClass(LoginUser.class)
+@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
+public class YudaoDeptDataPermissionAutoConfiguration {
+
+ @Bean
+ public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
+ List customizers) {
+ // 创建 DeptDataPermissionRule 对象
+ DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
+ // 补全表配置
+ customizers.forEach(customizer -> customizer.customize(rule));
+ return rule;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java
new file mode 100644
index 000000000..4e8a92192
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.datapermission.core.annotation;
+
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限注解
+ * 可声明在类或者方法上,标识使用的数据权限规则
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataPermission {
+
+ /**
+ * 当前类或方法是否开启数据权限
+ * 即使不添加 @DataPermission 注解,默认是开启状态
+ * 可通过设置 enable 为 false 禁用
+ */
+ boolean enable() default true;
+
+ /**
+ * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()}
+ */
+ Class extends DataPermissionRule>[] includeRules() default {};
+
+ /**
+ * 排除的数据权限规则数组,优先级最低
+ */
+ Class extends DataPermissionRule>[] excludeRules() default {};
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java
new file mode 100644
index 000000000..03d212ca8
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import org.aopalliance.aop.Advice;
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.AbstractPointcutAdvisor;
+import org.springframework.aop.support.ComposablePointcut;
+import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
+
+/**
+ * {@link cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类
+ *
+ * @author 芋道源码
+ */
+@Getter
+@EqualsAndHashCode(callSuper = true)
+public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
+
+ private final Advice advice;
+
+ private final Pointcut pointcut;
+
+ public DataPermissionAnnotationAdvisor() {
+ this.advice = new DataPermissionAnnotationInterceptor();
+ this.pointcut = this.buildPointcut();
+ }
+
+ protected Pointcut buildPointcut() {
+ Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
+ Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
+ return new ComposablePointcut(classPointcut).union(methodPointcut);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java
new file mode 100644
index 000000000..48a03541f
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import lombok.Getter;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.core.MethodClassKey;
+import org.springframework.core.annotation.AnnotationUtils;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link DataPermission} 注解的拦截器
+ * 1. 在执行方法前,将 @DataPermission 注解入栈
+ * 2. 在执行方法后,将 @DataPermission 注解出栈
+ *
+ * @author 芋道源码
+ */
+@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
+public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
+
+ /**
+ * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位
+ */
+ static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
+
+ @Getter
+ private final Map dataPermissionCache = new ConcurrentHashMap<>();
+
+ @Override
+ public Object invoke(MethodInvocation methodInvocation) throws Throwable {
+ // 入栈
+ DataPermission dataPermission = this.findAnnotation(methodInvocation);
+ if (dataPermission != null) {
+ DataPermissionContextHolder.add(dataPermission);
+ }
+ try {
+ // 执行逻辑
+ return methodInvocation.proceed();
+ } finally {
+ // 出栈
+ if (dataPermission != null) {
+ DataPermissionContextHolder.remove();
+ }
+ }
+ }
+
+ private DataPermission findAnnotation(MethodInvocation methodInvocation) {
+ // 1. 从缓存中获取
+ Method method = methodInvocation.getMethod();
+ Object targetObject = methodInvocation.getThis();
+ Class> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
+ MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
+ DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
+ if (dataPermission != null) {
+ return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
+ }
+
+ // 2.1 从方法中获取
+ dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
+ // 2.2 从类上获取
+ if (dataPermission == null) {
+ dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
+ }
+ // 2.3 添加到缓存中
+ dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
+ return dataPermission;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java
new file mode 100644
index 000000000..9705d88a9
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * {@link DataPermission} 注解的 Context 上下文
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionContextHolder {
+
+ /**
+ * 使用 List 的原因,可能存在方法的嵌套调用
+ */
+ private static final ThreadLocal> DATA_PERMISSIONS =
+ TransmittableThreadLocal.withInitial(LinkedList::new);
+
+ /**
+ * 获得当前的 DataPermission 注解
+ *
+ * @return DataPermission 注解
+ */
+ public static DataPermission get() {
+ return DATA_PERMISSIONS.get().peekLast();
+ }
+
+ /**
+ * 入栈 DataPermission 注解
+ *
+ * @param dataPermission DataPermission 注解
+ */
+ public static void add(DataPermission dataPermission) {
+ DATA_PERMISSIONS.get().addLast(dataPermission);
+ }
+
+ /**
+ * 出栈 DataPermission 注解
+ *
+ * @return DataPermission 注解
+ */
+ public static DataPermission remove() {
+ DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
+ // 无元素时,清空 ThreadLocal
+ if (DATA_PERMISSIONS.get().isEmpty()) {
+ DATA_PERMISSIONS.remove();
+ }
+ return dataPermission;
+ }
+
+ /**
+ * 获得所有 DataPermission
+ *
+ * @return DataPermission 队列
+ */
+ public static List getAll() {
+ return DATA_PERMISSIONS.get();
+ }
+
+ /**
+ * 清空上下文
+ *
+ * 目前仅仅用于单测
+ */
+ public static void clear() {
+ DATA_PERMISSIONS.remove();
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java
new file mode 100644
index 000000000..a9a4d24d4
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java
@@ -0,0 +1,508 @@
+package cn.iocoder.yudao.framework.datapermission.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import com.alibaba.ttl.TransmittableThreadLocal;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import net.sf.jsqlparser.expression.*;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.expression.operators.relational.ItemsList;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.select.*;
+import net.sf.jsqlparser.statement.update.Update;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.SqlCommandType;
+import org.apache.ibatis.session.ResultHandler;
+import org.apache.ibatis.session.RowBounds;
+
+import java.sql.Connection;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现
+ * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, Table)} 方法
+ *
+ * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。
+ * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更!
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor {
+
+ private final DataPermissionRuleFactory ruleFactory;
+
+ @Getter
+ private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
+
+ @Override // SELECT 场景
+ public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
+ RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+ // 获得 Mapper 对应的数据权限的规则
+ List rules = ruleFactory.getDataPermissionRule(ms.getId());
+ if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
+ return;
+ }
+
+ PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
+ try {
+ // 初始化上下文
+ ContextHolder.init(rules);
+ // 处理 SQL
+ mpBs.sql(parserSingle(mpBs.sql(), null));
+ } finally {
+ addMappedStatementCache(ms);
+ ContextHolder.clear();
+ }
+ }
+
+ @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景
+ public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
+ PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
+ MappedStatement ms = mpSh.mappedStatement();
+ SqlCommandType sct = ms.getSqlCommandType();
+ if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
+ // 获得 Mapper 对应的数据权限的规则
+ List rules = ruleFactory.getDataPermissionRule(ms.getId());
+ if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
+ return;
+ }
+
+ PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
+ try {
+ // 初始化上下文
+ ContextHolder.init(rules);
+ // 处理 SQL
+ mpBs.sql(parserMulti(mpBs.sql(), null));
+ } finally {
+ addMappedStatementCache(ms);
+ ContextHolder.clear();
+ }
+ }
+ }
+
+ @Override
+ protected void processSelect(Select select, int index, String sql, Object obj) {
+ processSelectBody(select.getSelectBody());
+ List withItemsList = select.getWithItemsList();
+ if (!CollectionUtils.isEmpty(withItemsList)) {
+ withItemsList.forEach(this::processSelectBody);
+ }
+ }
+
+ protected void processSelectBody(SelectBody selectBody) {
+ if (selectBody == null) {
+ return;
+ }
+ if (selectBody instanceof PlainSelect) {
+ processPlainSelect((PlainSelect) selectBody);
+ } else if (selectBody instanceof WithItem) {
+ WithItem withItem = (WithItem) selectBody;
+ processSelectBody(withItem.getSubSelect().getSelectBody());
+ } else {
+ SetOperationList operationList = (SetOperationList) selectBody;
+ List selectBodys = operationList.getSelects();
+ if (CollectionUtils.isNotEmpty(selectBodys)) {
+ selectBodys.forEach(this::processSelectBody);
+ }
+ }
+ }
+
+ /**
+ * update 语句处理
+ */
+ @Override
+ protected void processUpdate(Update update, int index, String sql, Object obj) {
+ final Table table = update.getTable();
+ update.setWhere(this.builderExpression(update.getWhere(), table));
+ }
+
+ /**
+ * delete 语句处理
+ */
+ @Override
+ protected void processDelete(Delete delete, int index, String sql, Object obj) {
+ delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
+ }
+
+ /**
+ * 处理 PlainSelect
+ */
+ protected void processPlainSelect(PlainSelect plainSelect) {
+ FromItem fromItem = plainSelect.getFromItem();
+ Expression where = plainSelect.getWhere();
+ processWhereSubSelect(where);
+ if (fromItem instanceof Table) {
+ Table fromTable = (Table) fromItem;
+ plainSelect.setWhere(builderExpression(where, fromTable));
+ } else {
+ processFromItem(fromItem);
+ }
+ //#3087 github
+ List selectItems = plainSelect.getSelectItems();
+ if (CollectionUtils.isNotEmpty(selectItems)) {
+ selectItems.forEach(this::processSelectItem);
+ }
+ List joins = plainSelect.getJoins();
+ if (CollectionUtils.isNotEmpty(joins)) {
+ processJoins(joins);
+ }
+ }
+
+ /**
+ * 处理where条件内的子查询
+ *
+ * 支持如下:
+ * 1. in
+ * 2. =
+ * 3. >
+ * 4. <
+ * 5. >=
+ * 6. <=
+ * 7. <>
+ * 8. EXISTS
+ * 9. NOT EXISTS
+ *
+ * 前提条件:
+ * 1. 子查询必须放在小括号中
+ * 2. 子查询一般放在比较操作符的右边
+ *
+ * @param where where 条件
+ */
+ protected void processWhereSubSelect(Expression where) {
+ if (where == null) {
+ return;
+ }
+ if (where instanceof FromItem) {
+ processFromItem((FromItem) where);
+ return;
+ }
+ if (where.toString().indexOf("SELECT") > 0) {
+ // 有子查询
+ if (where instanceof BinaryExpression) {
+ // 比较符号 , and , or , 等等
+ BinaryExpression expression = (BinaryExpression) where;
+ processWhereSubSelect(expression.getLeftExpression());
+ processWhereSubSelect(expression.getRightExpression());
+ } else if (where instanceof InExpression) {
+ // in
+ InExpression expression = (InExpression) where;
+ ItemsList itemsList = expression.getRightItemsList();
+ if (itemsList instanceof SubSelect) {
+ processSelectBody(((SubSelect) itemsList).getSelectBody());
+ }
+ } else if (where instanceof ExistsExpression) {
+ // exists
+ ExistsExpression expression = (ExistsExpression) where;
+ processWhereSubSelect(expression.getRightExpression());
+ } else if (where instanceof NotExpression) {
+ // not exists
+ NotExpression expression = (NotExpression) where;
+ processWhereSubSelect(expression.getExpression());
+ } else if (where instanceof Parenthesis) {
+ Parenthesis expression = (Parenthesis) where;
+ processWhereSubSelect(expression.getExpression());
+ }
+ }
+ }
+
+ protected void processSelectItem(SelectItem selectItem) {
+ if (selectItem instanceof SelectExpressionItem) {
+ SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
+ if (selectExpressionItem.getExpression() instanceof SubSelect) {
+ processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
+ } else if (selectExpressionItem.getExpression() instanceof Function) {
+ processFunction((Function) selectExpressionItem.getExpression());
+ }
+ }
+ }
+
+ /**
+ * 处理函数
+ *
支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)
+ *
fixed gitee pulls/141
+ *
+ * @param function 函数
+ */
+ protected void processFunction(Function function) {
+ ExpressionList parameters = function.getParameters();
+ if (parameters != null) {
+ parameters.getExpressions().forEach(expression -> {
+ if (expression instanceof SubSelect) {
+ processSelectBody(((SubSelect) expression).getSelectBody());
+ } else if (expression instanceof Function) {
+ processFunction((Function) expression);
+ }
+ });
+ }
+ }
+
+ /**
+ * 处理子查询等
+ */
+ protected void processFromItem(FromItem fromItem) {
+ if (fromItem instanceof SubJoin) {
+ SubJoin subJoin = (SubJoin) fromItem;
+ if (subJoin.getJoinList() != null) {
+ processJoins(subJoin.getJoinList());
+ }
+ if (subJoin.getLeft() != null) {
+ processFromItem(subJoin.getLeft());
+ }
+ } else if (fromItem instanceof SubSelect) {
+ SubSelect subSelect = (SubSelect) fromItem;
+ if (subSelect.getSelectBody() != null) {
+ processSelectBody(subSelect.getSelectBody());
+ }
+ } else if (fromItem instanceof ValuesList) {
+ logger.debug("Perform a subquery, if you do not give us feedback");
+ } else if (fromItem instanceof LateralSubSelect) {
+ LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
+ if (lateralSubSelect.getSubSelect() != null) {
+ SubSelect subSelect = lateralSubSelect.getSubSelect();
+ if (subSelect.getSelectBody() != null) {
+ processSelectBody(subSelect.getSelectBody());
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理 joins
+ *
+ * @param joins join 集合
+ */
+ private void processJoins(List joins) {
+ //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
+ Deque tables = new LinkedList<>();
+ for (Join join : joins) {
+ // 处理 on 表达式
+ FromItem fromItem = join.getRightItem();
+ if (fromItem instanceof Table) {
+ Table fromTable = (Table) fromItem;
+ // 获取 join 尾缀的 on 表达式列表
+ Collection originOnExpressions = join.getOnExpressions();
+ // 正常 join on 表达式只有一个,立刻处理
+ if (originOnExpressions.size() == 1) {
+ processJoin(join);
+ continue;
+ }
+ tables.push(fromTable);
+ // 尾缀多个 on 表达式的时候统一处理
+ if (originOnExpressions.size() > 1) {
+ Collection onExpressions = new LinkedList<>();
+ for (Expression originOnExpression : originOnExpressions) {
+ Table currentTable = tables.poll();
+ onExpressions.add(builderExpression(originOnExpression, currentTable));
+ }
+ join.setOnExpressions(onExpressions);
+ }
+ } else {
+ // 处理右边连接的子表达式
+ processFromItem(fromItem);
+ }
+ }
+ }
+
+ /**
+ * 处理联接语句
+ */
+ protected void processJoin(Join join) {
+ if (join.getRightItem() instanceof Table) {
+ Table fromTable = (Table) join.getRightItem();
+ Expression originOnExpression = CollUtil.getFirst(join.getOnExpressions());
+ originOnExpression = builderExpression(originOnExpression, fromTable);
+ join.setOnExpressions(CollUtil.newArrayList(originOnExpression));
+ }
+ }
+
+ /**
+ * 处理条件
+ */
+ protected Expression builderExpression(Expression currentExpression, Table table) {
+ // 获得 Table 对应的数据权限条件
+ Expression equalsTo = buildDataPermissionExpression(table);
+ if (equalsTo == null) { // 如果没条件,则返回 currentExpression 默认
+ return currentExpression;
+ }
+
+ // 表达式为空,则直接返回 equalsTo
+ if (currentExpression == null) {
+ return equalsTo;
+ }
+ // 如果表达式为 Or,则需要 (currentExpression) AND equalsTo
+ if (currentExpression instanceof OrExpression) {
+ return new AndExpression(new Parenthesis(currentExpression), equalsTo);
+ }
+ // 如果表达式为 And,则直接返回 currentExpression AND equalsTo
+ return new AndExpression(currentExpression, equalsTo);
+ }
+
+ /**
+ * 构建指定表的数据权限的 Expression 过滤条件
+ *
+ * @param table 表
+ * @return Expression 过滤条件
+ */
+ private Expression buildDataPermissionExpression(Table table) {
+ // 生成条件
+ Expression allExpression = null;
+ for (DataPermissionRule rule : ContextHolder.getRules()) {
+ // 判断表名是否匹配
+ if (!rule.getTableNames().contains(table.getName())) {
+ continue;
+ }
+ // 如果有匹配的规则,说明可重写。
+ // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
+ // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
+ ContextHolder.setRewrite(true);
+
+ // 单条规则的条件
+ String tableName = MyBatisUtils.getTableName(table);
+ Expression oneExpress = rule.getExpression(tableName, table.getAlias());
+ // 拼接到 allExpression 中
+ allExpression = allExpression == null ? oneExpress
+ : new AndExpression(allExpression, oneExpress);
+ }
+
+ return allExpression;
+ }
+
+ /**
+ * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中
+ *
+ * @param ms MappedStatement
+ */
+ private void addMappedStatementCache(MappedStatement ms) {
+ if (ContextHolder.getRewrite()) {
+ return;
+ }
+ // 无重写,进行添加
+ mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules());
+ }
+
+ /**
+ * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则
+ *
+ * @author 芋道源码
+ */
+ static final class ContextHolder {
+
+ /**
+ * 该 {@link MappedStatement} 对应的规则
+ */
+ private static final ThreadLocal> RULES = new TransmittableThreadLocal<>();
+ /**
+ * SQL 是否进行重写
+ */
+ private static final ThreadLocal REWRITE = new TransmittableThreadLocal<>();
+
+ public static void init(List rules) {
+ RULES.set(rules);
+ REWRITE.set(false);
+ }
+
+ public static void clear() {
+ RULES.remove();
+ REWRITE.remove();
+ }
+
+ public static boolean getRewrite() {
+ return REWRITE.get();
+ }
+
+ public static void setRewrite(boolean rewrite) {
+ REWRITE.set(rewrite);
+ }
+
+ public static List getRules() {
+ return RULES.get();
+ }
+
+ }
+
+ /**
+ * {@link MappedStatement} 缓存
+ * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效
+ * 如果无效,则可以避免 SQL 的解析,加快速度
+ *
+ * @author 芋道源码
+ */
+ static final class MappedStatementCache {
+
+ /**
+ * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存
+ *
+ * value:{@link MappedStatement#getId()} 编号
+ */
+ @Getter
+ private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>();
+
+ /**
+ * 判断是否无需重写
+ * ps:虽然有点中文式英语,但是容易读懂即可
+ *
+ * @param ms MappedStatement
+ * @param rules 数据权限规则数组
+ * @return 是否无需重写
+ */
+ public boolean noRewritable(MappedStatement ms, List rules) {
+ // 如果规则为空,说明无需重写
+ if (CollUtil.isEmpty(rules)) {
+ return true;
+ }
+ // 任一规则不在 noRewritableMap 中,则说明可能需要重写
+ for (DataPermissionRule rule : rules) {
+ Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
+ if (!CollUtil.contains(mappedStatementIds, ms.getId())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 添加无需重写的 MappedStatement
+ *
+ * @param ms MappedStatement
+ * @param rules 数据权限规则数组
+ */
+ public void addNoRewritable(MappedStatement ms, List rules) {
+ for (DataPermissionRule rule : rules) {
+ Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
+ if (CollUtil.isNotEmpty(mappedStatementIds)) {
+ mappedStatementIds.add(ms.getId());
+ } else {
+ noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId()));
+ }
+ }
+ }
+
+ /**
+ * 清空缓存
+ * 目前主要提供给单元测试
+ */
+ public void clear() {
+ noRewritableMappedStatements.clear();
+ }
+
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java
new file mode 100644
index 000000000..2bccde85f
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule;
+
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+
+import java.util.Set;
+
+/**
+ * 数据权限规则接口
+ * 通过实现接口,自定义数据规则。例如说,
+ *
+ * @author 芋道源码
+ */
+public interface DataPermissionRule {
+
+ /**
+ * 返回需要生效的表名数组
+ * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
+ *
+ * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得
+ *
+ * @return 表名数组
+ */
+ Set getTableNames();
+
+ /**
+ * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
+ *
+ * @param tableName 表名
+ * @param tableAlias 别名,可能为空
+ * @return 过滤条件 Expression 表达式
+ */
+ Expression getExpression(String tableName, Alias tableAlias);
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java
new file mode 100644
index 000000000..166dfea6c
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule;
+
+import java.util.List;
+
+/**
+ * {@link DataPermissionRule} 工厂接口
+ * 作为 {@link DataPermissionRule} 的容器,提供管理能力
+ *
+ * @author 芋道源码
+ */
+public interface DataPermissionRuleFactory {
+
+ /**
+ * 获得所有数据权限规则数组
+ *
+ * @return 数据权限规则数组
+ */
+ List getDataPermissionRules();
+
+ /**
+ * 获得指定 Mapper 的数据权限规则数组
+ *
+ * @param mappedStatementId 指定 Mapper 的编号
+ * @return 数据权限规则数组
+ */
+ List getDataPermissionRule(String mappedStatementId);
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java
new file mode 100644
index 000000000..eaa6e6aed
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java
@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 默认的 DataPermissionRuleFactoryImpl 实现类
+ * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {
+
+ /**
+ * 数据权限规则数组
+ */
+ private final List rules;
+
+ @Override
+ public List getDataPermissionRules() {
+ return rules;
+ }
+
+ @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
+ public List getDataPermissionRule(String mappedStatementId) {
+ // 1. 无数据权限
+ if (CollUtil.isEmpty(rules)) {
+ return Collections.emptyList();
+ }
+ // 2. 未配置,则默认开启
+ DataPermission dataPermission = DataPermissionContextHolder.get();
+ if (dataPermission == null) {
+ return rules;
+ }
+ // 3. 已配置,但禁用
+ if (!dataPermission.enable()) {
+ return Collections.emptyList();
+ }
+
+ // 4. 已配置,只选择部分规则
+ if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
+ return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
+ .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
+ }
+ // 5. 已配置,只排除部分规则
+ if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
+ return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
+ .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
+ }
+ // 6. 已配置,全部规则
+ return rules;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
new file mode 100644
index 000000000..7f9a74a61
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java
@@ -0,0 +1,208 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.NullValue;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 基于部门的 {@link DataPermissionRule} 数据权限规则实现
+ *
+ * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
+ *
+ * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
+ * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-server 采用该方案】
+ * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
+ * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】
+ * 最终过滤条件是 WHERE dept_id = ?
+ * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号;
+ * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...)
+ * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤;
+ * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...)
+ *
+ * @author 芋道源码
+ */
+@AllArgsConstructor
+@Slf4j
+public class DeptDataPermissionRule implements DataPermissionRule {
+
+ /**
+ * LoginUser 的 Context 缓存 Key
+ */
+ protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName();
+
+ private static final String DEPT_COLUMN_NAME = "dept_id";
+ private static final String USER_COLUMN_NAME = "user_id";
+
+ static final Expression EXPRESSION_NULL = new NullValue();
+
+ private final PermissionApi permissionApi;
+
+ /**
+ * 基于部门的表字段配置
+ * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
+ *
+ * key:表名
+ * value:字段名
+ */
+ private final Map deptColumns = new HashMap<>();
+ /**
+ * 基于用户的表字段配置
+ * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
+ *
+ * key:表名
+ * value:字段名
+ */
+ private final Map userColumns = new HashMap<>();
+ /**
+ * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集
+ */
+ private final Set TABLE_NAMES = new HashSet<>();
+
+ @Override
+ public Set getTableNames() {
+ return TABLE_NAMES;
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ // 只有有登陆用户的情况下,才进行数据权限的处理
+ LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+ if (loginUser == null) {
+ return null;
+ }
+ // 只有管理员类型的用户,才进行数据权限的处理
+ if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
+ return null;
+ }
+
+ // 获得数据权限
+ DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
+ // 从上下文中拿不到,则调用逻辑进行获取
+ if (deptDataPermission == null) {
+ deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
+ if (deptDataPermission == null) {
+ log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
+ throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
+ loginUser.getId(), tableName, tableAlias.getName()));
+ }
+ // 添加到上下文中,避免重复计算
+ loginUser.setContext(CONTEXT_KEY, deptDataPermission);
+ }
+
+ // 情况一,如果是 ALL 可查看全部,则无需拼接条件
+ if (deptDataPermission.getAll()) {
+ return null;
+ }
+
+ // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
+ if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
+ && Boolean.FALSE.equals(deptDataPermission.getSelf())) {
+ return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
+ }
+
+ // 情况三,拼接 Dept 和 User 的条件,最后组合
+ Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
+ Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
+ if (deptExpression == null && userExpression == null) {
+ // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
+ log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
+ JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
+// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
+// loginUser.getId(), tableName, tableAlias.getName()));
+ return EXPRESSION_NULL;
+ }
+ if (deptExpression == null) {
+ return userExpression;
+ }
+ if (userExpression == null) {
+ return deptExpression;
+ }
+ // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ?
+ return new OrExpression(deptExpression, userExpression);
+ }
+
+ private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) {
+ // 如果不存在配置,则无需作为条件
+ String columnName = deptColumns.get(tableName);
+ if (StrUtil.isEmpty(columnName)) {
+ return null;
+ }
+ // 如果为空,则无条件
+ if (CollUtil.isEmpty(deptIds)) {
+ return null;
+ }
+ // 拼接条件
+ return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
+ new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new)));
+ }
+
+ private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {
+ // 如果不查看自己,则无需作为条件
+ if (Boolean.FALSE.equals(self)) {
+ return null;
+ }
+ String columnName = userColumns.get(tableName);
+ if (StrUtil.isEmpty(columnName)) {
+ return null;
+ }
+ // 拼接条件
+ return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));
+ }
+
+ // ==================== 添加配置 ====================
+
+ public void addDeptColumn(Class extends BaseDO> entityClass) {
+ addDeptColumn(entityClass, DEPT_COLUMN_NAME);
+ }
+
+ public void addDeptColumn(Class extends BaseDO> entityClass, String columnName) {
+ String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
+ addDeptColumn(tableName, columnName);
+ }
+
+ public void addDeptColumn(String tableName, String columnName) {
+ deptColumns.put(tableName, columnName);
+ TABLE_NAMES.add(tableName);
+ }
+
+ public void addUserColumn(Class extends BaseDO> entityClass) {
+ addUserColumn(entityClass, USER_COLUMN_NAME);
+ }
+
+ public void addUserColumn(Class extends BaseDO> entityClass, String columnName) {
+ String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
+ addUserColumn(tableName, columnName);
+ }
+
+ public void addUserColumn(String tableName, String columnName) {
+ userColumns.put(tableName, columnName);
+ TABLE_NAMES.add(tableName);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java
new file mode 100644
index 000000000..e5e4f578c
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
+
+/**
+ * {@link DeptDataPermissionRule} 的自定义配置接口
+ *
+ * @author 芋道源码
+ */
+@FunctionalInterface
+public interface DeptDataPermissionRuleCustomizer {
+
+ /**
+ * 自定义该权限规则
+ * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则
+ * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则
+ *
+ * @param rule 权限规则
+ */
+ void customize(DeptDataPermissionRule rule);
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java
new file mode 100644
index 000000000..66e932680
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 基于部门的数据权限规则
+ *
+ * @author 芋道源码
+ */
+package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java
new file mode 100644
index 000000000..831aa7c62
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件
+ */
+package cn.iocoder.yudao.framework.datapermission;
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..1a4c029c9
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,3 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\
+ cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java
new file mode 100644
index 000000000..ba97ede2f
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java
@@ -0,0 +1,108 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import org.aopalliance.intercept.MethodInvocation;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link DataPermissionAnnotationInterceptor} 的单元测试
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private DataPermissionAnnotationInterceptor interceptor;
+
+ @Mock
+ private MethodInvocation methodInvocation;
+
+ @BeforeEach
+ public void setUp() {
+ interceptor.getDataPermissionCache().clear();
+ }
+
+ @Test // 无 @DataPermission 注解
+ public void testInvoke_none() throws Throwable {
+ // 参数
+ mockMethodInvocation(TestNone.class);
+
+ // 调用
+ Object result = interceptor.invoke(methodInvocation);
+ // 断言
+ assertEquals("none", result);
+ assertEquals(1, interceptor.getDataPermissionCache().size());
+ assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
+ }
+
+ @Test // 在 Method 上有 @DataPermission 注解
+ public void testInvoke_method() throws Throwable {
+ // 参数
+ mockMethodInvocation(TestMethod.class);
+
+ // 调用
+ Object result = interceptor.invoke(methodInvocation);
+ // 断言
+ assertEquals("method", result);
+ assertEquals(1, interceptor.getDataPermissionCache().size());
+ assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
+ }
+
+ @Test // 在 Class 上有 @DataPermission 注解
+ public void testInvoke_class() throws Throwable {
+ // 参数
+ mockMethodInvocation(TestClass.class);
+
+ // 调用
+ Object result = interceptor.invoke(methodInvocation);
+ // 断言
+ assertEquals("class", result);
+ assertEquals(1, interceptor.getDataPermissionCache().size());
+ assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable());
+ }
+
+ private void mockMethodInvocation(Class> clazz) throws Throwable {
+ Object targetObject = clazz.newInstance();
+ Method method = targetObject.getClass().getMethod("echo");
+ when(methodInvocation.getThis()).thenReturn(targetObject);
+ when(methodInvocation.getMethod()).thenReturn(method);
+ when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject));
+ }
+
+ static class TestMethod {
+
+ @DataPermission(enable = false)
+ public String echo() {
+ return "method";
+ }
+
+ }
+
+ @DataPermission(enable = false)
+ static class TestClass {
+
+ public String echo() {
+ return "class";
+ }
+
+ }
+
+ static class TestNone {
+
+ public String echo() {
+ return "none";
+ }
+
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java
new file mode 100644
index 000000000..688b92d9f
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java
@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.framework.datapermission.core.aop;
+
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+
+/**
+ * {@link DataPermissionContextHolder} 的单元测试
+ *
+ * @author 芋道源码
+ */
+class DataPermissionContextHolderTest {
+
+ @BeforeEach
+ public void setUp() {
+ DataPermissionContextHolder.clear();
+ }
+
+ @Test
+ public void testGet() {
+ // mock 方法
+ DataPermission dataPermission01 = mock(DataPermission.class);
+ DataPermissionContextHolder.add(dataPermission01);
+ DataPermission dataPermission02 = mock(DataPermission.class);
+ DataPermissionContextHolder.add(dataPermission02);
+
+ // 调用
+ DataPermission result = DataPermissionContextHolder.get();
+ // 断言
+ assertSame(result, dataPermission02);
+ }
+
+ @Test
+ public void testPush() {
+ // 调用
+ DataPermission dataPermission01 = mock(DataPermission.class);
+ DataPermissionContextHolder.add(dataPermission01);
+ DataPermission dataPermission02 = mock(DataPermission.class);
+ DataPermissionContextHolder.add(dataPermission02);
+ // 断言
+ DataPermission first = DataPermissionContextHolder.getAll().get(0);
+ DataPermission second = DataPermissionContextHolder.getAll().get(1);
+ assertSame(dataPermission01, first);
+ assertSame(dataPermission02, second);
+ }
+
+ @Test
+ public void testRemove() {
+ // mock 方法
+ DataPermission dataPermission01 = mock(DataPermission.class);
+ DataPermissionContextHolder.add(dataPermission01);
+ DataPermission dataPermission02 = mock(DataPermission.class);
+ DataPermissionContextHolder.add(dataPermission02);
+
+ // 调用
+ DataPermission result = DataPermissionContextHolder.remove();
+ // 断言
+ assertSame(result, dataPermission02);
+ assertEquals(1, DataPermissionContextHolder.getAll().size());
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java
new file mode 100644
index 000000000..4c1494aca
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java
@@ -0,0 +1,190 @@
+package cn.iocoder.yudao.framework.datapermission.core.db;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.schema.Column;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+
+import java.sql.Connection;
+import java.util.*;
+
+import static java.util.Collections.singletonList;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link DataPermissionDatabaseInterceptor} 的单元测试
+ * 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)}
+ * 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)}
+ * 以及在这个过程中,ContextHolder 和 MappedStatementCache
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private DataPermissionDatabaseInterceptor interceptor;
+
+ @Mock
+ private DataPermissionRuleFactory ruleFactory;
+
+ @BeforeEach
+ public void setUp() {
+ // 清理上下文
+ DataPermissionDatabaseInterceptor.ContextHolder.clear();
+ // 清空缓存
+ interceptor.getMappedStatementCache().clear();
+ }
+
+ @Test // 不存在规则,且不匹配
+ public void testBeforeQuery_withoutRule() {
+ try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) {
+ // 准备参数
+ MappedStatement mappedStatement = mock(MappedStatement.class);
+ BoundSql boundSql = mock(BoundSql.class);
+
+ // 调用
+ interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+ // 断言
+ pluginUtilsMock.verify(() -> PluginUtils.mpBoundSql(boundSql), never());
+ }
+ }
+
+ @Test // 存在规则,且不匹配
+ public void testBeforeQuery_withMatchRule() {
+ try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) {
+ // 准备参数
+ MappedStatement mappedStatement = mock(MappedStatement.class);
+ BoundSql boundSql = mock(BoundSql.class);
+ // mock 方法(数据权限)
+ when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
+ .thenReturn(singletonList(new DeptDataPermissionRule()));
+ // mock 方法(MPBoundSql)
+ PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+ pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+ // mock 方法(SQL)
+ String sql = "select * from t_user where id = 1";
+ when(mpBs.sql()).thenReturn(sql);
+ // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
+
+ // 调用
+ interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+ // 断言
+ verify(mpBs, times(1)).sql(
+ eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100"));
+ // 断言缓存
+ assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
+ }
+ }
+
+ @Test // 存在规则,但不匹配
+ public void testBeforeQuery_withoutMatchRule() {
+ try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) {
+ // 准备参数
+ MappedStatement mappedStatement = mock(MappedStatement.class);
+ BoundSql boundSql = mock(BoundSql.class);
+ // mock 方法(数据权限)
+ when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId())))
+ .thenReturn(singletonList(new DeptDataPermissionRule()));
+ // mock 方法(MPBoundSql)
+ PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class);
+ pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs);
+ // mock 方法(SQL)
+ String sql = "select * from t_role where id = 1";
+ when(mpBs.sql()).thenReturn(sql);
+ // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
+
+ // 调用
+ interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql);
+ // 断言
+ verify(mpBs, times(1)).sql(
+ eq("SELECT * FROM t_role WHERE id = 1"));
+ // 断言缓存
+ assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty());
+ }
+ }
+
+ @Test
+ public void testAddNoRewritable() {
+ // 准备参数
+ MappedStatement ms = mock(MappedStatement.class);
+ List rules = singletonList(new DeptDataPermissionRule());
+ // mock 方法
+ when(ms.getId()).thenReturn("selectById");
+
+ // 调用
+ interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
+ // 断言
+ Map, Set> noRewritableMappedStatements =
+ interceptor.getMappedStatementCache().getNoRewritableMappedStatements();
+ assertEquals(1, noRewritableMappedStatements.size());
+ assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class));
+ }
+
+ @Test
+ public void testNoRewritable() {
+ // 准备参数
+ MappedStatement ms = mock(MappedStatement.class);
+ // mock 方法
+ when(ms.getId()).thenReturn("selectById");
+ // mock 数据
+ List rules = singletonList(new DeptDataPermissionRule());
+ interceptor.getMappedStatementCache().addNoRewritable(ms, rules);
+
+ // 场景一,rules 为空
+ assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null));
+ // 场景二,rules 非空,可重写
+ assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule())));
+ // 场景三,rule 非空,不可重写
+ assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules));
+ }
+
+ private static class DeptDataPermissionRule implements DataPermissionRule {
+
+ private static final String COLUMN = "dept_id";
+
+ @Override
+ public Set getTableNames() {
+ return SetUtils.asSet("t_user");
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
+ LongValue value = new LongValue(100L);
+ return new EqualsTo(column, value);
+ }
+
+ }
+
+ private static class EmptyDataPermissionRule implements DataPermissionRule {
+
+ @Override
+ public Set getTableNames() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ return null;
+ }
+
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java
new file mode 100644
index 000000000..8c0772f1a
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java
@@ -0,0 +1,370 @@
+package cn.iocoder.yudao.framework.datapermission.core.db;
+
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule;
+import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory;
+import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.schema.Column;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * {@link DataPermissionDatabaseInterceptor} 的单元测试
+ * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试
+ * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~
+ *
+ * @author 芋道源码
+ */
+public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private DataPermissionDatabaseInterceptor interceptor;
+
+ @Mock
+ private DataPermissionRuleFactory ruleFactory;
+
+ @BeforeEach
+ public void setUp() {
+ // 租户的数据权限规则
+ DataPermissionRule tenantRule = new DataPermissionRule() {
+
+ private static final String COLUMN = "tenant_id";
+
+ @Override
+ public Set getTableNames() {
+ return asSet("entity", "entity1", "entity2", "t1", "t2", // 支持 MyBatis Plus 的单元测试
+ "t_user", "t_role"); // 满足自己的单元测试
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
+ LongValue value = new LongValue(1L);
+ return new EqualsTo(column, value);
+ }
+
+ };
+ // 部门的数据权限规则
+ DataPermissionRule deptRule = new DataPermissionRule() {
+
+ private static final String COLUMN = "dept_id";
+
+ @Override
+ public Set getTableNames() {
+ return asSet("t_user"); // 满足自己的单元测试
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
+ ExpressionList values = new ExpressionList(new LongValue(10L),
+ new LongValue(20L));
+ return new InExpression(column, values);
+ }
+
+ };
+ // 设置到上下文,保证
+ DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule));
+ }
+
+ @Test
+ void delete() {
+ assertSql("delete from entity where id = ?",
+ "DELETE FROM entity WHERE id = ? AND tenant_id = 1");
+ }
+
+ @Test
+ void update() {
+ assertSql("update entity set name = ? where id = ?",
+ "UPDATE entity SET name = ? WHERE id = ? AND tenant_id = 1");
+ }
+
+ @Test
+ void selectSingle() {
+ // 单表
+ assertSql("select * from entity where id = ?",
+ "SELECT * FROM entity WHERE id = ? AND tenant_id = 1");
+
+ assertSql("select * from entity where id = ? or name = ?",
+ "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1");
+
+ assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)",
+ "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1");
+
+ /* not */
+ assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)",
+ "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND tenant_id = 1");
+ }
+
+ @Test
+ void selectSubSelectIn() {
+ /* in */
+ assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+ // 在最前
+ assertSql("SELECT * FROM entity e WHERE e.id IN " +
+ "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
+ "SELECT * FROM entity e WHERE e.id IN " +
+ "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
+ // 在最后
+ assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
+ "(select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
+ "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+ // 在中间
+ assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " +
+ "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?",
+ "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " +
+ "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectSubSelectEq() {
+ /* = */
+ assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectSubSelectInnerNotEq() {
+ /* inner not = */
+ assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))",
+ "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1");
+
+ assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)",
+ "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectSubSelectExists() {
+ /* EXISTS */
+ assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+
+
+ /* NOT EXISTS */
+ assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectSubSelect() {
+ /* >= */
+ assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+
+ /* <= */
+ assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+
+ /* <> */
+ assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)",
+ "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectFromSelect() {
+ assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))",
+ "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)");
+ }
+
+ @Test
+ void selectBodySubSelect() {
+ assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1",
+ "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1");
+ }
+
+ @Test
+ void selectLeftJoin() {
+ // left join
+ assertSql("SELECT * FROM entity e " +
+ "left join entity1 e1 on e1.id = e.id " +
+ "WHERE e.id = ? OR e.name = ?",
+ "SELECT * FROM entity e " +
+ "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+
+ assertSql("SELECT * FROM entity e " +
+ "left join entity1 e1 on e1.id = e.id " +
+ "WHERE (e.id = ? OR e.name = ?)",
+ "SELECT * FROM entity e " +
+ "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectRightJoin() {
+ // right join
+ assertSql("SELECT * FROM entity e " +
+ "right join entity1 e1 on e1.id = e.id",
+ "SELECT * FROM entity e " +
+ "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE e.tenant_id = 1");
+
+ assertSql("SELECT * FROM entity e " +
+ "right join entity1 e1 on e1.id = e.id " +
+ "WHERE e.id = ? OR e.name = ?",
+ "SELECT * FROM entity e " +
+ "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectLeftJoinMultipleTrailingOn() {
+ // 多个 on 尾缀的
+ assertSql("SELECT * FROM entity e " +
+ "LEFT JOIN entity1 e1 " +
+ "LEFT JOIN entity2 e2 ON e2.id = e1.id " +
+ "ON e1.id = e.id " +
+ "WHERE (e.id = ? OR e.NAME = ?)",
+ "SELECT * FROM entity e " +
+ "LEFT JOIN entity1 e1 " +
+ "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " +
+ "ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
+
+ assertSql("SELECT * FROM entity e " +
+ "LEFT JOIN entity1 e1 " +
+ "LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
+ "ON e1.id = e.id " +
+ "WHERE (e.id = ? OR e.NAME = ?)",
+ "SELECT * FROM entity e " +
+ "LEFT JOIN entity1 e1 " +
+ "LEFT JOIN with_as_A e2 ON e2.id = e1.id " +
+ "ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectInnerJoin() {
+ // inner join
+ assertSql("SELECT * FROM entity e " +
+ "inner join entity1 e1 on e1.id = e.id " +
+ "WHERE e.id = ? OR e.name = ?",
+ "SELECT * FROM entity e " +
+ "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+
+ assertSql("SELECT * FROM entity e " +
+ "inner join entity1 e1 on e1.id = e.id " +
+ "WHERE (e.id = ? OR e.name = ?)",
+ "SELECT * FROM entity e " +
+ "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+
+ // 垃圾 inner join todo
+// assertSql("SELECT * FROM entity,entity1 " +
+// "WHERE entity.id = entity1.id",
+// "SELECT * FROM entity e " +
+// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+ }
+
+ @Test
+ void selectWithAs() {
+ assertSql("with with_as_A as (select * from entity) select * from with_as_A",
+ "WITH with_as_A AS (SELECT * FROM entity WHERE tenant_id = 1) SELECT * FROM with_as_A");
+ }
+
+ private void assertSql(String sql, String targetSql) {
+ assertEquals(targetSql, interceptor.parserSingle(sql, null));
+ }
+
+ // ========== 额外的测试 ==========
+
+ @Test
+ public void testSelectSingle() {
+ // 单表
+ assertSql("select * from t_user where id = ?",
+ "SELECT * FROM t_user WHERE id = ? AND tenant_id = 1 AND dept_id IN (10, 20)");
+
+ assertSql("select * from t_user where id = ? or name = ?",
+ "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
+
+ assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)",
+ "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
+
+ /* not */
+ assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)",
+ "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)");
+ }
+
+ @Test
+ public void testSelectLeftJoin() {
+ // left join
+ assertSql("SELECT * FROM t_user e " +
+ "left join t_role e1 on e1.id = e.id " +
+ "WHERE e.id = ? OR e.name = ?",
+ "SELECT * FROM t_user e " +
+ "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+
+ // 条件 e.id = ? OR e.name = ? 带括号
+ assertSql("SELECT * FROM t_user e " +
+ "left join t_role e1 on e1.id = e.id " +
+ "WHERE (e.id = ? OR e.name = ?)",
+ "SELECT * FROM t_user e " +
+ "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+ }
+
+ @Test
+ public void testSelectRightJoin() {
+ // right join
+ assertSql("SELECT * FROM t_user e " +
+ "right join t_role e1 on e1.id = e.id " +
+ "WHERE e.id = ? OR e.name = ?",
+ "SELECT * FROM t_user e " +
+ "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+
+ // 条件 e.id = ? OR e.name = ? 带括号
+ assertSql("SELECT * FROM t_user e " +
+ "right join t_role e1 on e1.id = e.id " +
+ "WHERE (e.id = ? OR e.name = ?)",
+ "SELECT * FROM t_user e " +
+ "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+ }
+
+ @Test
+ public void testSelectInnerJoin() {
+ // inner join
+ assertSql("SELECT * FROM t_user e " +
+ "inner join entity1 e1 on e1.id = e.id " +
+ "WHERE e.id = ? OR e.name = ?",
+ "SELECT * FROM t_user e " +
+ "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+
+ // 条件 e.id = ? OR e.name = ? 带括号
+ assertSql("SELECT * FROM t_user e " +
+ "inner join t_role e1 on e1.id = e.id " +
+ "WHERE (e.id = ? OR e.name = ?)",
+ "SELECT * FROM t_user e " +
+ "INNER JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+ "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)");
+
+ // 垃圾 inner join todo
+// assertSql("SELECT * FROM entity,entity1 " +
+// "WHERE entity.id = entity1.id",
+// "SELECT * FROM entity e " +
+// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
+// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java
new file mode 100644
index 000000000..17dddc929
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java
@@ -0,0 +1,145 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule;
+
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Spy;
+import org.springframework.core.annotation.AnnotationUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link DataPermissionRuleFactoryImpl} 单元测试
+ *
+ * @author 芋道源码
+ */
+class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private DataPermissionRuleFactoryImpl dataPermissionRuleFactory;
+
+ @Spy
+ private List rules = Arrays.asList(new DataPermissionRule01(),
+ new DataPermissionRule02());
+
+ @BeforeEach
+ public void setUp() {
+ DataPermissionContextHolder.clear();
+ }
+
+ @Test
+ public void testGetDataPermissionRule_02() {
+ // 准备参数
+ String mappedStatementId = randomString();
+
+ // 调用
+ List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+ // 断言
+ assertSame(rules, result);
+ }
+
+ @Test
+ public void testGetDataPermissionRule_03() {
+ // 准备参数
+ String mappedStatementId = randomString();
+ // mock 方法
+ DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class));
+
+ // 调用
+ List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+ // 断言
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void testGetDataPermissionRule_04() {
+ // 准备参数
+ String mappedStatementId = randomString();
+ // mock 方法
+ DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class));
+
+ // 调用
+ List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+ // 断言
+ assertEquals(1, result.size());
+ assertEquals(DataPermissionRule01.class, result.get(0).getClass());
+ }
+
+ @Test
+ public void testGetDataPermissionRule_05() {
+ // 准备参数
+ String mappedStatementId = randomString();
+ // mock 方法
+ DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class));
+
+ // 调用
+ List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+ // 断言
+ assertEquals(1, result.size());
+ assertEquals(DataPermissionRule02.class, result.get(0).getClass());
+ }
+
+ @Test
+ public void testGetDataPermissionRule_06() {
+ // 准备参数
+ String mappedStatementId = randomString();
+ // mock 方法
+ DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class));
+
+ // 调用
+ List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId);
+ // 断言
+ assertSame(rules, result);
+ }
+
+ @DataPermission(enable = false)
+ static class TestClass03 {}
+
+ @DataPermission(includeRules = DataPermissionRule01.class)
+ static class TestClass04 {}
+
+ @DataPermission(excludeRules = DataPermissionRule01.class)
+ static class TestClass05 {}
+
+ @DataPermission
+ static class TestClass06 {}
+
+ static class DataPermissionRule01 implements DataPermissionRule {
+
+ @Override
+ public Set getTableNames() {
+ return null;
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ return null;
+ }
+
+ }
+
+ static class DataPermissionRule02 implements DataPermissionRule {
+
+ @Override
+ public Set getTableNames() {
+ return null;
+ }
+
+ @Override
+ public Expression getExpression(String tableName, Alias tableAlias) {
+ return null;
+ }
+
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java
new file mode 100644
index 000000000..a50e2382f
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java
@@ -0,0 +1,235 @@
+package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link DeptDataPermissionRule} 的单元测试
+ *
+ * @author 芋道源码
+ */
+class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private DeptDataPermissionRule rule;
+
+ @Mock
+ private PermissionApi permissionApi;
+
+ @BeforeEach
+ @SuppressWarnings("unchecked")
+ public void setUp() {
+ // 清空 rule
+ rule.getTableNames().clear();
+ ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
+ ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
+ }
+
+ @Test // 无 LoginUser
+ public void testGetExpression_noLoginUser() {
+ // 准备参数
+ String tableName = randomString();
+ Alias tableAlias = new Alias(randomString());
+ // mock 方法
+
+ // 调用
+ Expression expression = rule.getExpression(tableName, tableAlias);
+ // 断言
+ assertNull(expression);
+ }
+
+ @Test // 无数据权限时
+ public void testGetExpression_noDeptDataPermission() {
+ try (MockedStatic securityFrameworkUtilsMock
+ = mockStatic(SecurityFrameworkUtils.class)) {
+ // 准备参数
+ String tableName = "t_user";
+ Alias tableAlias = new Alias("u");
+ // mock 方法
+ LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+ .setUserType(UserTypeEnum.ADMIN.getValue()));
+ securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+
+ // 调用
+ NullPointerException exception = assertThrows(NullPointerException.class,
+ () -> rule.getExpression(tableName, tableAlias));
+ // 断言
+ assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage());
+ }
+ }
+
+ @Test // 全部数据权限
+ public void testGetExpression_allDeptDataPermission() {
+ try (MockedStatic securityFrameworkUtilsMock
+ = mockStatic(SecurityFrameworkUtils.class)) {
+ // 准备参数
+ String tableName = "t_user";
+ Alias tableAlias = new Alias("u");
+ // mock 方法(LoginUser)
+ LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+ .setUserType(UserTypeEnum.ADMIN.getValue()));
+ securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+ // mock 方法(DeptDataPermissionRespDTO)
+ DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true);
+ when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+
+ // 调用
+ Expression expression = rule.getExpression(tableName, tableAlias);
+ // 断言
+ assertNull(expression);
+ assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+ }
+ }
+
+ @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限
+ public void testGetExpression_noDept_noSelf() {
+ try (MockedStatic securityFrameworkUtilsMock
+ = mockStatic(SecurityFrameworkUtils.class)) {
+ // 准备参数
+ String tableName = "t_user";
+ Alias tableAlias = new Alias("u");
+ // mock 方法(LoginUser)
+ LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+ .setUserType(UserTypeEnum.ADMIN.getValue()));
+ securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+ // mock 方法(DeptDataPermissionRespDTO)
+ DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO();
+ when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+
+ // 调用
+ Expression expression = rule.getExpression(tableName, tableAlias);
+ // 断言
+ assertEquals("null = null", expression.toString());
+ assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+ }
+ }
+
+ @Test // 拼接 Dept 和 User 的条件(字段都不符合)
+ public void testGetExpression_noDeptColumn_noSelfColumn() {
+ try (MockedStatic securityFrameworkUtilsMock
+ = mockStatic(SecurityFrameworkUtils.class)) {
+ // 准备参数
+ String tableName = "t_user";
+ Alias tableAlias = new Alias("u");
+ // mock 方法(LoginUser)
+ LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+ .setUserType(UserTypeEnum.ADMIN.getValue()));
+ securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+ // mock 方法(DeptDataPermissionRespDTO)
+ DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+ .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true);
+ when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+
+ // 调用
+ Expression expression = rule.getExpression(tableName, tableAlias);
+ // 断言
+ assertSame(EXPRESSION_NULL, expression);
+ assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+ }
+ }
+
+ @Test // 拼接 Dept 和 User 的条件(self 符合)
+ public void testGetExpression_noDeptColumn_yesSelfColumn() {
+ try (MockedStatic securityFrameworkUtilsMock
+ = mockStatic(SecurityFrameworkUtils.class)) {
+ // 准备参数
+ String tableName = "t_user";
+ Alias tableAlias = new Alias("u");
+ // mock 方法(LoginUser)
+ LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+ .setUserType(UserTypeEnum.ADMIN.getValue()));
+ securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+ // mock 方法(DeptDataPermissionRespDTO)
+ DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+ .setSelf(true);
+ when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+ // 添加 user 字段配置
+ rule.addUserColumn("t_user", "id");
+
+ // 调用
+ Expression expression = rule.getExpression(tableName, tableAlias);
+ // 断言
+ assertEquals("u.id = 1", expression.toString());
+ assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+ }
+ }
+
+ @Test // 拼接 Dept 和 User 的条件(dept 符合)
+ public void testGetExpression_yesDeptColumn_noSelfColumn() {
+ try (MockedStatic securityFrameworkUtilsMock
+ = mockStatic(SecurityFrameworkUtils.class)) {
+ // 准备参数
+ String tableName = "t_user";
+ Alias tableAlias = new Alias("u");
+ // mock 方法(LoginUser)
+ LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+ .setUserType(UserTypeEnum.ADMIN.getValue()));
+ securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+ // mock 方法(DeptDataPermissionRespDTO)
+ DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+ .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L));
+ when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+ // 添加 dept 字段配置
+ rule.addDeptColumn("t_user", "dept_id");
+
+ // 调用
+ Expression expression = rule.getExpression(tableName, tableAlias);
+ // 断言
+ assertEquals("u.dept_id IN (10, 20)", expression.toString());
+ assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+ }
+ }
+
+ @Test // 拼接 Dept 和 User 的条件(dept + self 符合)
+ public void testGetExpression_yesDeptColumn_yesSelfColumn() {
+ try (MockedStatic securityFrameworkUtilsMock
+ = mockStatic(SecurityFrameworkUtils.class)) {
+ // 准备参数
+ String tableName = "t_user";
+ Alias tableAlias = new Alias("u");
+ // mock 方法(LoginUser)
+ LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
+ .setUserType(UserTypeEnum.ADMIN.getValue()));
+ securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
+ // mock 方法(DeptDataPermissionRespDTO)
+ DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
+ .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true);
+ when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
+ // 添加 user 字段配置
+ rule.addUserColumn("t_user", "id");
+ // 添加 dept 字段配置
+ rule.addDeptColumn("t_user", "dept_id");
+
+ // 调用
+ Expression expression = rule.getExpression(tableName, tableAlias);
+ // 断言
+ assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString());
+ assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
+ }
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-biz-dict/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-dict/pom.xml
new file mode 100644
index 000000000..b1ee580f4
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-biz-dict/pom.xml
@@ -0,0 +1,30 @@
+
+
+