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 before) { + Objects.requireNonNull(before); + return (V v) -> apply(before.apply(v)); + } + + default ThrowsExceptionFunction andThen(ThrowsExceptionFunction 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