diff --git a/milkbox-app/src/main/java/top/milkbox/Application.java b/milkbox-app/src/main/java/top/milkbox/Application.java
index 7914742271797517a594c050b56b74430c85c1f1..fcb77930f49b12b76b24dcb32f1d115db08647da 100644
--- a/milkbox-app/src/main/java/top/milkbox/Application.java
+++ b/milkbox-app/src/main/java/top/milkbox/Application.java
@@ -22,7 +22,8 @@ import java.util.TimeZone;
* @author milkbox
*/
@Slf4j
-@EnableWebMvc
+// 注意:使用satoken不能加这个注解,否则在SaServletFilter中调用SpringMVCUtil.getRequest()“报错非Web上下文无法获取Request”
+//@EnableWebMvc
@RestController
@SpringBootApplication
public class Application {
diff --git a/milkbox-app/src/main/java/top/milkbox/core/config/SaTokenConfiguration.java b/milkbox-app/src/main/java/top/milkbox/core/config/SaTokenConfiguration.java
index 225c2c4b1ee70112dba3063befa0f1ff49d2c6c5..ae1b2d35343367098a53ce40bb2a2a1711038dc7 100644
--- a/milkbox-app/src/main/java/top/milkbox/core/config/SaTokenConfiguration.java
+++ b/milkbox-app/src/main/java/top/milkbox/core/config/SaTokenConfiguration.java
@@ -1,10 +1,22 @@
package top.milkbox.core.config;
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.dev33.satoken.router.SaHttpMethod;
+import cn.dev33.satoken.router.SaRouter;
+import cn.dev33.satoken.util.SaTokenConsts;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import top.milkbox.common.enums.CommonStatusCodeEnum;
+import top.milkbox.common.pojo.CommonResult;
+@Slf4j
@Configuration
public class SaTokenConfiguration implements WebMvcConfigurer {
@@ -14,4 +26,117 @@ public class SaTokenConfiguration implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
+
+
+ /**
+ * 注册 [Sa-Token 全局过滤器]
+ *
+ * 可通过如下方式设置过滤器链,详情请参照官方文档:
+ * 自定义过滤器执行顺序
+ * @Bean
+ * public FilterRegistrationBean<SaServletFilter>
+ *
+ *
+ * 这里的filter拦截器早于interceptor
+ *
+ */
+ @Bean
+ public SaServletFilter getSaServletFilter() {
+ SaServletFilter saServletFilter = new SaServletFilter();
+
+ // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
+ // 不管什么请求,都先经过这里。obj是拓展字段,目前没有任何意义(1.37.0版本的sa-token)
+ saServletFilter.setBeforeAuth(obj -> {
+
+ SaHolder.getResponse()
+
+ // ---------- 设置跨域响应头 ----------
+ /*
+ 对于跨域请求,我们分两种情况
+ 1. 不使用浏览器的自动管理cookie功。也就是说在前后端分离的项目中(小程序,h5,手机应用等)禁用了cookie功能。
+ sa-token的登录令牌(token)需要在前端手动控制存储
+ 浏览器无需管理cookie,所以权限比较松,配置参考如下:
+ // 允许所有域
+ .setHeader("Access-Control-Allow-Origin", "*")
+ // 允许所有请求方式
+ .setHeader("Access-Control-Allow-Methods", "*")
+ // 允许所有头
+ .setHeader("Access-Control-Allow-Headers", "*")
+ // 有效时间
+ .setHeader("Access-Control-Max-Age", "3600");
+
+ 2. 如果硬是要用浏览器的自动存储cookie功能,需要一些额外配置,由于浏览器安全策略,必须明确指定允许的域、方法和允许的头。
+ 并且需要额外配置Access-Control-Allow-Credentials为true。配置参考如下:
+ // 使用SaHolder.getRequest().getHeader("Origin")获取请求的来源,相当于允许所有的域
+ .setHeader("Access-Control-Allow-Origin", SaHolder.getRequest().getHeader("Origin"))
+ // 允许所有请求方式
+ .setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT")
+ // 允许的header参数
+ .setHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With")
+ // 有效时间
+ .setHeader("Access-Control-Max-Age", "3600")
+ // 允许浏览器发送与保存cookie
+ .setHeader("Access-Control-Allow-Credentials", "true");
+
+ // 如果前端使用的是axios(其他的请求框架类似)这需要额外配置,允许跨域携带cookie:withCredentials: true
+ // 设置axios
+ const axiosInstance = axios.create({
+ // 设置请求地址
+ baseURL: import.meta.env.VITE_APP_SERVER_URL,
+ // 允许跨域保存与携带cookie
+ withCredentials: true,
+ })
+ */
+ .setHeader("Access-Control-Allow-Origin", SaHolder.getRequest().getHeader("Origin"))
+ // 允许所有请求方式
+ .setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT")
+ // 允许的header参数
+ .setHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With")
+ // 有效时间
+ .setHeader("Access-Control-Max-Age", "3600")
+ // 允许浏览器发送与保存cookie
+ .setHeader("Access-Control-Allow-Credentials", "true");
+
+ // 如果是预检请求,则立即返回到前端
+ // 这里的match可以理解为立即匹配请求方法,其参数是可变参数,可以传递多个
+ SaRouter.match(SaHttpMethod.OPTIONS)
+ // 当匹配到指定的请求方法后执行free中的函数式接口,参数r代表的是链式写法中操作的对象SaRouterStaff
+ // 可以在函数式接口中抛出StopMatchException异常来立即跳过SaServletFilter过滤器
+ .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
+ // 若匹配到则报错BackResultException,若未匹配到则只返回SaRouterStaff对象
+ // 抛出BackResultException异常会由sa-token处理,以text/plain格式将报错信息写入响应体
+ // 而实际上报错的BackResultException中没有任何信息
+ .back();
+ });
+
+ // 指定 [拦截路由] 与 [放行路由]
+ saServletFilter
+ .addInclude("/**")
+ .addExclude("/favicon.ico");
+
+ // 认证函数: 每次请求执行
+ // 在setBeforeAuth之后,且必须满足addInclude与addExclude的条件时才执行
+ // obj是拓展字段,目前没有任何意义(1.37.0版本的sa-token)
+ saServletFilter.setAuth(obj -> {
+ // 拦截指定的路径后要干的事情...(目前没什么事可干)
+ // ...
+ });
+
+ // 异常处理函数:每次认证函数发生异常时执行此函数
+ // 在setBeforeAuth和setAuth阶段发生异常时执行此函数传递的函数式接口
+ // 函数式接口中,e:表示异常对象;返回值将会写入到响应对象中,默认是text/plain格式,可以修改响应的格式来返回json,具体见其源码的注释
+ // 此处拦截的异常早于SpringBoot的全局异常拦截器,不受全局异常拦截器的影响
+ saServletFilter.setError(e -> {
+ String message = StrUtil.format("顶级拦截器(跨域处理)异常,原因:{}", e.getMessage());
+ log.error(message, e);
+ SaHolder.getResponse().setHeader("Content-Type", SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
+ // 由于此处不经过springMVC,所以需要手动转换为json
+ return JSONUtil.toJsonStr(
+ new CommonResult<>(CommonStatusCodeEnum.ERROR500.getCode(), message, e.getStackTrace()));
+ });
+
+ return saServletFilter;
+ }
+
+
}
diff --git a/milkbox-app/src/main/java/top/milkbox/core/handler/GlobalControllerAdvice.java b/milkbox-app/src/main/java/top/milkbox/core/handler/GlobalControllerAdvice.java
index 638f4cde072ee997d2f7b0d9c8890b1b26a2f75f..1ad6efd28e8f0a84fc46e12f1ad274790a9c94ff 100644
--- a/milkbox-app/src/main/java/top/milkbox/core/handler/GlobalControllerAdvice.java
+++ b/milkbox-app/src/main/java/top/milkbox/core/handler/GlobalControllerAdvice.java
@@ -58,8 +58,9 @@ public class GlobalControllerAdvice {
// 服务层业务逻辑异常
if (exception instanceof CommonServiceException commonServiceException) {
- return new CommonResult<>(CommonStatusCodeEnum.ERROR506.getCode(),
- CommonStatusCodeEnum.ERROR506.getMessage(), commonServiceException.getMessage());
+ return CommonResult.create()
+ .withCode(CommonStatusCodeEnum.ERROR506.getCode())
+ .withMessage(commonServiceException.getMessage());
}
// 用户未登录异常处理
diff --git a/milkbox-common/src/main/java/top/milkbox/common/pojo/CommonResult.java b/milkbox-common/src/main/java/top/milkbox/common/pojo/CommonResult.java
index 641331b014f3e1b3fa0319627a755e9889c3fc1a..35febebfce3f18e283d8ac94ff9701e0b0cd088b 100644
--- a/milkbox-common/src/main/java/top/milkbox/common/pojo/CommonResult.java
+++ b/milkbox-common/src/main/java/top/milkbox/common/pojo/CommonResult.java
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import lombok.With;
import top.milkbox.common.enums.CommonStatusCodeEnum;
import java.io.Serializable;
@@ -15,6 +16,7 @@ import java.io.Serializable;
* @author milkbox
*/
@Data
+@With
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult implements Serializable {
@@ -30,6 +32,10 @@ public class CommonResult implements Serializable {
@Schema(description = "返回的数据")
private ResultType data;
+ public static CommonResult create() {
+ return new CommonResult<>();
+ }
+
public static CommonResult ok() {
return CommonResult.ok(null);
}
diff --git a/milkbox-common/src/main/java/top/milkbox/common/utils/CommonCollUtil.java b/milkbox-common/src/main/java/top/milkbox/common/utils/CommonCollUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e55814c25c73e0e2ded5c42730028ad29c77223
--- /dev/null
+++ b/milkbox-common/src/main/java/top/milkbox/common/utils/CommonCollUtil.java
@@ -0,0 +1,180 @@
+package top.milkbox.common.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import top.milkbox.common.utils.base.CommonNode;
+import top.milkbox.common.utils.base.CycleRefException;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 集合工具类
+ *
+ * @author 郭泳辰
+ */
+public class CommonCollUtil {
+
+ /**
+ * 获取给定集合中重复的元素
+ *
+ * // 使用举例:
+ * List<ImportCategoryData> cachedDataList = new ArrayList<>();
+ * // cachedDataList............
+ * List<String> duplicateList =
+ * CommonCollUtil.getDuplicateElements(cachedDataList, ImportCategoryData::getCode);
+ * if (ObjectUtil.isNotEmpty(duplicateList)) {
+ * // 某某某重复
+ * duplicateList.forEach(item -> log.info("某某某【" + item + "】重复"));
+ * }
+ *
+ *
+ * @param objectList 指定集合
+ * @param function 指定字段
+ * @param 集合中对象的类型
+ * @param 指定的字段类型
+ * @return 返回集合中重复的所有元素
+ */
+ public static List getDuplicateElements(Collection objectList, Function function) {
+ return getDuplicateElements(objectList.stream().map(function));
+ }
+
+ /**
+ * 获取给定的简单集合中重复的元素
+ * 仅支持简单类型的集合,复杂类型集合请使用重载方法:{@link #getDuplicateElements(Collection, Function)}
+ *
+ * @param simpleList 指定集合
+ * @param 集合中对象的类型,必须是简单类型对象
+ * @return 返回重复的所有元素
+ */
+ public static List getDuplicateElements(Collection simpleList) {
+ return getDuplicateElements(simpleList.stream());
+ }
+
+ /**
+ * 获取给定的流中重复的元素
+ * 依靠分组操作求重复元素
+ *
+ * @param stream 指定的流对象(内部必须是简单类型)
+ * @param 流中元素的类型,必须是简单类型
+ * @return 返回重复的元素
+ */
+ public static List getDuplicateElements(Stream stream) {
+ return stream
+ // 去空
+ .filter(ObjectUtil::isNotNull)
+ // 分组
+ .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
+ // 获取键值对集合
+ .entrySet().stream()
+ // 值大于1的键就是重复的元素
+ .filter(entry -> entry.getValue() > 1)
+ // 获取键
+ .map(Map.Entry::getKey)
+ // 转为键集合
+ .collect(Collectors.toList());
+ }
+
+
+ /**
+ * 线性集合转为森林
+ *
+ * 节点类型必须实现{@link CommonNode}接口
+ *
+ * 满足一下任意条件表示是一个顶级节点:
+ * 1. 当前节点的parentId为空
+ * 2. 当前节点的parentId为空或者等于0
+ * 3. 当前节点的有parentId但是未在节点集合中找到其上级
+ *
+ *
+ * @param nodes 节点集合
+ * @param 节点id类型
+ * @param 节点类型
+ * @return 返回森林
+ * @throws CycleRefException 构建过程中出现循环引用异常
+ */
+ public static > List toForest(List nodes) throws CycleRefException {
+ Map nodeMap = nodes.stream().collect(Collectors.toMap(CommonNode::getId, node -> node));
+ List rootForest = new ArrayList<>();
+
+ for (T node : nodes) {
+ if (ObjectUtil.isEmpty(node.getParentId()) || CommonUtil.isZero(node.getParentId())) {
+ rootForest.add(node);
+ } else {
+ T parentNode = nodeMap.get(node.getParentId());
+ if (parentNode != null) {
+ LinkedList path = findFromForest(node.getParentId(), Collections.singletonList(node));
+ if (ObjectUtil.isNotEmpty(path)) {
+ throw new CycleRefException("循环引用:" + CollUtil.join(path, " <- "));
+ }
+ if (parentNode.getChildrenList() == null) {
+ parentNode.initChildrenList();
+ }
+ parentNode.getChildrenList().add(node);
+ } else {
+ rootForest.add(node);
+ }
+ }
+ }
+
+ return rootForest;
+ }
+
+ /**
+ * 递归查找指定节点在森林中的路径
+ *
+ * @param id 节点id
+ * @param forest 森林
+ * @param path 节点所在的路径
+ * @param 节点id类型
+ * @param 节点类型
+ * @return 递归过程中使用,true表示找到节点并立即结束整个递归
+ */
+ private static > boolean findFromForestDFS(S id, List forest, LinkedList path) {
+ if (ObjectUtil.isEmpty(forest)) {
+ return false;
+ }
+ for (T node : forest) {
+ path.add(node.getId());
+
+ // 找到目标节点 或者 下一层递归告诉我需要立即结束递归,则返回true(返回true表示立即结束整个递归)
+ if (id.equals(node.getId()) || findFromForestDFS(id, node.getChildrenList(), path)) {
+ return true;
+ }
+
+ path.removeLast();
+ }
+ return false;
+ }
+
+ /**
+ * 找到指定节点在森林中的路径
+ *
+ * @param id 节点id
+ * @param forest 森林
+ * @param 节点id类型
+ * @param 节点类型
+ * @return 如果找到,则返回路径,否则返回空集合
+ */
+ public static > LinkedList findFromForest(S id, List forest) {
+ LinkedList path = new LinkedList<>();
+ findFromForestDFS(id, forest, path);
+ return path;
+ }
+
+ /**
+ * 判断指定节点是否在森林中
+ *
+ * @param id 节点id
+ * @param forest 森林
+ * @param 节点id类型
+ * @param 节点类型
+ * @return 如果在,则返回true,否则返回false
+ */
+ public static > boolean isInForest(S id, List forest) {
+ return ObjectUtil.isNotEmpty(findFromForest(id, forest));
+ }
+
+}
diff --git a/milkbox-common/src/main/java/top/milkbox/common/utils/CommonUtil.java b/milkbox-common/src/main/java/top/milkbox/common/utils/CommonUtil.java
index 39b7fd52a2b93eda13c2a0738337956770bc32c1..729650205ef3e904305172638e6668f891577e98 100644
--- a/milkbox-common/src/main/java/top/milkbox/common/utils/CommonUtil.java
+++ b/milkbox-common/src/main/java/top/milkbox/common/utils/CommonUtil.java
@@ -5,10 +5,12 @@ import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNode;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -25,15 +27,14 @@ public class CommonUtil {
/**
* 将集合转换为森林
*
- * @param sourceList 要转换的集合,集合中的元素必须实现接口CommonMethodUtil.EntityTree
+ * @param sourceList 要转换的集合,集合中的元素必须实现接口CommonUtil.EntityTree
* @param rootId 以这个值为树的根节点
* @param id与parentId的数据类型
* @param 集合中每个元素的类型
* @return 返回森林
- * @author milkbox
+ * @author 郭泳辰
*/
- public static > List> toTree(
- List sourceList, S rootId) {
+ public static > List> toTree(List sourceList, S rootId) {
// 将OkrNormCategory集合对象转为TreeNode集合对象
List> treeNodeList = sourceList.stream().map(treeNode -> {
if (ObjectUtil.isEmpty(treeNode.getParentId())) {
@@ -57,6 +58,102 @@ public class CommonUtil {
String getName();
}
+ /**
+ * 将森林转换为只包含id的集合
+ *
+ * @param forest 森林
+ * @param Tree的泛型
+ * @return 返回Tree对象的id字段的集合
+ * @author 郭泳辰
+ */
+ public static List forestToIdList(List> forest) {
+ ArrayList resultList = new ArrayList<>();
+ generateIdListByDFS(forest, resultList);
+ return resultList;
+ }
+
+ /**
+ * 从指定的数组中根据传递的id查询这个id的所有子级。
+ * 此方法为内部方法,不建议直接调用。请使用findAllChildren(......)系列方法
+ *
+ * @param entityList 被查找的元素集合(集合中的元素必须实现接口CommonUtil.EntityTree)
+ * @param id 上级id
+ * @param childrenList 结果(集合中的元素必须实现接口CommonUtil.EntityTree)
+ * @param deepCount 当前递归深度(从1开始)
+ * @param deepLimit 最大递归深度(传递0表示无限制递归深度)
+ * @param 集合中id与parentId的类型
+ * @param 集合中元素的类型
+ * @return 返回子级集合
+ */
+ private static > List findAllChildrenHelper(
+ List entityList, S id, List childrenList, int deepCount, int deepLimit) {
+ if (entityList == null || id == null || childrenList == null || (deepLimit != 0 && deepCount > deepLimit)) {
+ return childrenList;
+ }
+ for (T entity : entityList) {
+ if (id.equals(entity.getParentId())) {
+ childrenList.add(entity); // 不确定此处如果同时将原集合的元素删除是否会提高效率
+ findAllChildrenHelper(entityList, entity.getId(), childrenList, deepCount + 1, deepLimit);
+ }
+ }
+ return childrenList;
+ }
+
+ /**
+ * 此方法为内部方法,不建议直接调用。请使用findAllChildren(......)系列方法
+ */
+ private static > List findAllChildrenHelper(
+ List entityList, S id, List childrenList, int deepLimit) {
+ return findAllChildrenHelper(entityList, id, childrenList, 1, deepLimit);
+ }
+
+ /**
+ * 从指定的集合(entityList)中找出id的所有子级(包括子级的子级......)。
+ *
+ * @param entityList 被查找的元素集合(集合中的元素必须实现接口CommonUtil.EntityTree)
+ * @param id 上级id
+ * @param deepLimit 最大递归深度(传递0表示无限制递归深度)
+ * @param 集合中id与parentId的类型
+ * @param 集合中元素的类型
+ * @return 返回子级集合
+ */
+ public static > List findAllChildren(
+ List entityList, S id, int deepLimit) {
+ return findAllChildrenHelper(entityList, id, new ArrayList<>(), deepLimit);
+ }
+
+ /**
+ * 从指定的集合(entityList)中找出id的所有子级(包括子级的子级......)。
+ * 此方法限制递归深度为32层
+ *
+ * @param entityList 被查找的元素集合(集合中的元素必须实现接口CommonUtil.EntityTree)
+ * @param id 上级id
+ * @param 集合中id与parentId的类型
+ * @param 集合中元素的类型
+ * @return 返回子级集合
+ */
+ public static > List findAllChildren(
+ List entityList, S id) {
+ return findAllChildrenHelper(entityList, id, new ArrayList<>(), 32);
+ }
+
+ /**
+ * 深度优先遍历,将森林转换为只包含id的集合
+ *
+ * @param forest 森林
+ * @param resultList 返回结果集合
+ * @param Tree对象的泛型
+ * @author 郭泳辰
+ */
+ private static void generateIdListByDFS(List> forest, List resultList) {
+ forest.forEach(tree -> {
+ resultList.add(tree.getId());
+ if (ObjectUtil.isNotEmpty(tree.getChildren())) {
+ generateIdListByDFS(tree.getChildren(), resultList);
+ }
+ });
+ }
+
/**
* 转为其他类型page,自定义每一项的转换方式
@@ -66,7 +163,7 @@ public class CommonUtil {
* @param 目标page类型
* @param 原page类型
* @return 返回转换后的page
- * @author milkbox
+ * @author 郭泳辰
*/
public static Page convertPage(Page page, Function converter) {
return convertPage(page, page.getRecords().stream().map(converter).collect(Collectors.toList()));
@@ -80,7 +177,7 @@ public class CommonUtil {
* @param 目标page类型
* @param 原page类型
* @return 返回转换后的page
- * @author milkbox
+ * @author 郭泳辰
*/
public static Page convertPage(Page page, Class targetClass) {
return convertPage(page, BeanUtil.copyToList(page.getRecords(), targetClass));
@@ -94,9 +191,9 @@ public class CommonUtil {
* @param 目标page类型
* @param 原page类型
* @return 返回转换后的page
- * @author milkbox
+ * @author 郭泳辰
*/
- private static Page convertPage(Page page, List targetRecords) {
+ public static Page convertPage(Page page, List targetRecords) {
Page targetPage = new Page<>();
targetPage.setRecords(targetRecords);
targetPage.setTotal(page.getTotal());
@@ -111,4 +208,43 @@ public class CommonUtil {
return targetPage;
}
+ /**
+ * 将Page<Object>转换为Page<JSONObject>
+ *
+ * @param page 原始分页对象
+ * @param 原始分页对象类型
+ * @return 返回JSONObject类型分页对象
+ * @author 郭泳辰
+ */
+ public static Page toJSONObjects(Page page) {
+ return CommonUtil.convertPage(page, JSONUtil::parseObj);
+ }
+
+
+ /**
+ * 判断任意简单对象是否为0,如果是0则返回true,否则返回false
+ * IllegalArgumentException表示类型不支持,传递null值也会抛出异常
+ *
+ * @param obj 任意简单对象
+ */
+ public static boolean isZero(Object obj) {
+ if (obj instanceof String) {
+ return "0".equals(obj);
+ }
+ if (obj instanceof Number) {
+ Number number = (Number) obj;
+ if (number instanceof Integer || number instanceof Long || number instanceof Short || number instanceof Byte) {
+ return number.intValue() == 0;
+ } else if (number instanceof Double || number instanceof Float) {
+ return number.doubleValue() == 0.0;
+ }
+ }
+ if (obj instanceof Character) {
+ Character character = (Character) obj;
+ return character.equals('0');
+ }
+ throw new IllegalArgumentException("不支持的类型:" + obj.getClass().getName());
+ }
+
+
}
diff --git a/milkbox-common/src/main/java/top/milkbox/common/utils/RetryUtil.java b/milkbox-common/src/main/java/top/milkbox/common/utils/RetryUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..9d631fe98ff94fa1a2948b2956f51ef3aed52858
--- /dev/null
+++ b/milkbox-common/src/main/java/top/milkbox/common/utils/RetryUtil.java
@@ -0,0 +1,110 @@
+package top.milkbox.common.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import top.milkbox.common.utils.base.ThrowsExceptionFunction;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 重试工具类
+ *
+ * @author milkbox
+ */
+@Slf4j
+public class RetryUtil {
+
+ public static final int DEFAULT_TIMES = 2; // 默认重试次数
+ public static final long DEFAULT_WAIT_TIME = 3000; // 默认重试间隔时间(毫秒)
+
+ /**
+ * 重试函数
+ *
+ * 使用函数式接口方式,指定要重试的函数,当函数出现Exception类型的异常时,会进行重试。重试次数由times指定,每次重试之间会等待waitTime毫秒
+ *
+ *
+ * 重试次数不包括第1次执行,所以times=2表示重试2次,共执行3次。无论如何,第1次一定会执行
+ *
+ *
+ * 例:
+ *
+ *
+ * // 尝试通过接口获取token,如果失败,则再重试3次,每次重试之间等待2秒
+ * String token = RetryUtil.retries((i) -> rpcGetToken(config), 3, 2000);
+ *
+ *
+ * @param retriesFunction 需要重试的函数,此函数的参数为当前重试的次数(0表示首次),返回值会被原样返回
+ * @param times 重试次数,无论传递什么值,retriesFunction一定会被执行一次
+ * @param waitTime 每次重试之间的等待时间,单位为毫秒,如果小于等于0,则立即重试不进行等待
+ * @param retriesFunction的返回值类型
+ * @return 原样返回retriesFunction的返回值
+ * @throws Exception 如果重试次数耗尽,则抛出原异常(就是retriesFunction内部的异常)
+ */
+ private static R retries(ThrowsExceptionFunction retriesFunction, int times, long waitTime)
+ throws Exception {
+ int i = 0;
+ while (true) {
+ try {
+ // 执行函数,如果没有异常,则直接返回
+ return retriesFunction.apply(i);
+ } catch (Exception e) {
+ if (i >= times) {
+ log.error("重试次数耗尽,相应的操作失败!");
+ throw e; // 抛出原异常,并结束循环
+ }
+ i++;
+ log.warn(e.getMessage(), e);
+ if (waitTime > 0) {
+ // 保留两位小数
+ log.warn(String.format("%.2f秒后重试......", (float) (waitTime) / 1000));
+ // 本质上还是Thread.sleep(waitTime),只不过不需要处理异常,且当时间小于等于0的时候不会执行休眠操作
+ TimeUnit.MILLISECONDS.sleep(waitTime);
+ }
+ log.warn("正在重试({}/{})......", i, times);
+ }
+ }
+ }
+
+ /**
+ * 重试函数
+ * {@link RetryUtil#retries(ThrowsExceptionFunction, int, long)}的重载,自定义重试次数,重试间隔为{@link RetryUtil#DEFAULT_WAIT_TIME}
+ * 详情参见{@link RetryUtil#retries(ThrowsExceptionFunction, int, long)}
+ */
+ public static R retries(ThrowsExceptionFunction consumer, int times) throws Exception {
+ return retries(consumer, times, DEFAULT_WAIT_TIME);
+ }
+
+ /**
+ * 重试函数
+ * {@link RetryUtil#retries(ThrowsExceptionFunction, int, long)}的重载,默认重试次数为{@link RetryUtil#DEFAULT_TIMES},重试间隔为{@link RetryUtil#DEFAULT_WAIT_TIME}
+ * 详情参见{@link RetryUtil#retries(ThrowsExceptionFunction, int, long)}
+ */
+ public static R retries(ThrowsExceptionFunction consumer) throws Exception {
+ return retries(consumer, DEFAULT_TIMES, DEFAULT_WAIT_TIME);
+ }
+
+ public static void main(String[] args) {
+ for (int i = 0; i < 5; i++) {
+ log.info("第" + (i + 1) + "次测试开始......");
+ test();
+ log.info("\n\n");
+ }
+ }
+
+ /**
+ * 模拟除0异常测试函数
+ */
+ private static void test() {
+ try {
+ int result = RetryUtil.retries((i) -> {
+ int randomInt = (int) (Math.random() * 2); // 生成0或1,模拟除0异常
+ int num = 10;
+ log.info("尝试计算" + num + "除以" + randomInt + "的结果......");
+ return num / randomInt;
+ }, 2, 1000);
+ log.info("最终结果:" + result);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/milkbox-common/src/main/java/top/milkbox/common/utils/base/CommonNode.java b/milkbox-common/src/main/java/top/milkbox/common/utils/base/CommonNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..541fa51882de60b07adb506d997eaeb65b1dfc62
--- /dev/null
+++ b/milkbox-common/src/main/java/top/milkbox/common/utils/base/CommonNode.java
@@ -0,0 +1,32 @@
+package top.milkbox.common.utils.base;
+
+import java.util.List;
+
+public interface CommonNode {
+
+ /**
+ * 获取节点的唯一标识
+ */
+ T getId();
+
+ /**
+ * 获取父节点的唯一标识
+ */
+ T getParentId();
+
+ /**
+ * 获取子节点列表
+ */
+ > List getChildrenList();
+
+ /**
+ * 初始化子节点列表
+ *
+ * // 一般只需要这么写
+ * public void initChildrenList() {
+ * this.childrenList = new ArrayList<>();
+ * }
+ *
+ */
+ void initChildrenList();
+}
\ No newline at end of file
diff --git a/milkbox-common/src/main/java/top/milkbox/common/utils/base/CycleRefException.java b/milkbox-common/src/main/java/top/milkbox/common/utils/base/CycleRefException.java
new file mode 100644
index 0000000000000000000000000000000000000000..730f3d7a03852b3fc5f291b4c149a415ef86497a
--- /dev/null
+++ b/milkbox-common/src/main/java/top/milkbox/common/utils/base/CycleRefException.java
@@ -0,0 +1,9 @@
+package top.milkbox.common.utils.base;
+
+import cn.hutool.core.util.ObjectUtil;
+
+public class CycleRefException extends Exception {
+ public CycleRefException(String message) {
+ super(ObjectUtil.isEmpty(message) ? "循环引用异常" : message);
+ }
+}
diff --git a/milkbox-common/src/main/java/top/milkbox/common/utils/base/ThrowsExceptionFunction.java b/milkbox-common/src/main/java/top/milkbox/common/utils/base/ThrowsExceptionFunction.java
new file mode 100644
index 0000000000000000000000000000000000000000..a48807131c6d1fef09ca3a99438356852ecb37a3
--- /dev/null
+++ b/milkbox-common/src/main/java/top/milkbox/common/utils/base/ThrowsExceptionFunction.java
@@ -0,0 +1,28 @@
+package top.milkbox.common.utils.base;
+
+import java.util.Objects;
+
+/**
+ * 可以抛出异常的Function,由{@link java.util.function.Function}改造而来
+ *
+ * @param 参数类型
+ */
+@FunctionalInterface
+public interface ThrowsExceptionFunction {
+
+ R apply(T t) throws Exception;
+
+ default ThrowsExceptionFunction compose(ThrowsExceptionFunction super V, ? extends T> before) {
+ Objects.requireNonNull(before);
+ return (V v) -> apply(before.apply(v));
+ }
+
+ default ThrowsExceptionFunction andThen(ThrowsExceptionFunction super R, ? extends V> after) {
+ Objects.requireNonNull(after);
+ return (T t) -> after.apply(apply(t));
+ }
+
+ static ThrowsExceptionFunction identity() {
+ return t -> t;
+ }
+}
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/controller/SysMenuController.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/controller/SysMenuController.java
index f06f1d8b4fb293b6158858f4b5a2dfdb2415ce44..c4fd28aabf1c2cc30ab866c703c4c3498f789d3f 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/controller/SysMenuController.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/controller/SysMenuController.java
@@ -111,4 +111,14 @@ public class SysMenuController {
return CommonResult.ok(sysMenuService.forest());
}
+ // TODO 应设较高权限
+ // TODO 这里的菜单应该具有条件查询功能,在查询的时候需要注意通过结果查询其上级,最终构建一棵树
+ @GetMapping("/forestAll")
+ @Operation(summary = "获取所有菜单森林", description = "获取所有菜单森林")
+ @CommonLog(value = "获取所有菜单森林", description = "获取所有菜单森林",
+ module = SysConfiguration.MODULE_NAME, type = LogTypeEnum.SELECT)
+ public CommonResult>> forestAll() {
+ return CommonResult.ok(sysMenuService.forestAll());
+ }
+
}
\ No newline at end of file
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/entity/SysMenuEntity.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/entity/SysMenuEntity.java
index 8ec44ac1d2838cd1f2412c24d6abb88d948e5850..2f2a5834d7c537c530b334154840c5db98f13195 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/entity/SysMenuEntity.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/entity/SysMenuEntity.java
@@ -67,7 +67,7 @@ public class SysMenuEntity extends CommonEntity implements CommonUtil.EntityTree
@Schema(title = "组件的导包路径",
description = "组件的导包路径。相对于组件的基准目录,基准目录单独配置,开头不加反斜杠")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
- private String component;
+ private String componentPath;
/**
* 相对于父级的路由地址;开头要加反斜杠,可以多级
@@ -93,6 +93,14 @@ public class SysMenuEntity extends CommonEntity implements CommonUtil.EntityTree
@TableField(typeHandler = JacksonTypeHandler.class, updateStrategy = FieldStrategy.ALWAYS)
private Object extend;
+ /**
+ * 布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效
+ */
+ @Schema(title = "布局页面的组件名称",
+ description = "布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效")
+ @TableField(updateStrategy = FieldStrategy.ALWAYS)
+ private String layoutName;
+
/**
* 是否可见;隐藏后可以访问页面,但不在菜单列表显示。1可见,0不可见
*/
@@ -102,10 +110,10 @@ public class SysMenuEntity extends CommonEntity implements CommonUtil.EntityTree
private Boolean isShow;
/**
- * 类型;菜单MENU或目录CATALOG
+ * 类型;页面PAGE或目录CATALOG
*/
@Schema(title = "类型",
- description = "类型。菜单MENU或目录CATALOG")
+ description = "类型;页面PAGE或目录CATALOG")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private SysMenuTypeEnum type;
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/enums/SysMenuTypeEnum.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/enums/SysMenuTypeEnum.java
index a67bfa2e3480e14153563db434f88e382af4a71a..4ab244f6c9e951b18ab041cd10febcfc83366363 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/enums/SysMenuTypeEnum.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/enums/SysMenuTypeEnum.java
@@ -13,7 +13,7 @@ import lombok.AllArgsConstructor;
@AllArgsConstructor // 如果使用此注解,请勿随意修改成员变量的定义顺序
public enum SysMenuTypeEnum {
- MENU("菜单", "MENU"),
+ MENU("页面", "PAGE"),
CATALOG("目录", "CATALOG");
private final String label;
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuAddParam.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuAddParam.java
index 08d4bfe7d97427cfc858a7d7c4d2808e90e8f509..89d175a09dd1c632dfca76b3f52607371e1a9e64 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuAddParam.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuAddParam.java
@@ -51,7 +51,7 @@ public class SysMenuAddParam implements Serializable {
*/
@Schema(title = "组件的导包路径",
description = "组件的导包路径。相对于组件的基准目录,基准目录单独配置,开头不加反斜杠")
- private String component;
+ private String componentPath;
/**
* 相对于父级的路由地址;开头要加反斜杠,可以多级
@@ -74,6 +74,13 @@ public class SysMenuAddParam implements Serializable {
description = "扩展信息。Json格式")
private Object extend;
+ /**
+ * 布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效
+ */
+ @Schema(title = "布局页面的组件名称",
+ description = "布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效")
+ private String layoutName;
+
/**
* 是否可见;隐藏后可以访问页面,但不在菜单列表显示。1可见,0不可见
*/
@@ -82,10 +89,10 @@ public class SysMenuAddParam implements Serializable {
private Boolean isShow;
/**
- * 类型;菜单MENU或目录CATALOG
+ * 类型;页面PAGE或目录CATALOG
*/
@Schema(title = "类型",
- description = "类型。菜单MENU或目录CATALOG")
+ description = "类型;页面PAGE或目录CATALOG")
private SysMenuTypeEnum type;
/**
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuEditParam.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuEditParam.java
index 57cb05f1977f3ae86e7d108eede93c54dbe59d22..5862807f4fb8a56f9ab060fd39280fd430214e86 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuEditParam.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuEditParam.java
@@ -60,7 +60,7 @@ public class SysMenuEditParam implements Serializable {
*/
@Schema(title = "组件的导包路径",
description = "组件的导包路径。相对于组件的基准目录,基准目录单独配置,开头不加反斜杠")
- private String component;
+ private String componentPath;
/**
* 相对于父级的路由地址;开头要加反斜杠,可以多级
@@ -83,6 +83,13 @@ public class SysMenuEditParam implements Serializable {
description = "扩展信息。Json格式")
private Object extend;
+ /**
+ * 布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效
+ */
+ @Schema(title = "布局页面的组件名称",
+ description = "布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效")
+ private String layoutName;
+
/**
* 是否可见;隐藏后可以访问页面,但不在菜单列表显示。1可见,0不可见
*/
@@ -91,10 +98,10 @@ public class SysMenuEditParam implements Serializable {
private Boolean isShow;
/**
- * 类型;菜单MENU或目录CATALOG
+ * 类型;页面PAGE或目录CATALOG
*/
@Schema(title = "类型",
- description = "类型。菜单MENU或目录CATALOG")
+ description = "类型;页面PAGE或目录CATALOG")
private SysMenuTypeEnum type;
/**
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuPageParam.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuPageParam.java
index 6cbbc4d2f877030fd6624708edd60fbe3afcc1de..4315530574d1d99cb87b996a0d25d60f1faeb72a 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuPageParam.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/param/SysMenuPageParam.java
@@ -49,7 +49,7 @@ public class SysMenuPageParam extends CommonPageParam implements Serializable {
*/
@Schema(title = "组件的导包路径",
description = "组件的导包路径。相对于组件的基准目录,基准目录单独配置,开头不加反斜杠")
- private String component;
+ private String componentPath;
/**
* 相对于父级的路由地址;开头要加反斜杠,可以多级
@@ -72,6 +72,13 @@ public class SysMenuPageParam extends CommonPageParam implements Serializable {
description = "扩展信息。Json格式")
private Object extend;
+ /**
+ * 布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效
+ */
+ @Schema(title = "布局页面的组件名称",
+ description = "布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效")
+ private String layoutName;
+
/**
* 是否可见;隐藏后可以访问页面,但不在菜单列表显示。1可见,0不可见
*/
@@ -80,10 +87,10 @@ public class SysMenuPageParam extends CommonPageParam implements Serializable {
private Boolean isShow;
/**
- * 类型;菜单MENU或目录CATALOG
+ * 类型;页面PAGE或目录CATALOG
*/
@Schema(title = "类型",
- description = "类型。菜单MENU或目录CATALOG")
+ description = "类型;页面PAGE或目录CATALOG")
private SysMenuTypeEnum type;
/**
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/SysMenuService.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/SysMenuService.java
index 6a1e9e875fbb4615a73eac09c5634dc2331e4461..70fa65ce52e5fdfcee93f31bcf670ededf827260 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/SysMenuService.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/SysMenuService.java
@@ -91,4 +91,11 @@ public interface SysMenuService extends CommonService {
* @return 菜单森林
*/
List> forest();
+
+ /**
+ * 获取所有菜单森林
+ *
+ * @return 菜单森林
+ */
+ List> forestAll();
}
\ No newline at end of file
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/impl/SysMenuServiceImpl.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/impl/SysMenuServiceImpl.java
index ba41be1446bec804005e712d2d431f653327da0a..9ea0cbeaf544f62048332c834328441dd4621c5d 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/impl/SysMenuServiceImpl.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/service/impl/SysMenuServiceImpl.java
@@ -210,7 +210,7 @@ public class SysMenuServiceImpl extends CommonServiceImpl userMenuIdList = sysRelationshipService.findObjectIdListByTargetId(
+ List userMenuIdList = sysRelationshipService.findTargetIdListByObjectId(
loginUserId, SysRelationshipTypeEnum.SYS_USER_RELATE_SYS_MENU);
// 合并两个集合
@@ -231,4 +231,9 @@ public class SysMenuServiceImpl extends CommonServiceImpl> forestAll() {
+ return CommonUtil.toTree(super.list(), 0);
+ }
+
}
\ No newline at end of file
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/vo/SysMenuVo.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/vo/SysMenuVo.java
index 75d62a3db921d876636089cbe6a1ecde30171348..d12bfaacd4ca515c8cd357313480a977d840e5b0 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/vo/SysMenuVo.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/menu/vo/SysMenuVo.java
@@ -57,7 +57,7 @@ public class SysMenuVo extends CommonVo implements Serializable {
*/
@Schema(title = "组件的导包路径",
description = "组件的导包路径。相对于组件的基准目录,基准目录单独配置,开头不加反斜杠")
- private String component;
+ private String componentPath;
/**
* 相对于父级的路由地址;开头要加反斜杠,可以多级
@@ -80,6 +80,13 @@ public class SysMenuVo extends CommonVo implements Serializable {
description = "扩展信息。Json格式")
private Object extend;
+ /**
+ * 布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效
+ */
+ @Schema(title = "布局页面的组件名称",
+ description = "布局页面的组件名称。布局页面的组件名称,表示当前记录在哪个布局页面内。仅顶级有效")
+ private String layoutName;
+
/**
* 是否可见;隐藏后可以访问页面,但不在菜单列表显示。1可见,0不可见
*/
@@ -88,10 +95,10 @@ public class SysMenuVo extends CommonVo implements Serializable {
private Boolean isShow;
/**
- * 类型;菜单MENU或目录CATALOG
+ * 类型;页面PAGE或目录CATALOG
*/
@Schema(title = "类型",
- description = "类型。菜单MENU或目录CATALOG")
+ description = "类型;页面PAGE或目录CATALOG")
private SysMenuTypeEnum type;
/**
diff --git a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/user/service/impl/SysUserServiceImpl.java b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/user/service/impl/SysUserServiceImpl.java
index dad219be0c2e334e8811f6e316384766519b6906..2313d7c957b32578ae3005256138c9e5b03e58ba 100644
--- a/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/user/service/impl/SysUserServiceImpl.java
+++ b/milkbox-service/service-sys/src/main/java/top/milkbox/sys/modular/user/service/impl/SysUserServiceImpl.java
@@ -163,7 +163,7 @@ public class SysUserServiceImpl extends CommonServiceImpl