diff --git a/README.md b/README.md index 6557b21cd..96dc4ca5e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/
| 权限注解 | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验
角色与权限校验支持多种条件 如 `AND` `OR` 或 `权限 OR 角色` 等复杂表达式 | 只支持是否存在匹配 | | 三方鉴权 | 采用 JustAuth 第三方登录组件 支持微信、钉钉等数十种三方认证 | 无 | | 关系数据库支持 | 原生支持 MySQL、Oracle、PostgreSQL、SQLServer
可同时使用异构切换(支持其他 mybatis-plus 支持的所有数据库 只需要增加jdbc依赖即可使用 达梦金仓等均有成功案例) | 支持 Mysql、Oracle 不支持同时使用、不支持异构切换 | -| 缓存数据库 | 支持 Redis 5-7 支持大部分新功能特性 如 分布式限流、分布式队列 | Redis 简单 get set 支持 | +| 缓存数据库 | 支持 Redis >= 6 支持大部分新功能特性 如 分布式限流、分布式队列 | Redis 简单 get set 支持 | | Redis客户端 | 采用 Redisson Redis官方推荐 基于Netty的客户端工具
支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan
支持单机、哨兵、单主集群、多主集群等模式 | Lettuce + RedisTemplate 支持模式少 工具使用繁琐
连接池采用 common-pool Bug多经常性出问题 | | 缓存注解 | 采用 Spring-Cache 注解 对其扩展了实现支持了更多功能
例如 过期时间 最大空闲时间 组最大长度等 只需一个注解即可完成数据自动缓存 | 需手动编写Redis代码逻辑 | | ORM框架 | 采用 Mybatis-Plus 基于对象几乎不用写SQL全java操作 功能强大插件众多
例如多租户插件 分页插件 乐观锁插件等等 | 采用 Mybatis 基于XML需要手写SQL | diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java index b1f546d36..4e07ac5f9 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/CacheNames.java @@ -47,7 +47,7 @@ public interface CacheNames { String SYS_USER_NAME = "sys_user_name#30d"; /** - * 用户名称 + * 用户昵称 */ String SYS_NICKNAME = "sys_nickname#30d"; diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java index 2f20b21f7..6001331d5 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/FlowCopyDTO.java @@ -23,8 +23,8 @@ public class FlowCopyDTO implements Serializable { private Long userId; /** - * 用户名称 + * 用户昵称 */ - private String userName; + private String nickName; } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java index fa3565789..115b42f68 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/StartProcessDTO.java @@ -48,7 +48,8 @@ public class StartProcessDTO implements Serializable { public Map getVariables() { if (variables == null) { - return new HashMap<>(16); + variables = new HashMap<>(16); + return variables; } variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); return variables; diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java index 43d8c3c11..4f6534ffa 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/domain/dto/UserOnlineDTO.java @@ -30,7 +30,7 @@ public class UserOnlineDTO implements Serializable { private String deptName; /** - * 用户名称 + * 用户账号 */ private String userName; diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java index eefeef011..c61b1ed90 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/UserService.java @@ -21,18 +21,18 @@ public interface UserService { String selectUserNameById(Long userId); /** - * 通过用户ID查询用户账户 + * 通过用户ID查询用户昵称 * * @param userId 用户ID - * @return 用户名称 + * @return 用户昵称 */ String selectNicknameById(Long userId); /** - * 通过用户ID查询用户账户 + * 通过用户ID查询用户昵称 * * @param userIds 用户ID 多个用逗号隔开 - * @return 用户名称 + * @return 用户昵称 */ String selectNicknameByIds(String userIds); @@ -93,11 +93,11 @@ public interface UserService { List selectUsersByPostIds(List postIds); /** - * 根据用户 ID 列表查询用户名称映射关系 + * 根据用户 ID 列表查询用户昵称映射关系 * * @param userIds 用户 ID 列表 - * @return Map,其中 key 为用户 ID,value 为对应的用户名称 + * @return Map,其中 key 为用户 ID,value 为对应的用户昵称 */ - Map selectUserNamesByIds(List userIds); + Map selectUserNicksByIds(List userIds); } diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java index f53cb5c47..6af231ee7 100644 --- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java @@ -7,6 +7,8 @@ import io.swagger.v3.oas.models.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.doc.config.properties.SpringDocProperties; +import org.dromara.common.doc.core.resolver.JavadocResolver; +import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver; import org.dromara.common.doc.handler.OpenApiHandler; import org.springdoc.core.configuration.SpringDocConfiguration; import org.springdoc.core.customizers.OpenApiBuilderCustomizer; @@ -84,8 +86,9 @@ public class SpringDocConfig { SecurityService securityParser, SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, Optional> openApiBuilderCustomisers, - Optional> serverBaseUrlCustomisers, Optional javadocProvider) { - return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider); + Optional> serverBaseUrlCustomisers, Optional javadocProvider, + List javadocResolvers) { + return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider, javadocResolvers); } /** @@ -112,6 +115,14 @@ public class SpringDocConfig { }; } + /** + * 注册SaToken JavaDoc权限注解解析器 + */ + @Bean + public JavadocResolver saTokenAnnotationJavadocResolver() { + return new SaTokenAnnotationMetadataJavadocResolver(); + } + /** * 单独使用一个类便于判断 解决springdoc路径拼接重复问题 * diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java new file mode 100644 index 000000000..e0782a20c --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java @@ -0,0 +1,175 @@ +package org.dromara.common.doc.core.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.Data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 存储权限框架注解解析后的权限和角色信息 + * + * @author AprilWind + */ +@Data +@JsonInclude(Include.NON_EMPTY) +public class SaTokenSecurityMetadata { + + /** + * 权限校验信息列表(对应 @SaCheckPermission 注解) + */ + private List permissions = new ArrayList<>(); + + /** + * 角色校验信息列表(对应 @SaCheckRole 注解) + */ + private List roles = new ArrayList<>(); + + /** + * 是否忽略校验(对应 @SaIgnore 注解) + */ + private boolean ignore = false; + + /** + * 添加权限信息 + * + * @param values 权限值数组 + * @param mode 校验模式(AND/OR) + * @param type 权限类型 + * @param orRoles 或角色数组 + */ + public void addPermission(String[] values, String mode, String type, String[] orRoles) { + if (values != null && values.length > 0) { + AuthInfo authInfo = new AuthInfo(); + authInfo.setValues(values); + authInfo.setMode(mode); + authInfo.setType(type); + if (orRoles != null && orRoles.length > 0) { + authInfo.setOrValues(orRoles); + authInfo.setOrType("role"); + } + this.permissions.add(authInfo); + } + } + + /** + * 添加角色信息 + * + * @param values 角色值数组 + * @param mode 校验模式(AND/OR) + * @param type 角色类型 + */ + public void addRole(String[] values, String mode, String type) { + if (values != null && values.length > 0) { + AuthInfo authInfo = new AuthInfo(); + authInfo.setValues(values); + authInfo.setMode(mode); + authInfo.setType(type); + this.roles.add(authInfo); + } + } + + /** + * 生成 Markdown 结构的权限说明 + * + * @return Markdown 文本 + */ + public String toMarkdownString() { + StringBuilder sb = new StringBuilder(); + sb.append("

访问权限


"); + + if (ignore) { + sb.append("> **权限策略**:忽略权限检查
"); + return sb.toString(); + } + + if (!ignore && permissions.isEmpty() && roles.isEmpty()){ + sb.append("> **权限策略**:需要登录

"); + return sb.toString(); + } + + if (!permissions.isEmpty()) { + sb.append("**权限校验:**

"); + + permissions.forEach(p -> { + String permTags = Arrays.stream(p.getValues()) + .map(v -> "`" + v + "`") + .collect(Collectors.joining(p.getModeSymbol())); + + sb.append("- ").append(permTags).append("
"); + + if (p.getOrValues() != null && p.getOrValues().length > 0) { + String orTags = Arrays.stream(p.getOrValues()) + .map(v -> "`" + v + "`") + .collect(Collectors.joining(p.getModeSymbol())); + sb.append(" - 或角色:").append(orTags).append("
"); + } + }); + + sb.append("
"); + } + + if (!roles.isEmpty()) { + sb.append("**角色校验:**

"); + + roles.forEach(r -> { + + String roleTags = Arrays.stream(r.getValues()) + .map(v -> "`" + v + "`") + .collect(Collectors.joining(r.getModeSymbol())); + + sb.append("- ").append(roleTags).append("
"); + }); + } + + return sb.toString().trim(); + } + + /** + * 认证信息 + */ + @Data + @JsonInclude(Include.NON_EMPTY) + public static class AuthInfo { + + /** + * 权限或角色值数组 + */ + private String[] values; + + /** + * 校验模式(AND/OR) + */ + private String mode; + + /** + * 类型说明 + */ + private String type; + + /** + * 或权限/角色值数组(用于权限校验时的或角色校验) + */ + private String[] orValues; + + /** + * 或值的类型(role/permission) + */ + private String orType; + + /** + * 重写mode的获取方法,返回符号而非文字 + * @return AND→&,OR→|,默认→& + */ + public String getModeSymbol() { + if (mode == null) { + return " & "; // 默认AND,返回& + } + return "AND".equalsIgnoreCase(mode) ? " & " : " | "; + } + + } +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/AbstractMetadataJavadocResolver.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/AbstractMetadataJavadocResolver.java new file mode 100644 index 000000000..e9333c044 --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/AbstractMetadataJavadocResolver.java @@ -0,0 +1,163 @@ +package org.dromara.common.doc.core.resolver; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.util.ClassLoaderUtil; +import io.swagger.v3.oas.models.Operation; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Map; +import java.util.function.Supplier; + +/** + * 抽象元数据 Javadoc 解析器 + * + * @param 元数据类型 + * @author 秋辞未寒 + */ +public abstract class AbstractMetadataJavadocResolver implements JavadocResolver { + + public static final int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; + public static final int LOWEST_PRECEDENCE = Integer.MAX_VALUE; + + private final Supplier metadataProvider; + + private final int order; + + public AbstractMetadataJavadocResolver(Supplier metadataProvider) { + this(metadataProvider, LOWEST_PRECEDENCE); + } + + public AbstractMetadataJavadocResolver(Supplier metadataProvider, int order) { + this.metadataProvider = metadataProvider; + this.order = order; + } + + @Override + public int getOrder() { + return order; + } + + @Override + public String resolve(HandlerMethod handlerMethod, Operation operation) { + return resolve(handlerMethod, operation, metadataProvider.get()); + } + + /** + * 执行解析并返回解析到的 Javadoc 内容 + * @param handlerMethod 处理器方法 + * @param operation Swagger Operation实例 + * @param metadata 元信息 + * @return 解析到的 Javadoc 内容 + */ + public abstract String resolve(HandlerMethod handlerMethod, Operation operation, M metadata); + + /** + * 检查处理器方法所属的类上是否存在注解 + * @param handlerMethod 处理器方法 + * @param annotationClass 注解类 + * @return 是否存在注解 + */ + public boolean hasClassAnnotation(HandlerMethod handlerMethod,Class annotationClass){ + return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationClass); + } + + /** + * 检查处理器方法所属的类上是否存在注解 + * @param handlerMethod 处理器方法 + * @param annotationTypeName 注解类名称 + * @return 是否存在注解 + */ + public boolean hasClassAnnotation(HandlerMethod handlerMethod, String annotationTypeName){ + return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationTypeName); + } + + /** + * 检查处理器方法上是否存在注解 + * @param handlerMethod 处理器方法 + * @param annotationClass 注解类 + * @return 是否存在注解 + */ + public boolean hasMethodAnnotation(HandlerMethod handlerMethod,Class annotationClass){ + return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationClass); + } + + /** + * 检查处理器方法上是否存在注解 + * @param handlerMethod 处理器方法 + * @param annotationTypeName 注解类名称 + * @return 是否存在注解 + */ + public boolean hasMethodAnnotation(HandlerMethod handlerMethod, String annotationTypeName){ + return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationTypeName); + } + + /** + * 检查处理器方法上是否存在注解 + * @param handlerMethod 处理器方法 + * @param annotationClass 注解类 + * @return 是否存在注解 + */ + public boolean hasAnnotation(HandlerMethod handlerMethod,Class annotationClass){ + return this.hasClassAnnotation(handlerMethod, annotationClass) || this.hasMethodAnnotation(handlerMethod, annotationClass); + } + + /** + * 检查处理器方法上是否存在注解 + * @param handlerMethod 处理器方法 + * @param annotationTypeName 注解类名称 + * @return 是否存在注解 + */ + public boolean hasAnnotation(HandlerMethod handlerMethod, String annotationTypeName){ + return this.hasClassAnnotation(handlerMethod, annotationTypeName) || this.hasMethodAnnotation(handlerMethod, annotationTypeName); + } + + /** + * 获取处理器方法所属类上的注解的值 + * @param handlerMethod 处理器方法 + * @param annotationClass 注解类 + * @return 注解的值 + */ + public Map getClassAnnotationValueMap(HandlerMethod handlerMethod, Class annotationClass) { + return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass); + } + + /** + * 获取处理器方法所属类上的注解的值 + * @param handlerMethod 处理器方法 + * @param annotationClassName 注解类名称 + * @return 注解的值 + */ + @SuppressWarnings("unchecked") + public Map getClassAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) { + Class annotationClass = (Class) ClassLoaderUtil.loadClass(annotationClassName, false); + return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass); + } + + /** + * 获取处理器方法上的注解的值 + * @param handlerMethod 处理器方法 + * @param annotationClass 注解类 + * @return 注解的值 + */ + public Map getMethodAnnotationValueMap(HandlerMethod handlerMethod, Class annotationClass) { + return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass); + } + + /** + * 获取处理器方法所属类上的注解的值 + * @param handlerMethod 处理器方法 + * @param annotationClassName 注解类名称 + * @return 注解的值 + */ + @SuppressWarnings("unchecked") + public Map getMethodAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) { + Class annotationClass = (Class) ClassLoaderUtil.loadClass(annotationClassName, false); + return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass); + } + + private Map getAnnotationValueMap(AnnotatedElement annotatedElement, Class annotationClass) { + return AnnotationUtil.getAnnotationValueMap(annotatedElement, annotationClass); + } +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/JavadocResolver.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/JavadocResolver.java new file mode 100644 index 000000000..1e295b8a1 --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/JavadocResolver.java @@ -0,0 +1,51 @@ +package org.dromara.common.doc.core.resolver; + +import io.swagger.v3.oas.models.Operation; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.Ordered; +import org.springframework.web.method.HandlerMethod; + +/** + * Javadoc解析器接口 + * + * @author echo + * @author 秋辞未寒 + */ +public interface JavadocResolver extends Comparable, Ordered { + + /** + * 检查解析器是否支持解析 HandlerMethod + * @param handlerMethod 处理器方法 + * @return 是否支持解析 + */ + boolean supports(HandlerMethod handlerMethod); + + /** + * 执行解析并返回解析到的 Javadoc 内容 + * @param handlerMethod 处理器方法 + * @param operation Swagger Operation实例 + * @return 解析到的 Javadoc 内容 + */ + String resolve(HandlerMethod handlerMethod, Operation operation); + + /** + * 获取解析器优先级 + */ + default int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + /** + * 获取解析器的名称 + * + * @return 解析器名称 + */ + default String getName() { + return this.getClass().getSimpleName(); + } + + @Override + default int compareTo(@NotNull JavadocResolver o) { + return Integer.compare(getOrder(), o.getOrder()); + } +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/SaTokenAnnotationMetadataJavadocResolver.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/SaTokenAnnotationMetadataJavadocResolver.java new file mode 100644 index 000000000..9e09619de --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/resolver/SaTokenAnnotationMetadataJavadocResolver.java @@ -0,0 +1,164 @@ +package org.dromara.common.doc.core.resolver; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ClassLoaderUtil; +import io.swagger.v3.oas.models.Operation; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.doc.core.model.SaTokenSecurityMetadata; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * 基于JavaDoc的SaToken权限解析器 + * + * @author echo + * @author 秋辞未寒 + */ +@SuppressWarnings("unchecked") +@Slf4j +public class SaTokenAnnotationMetadataJavadocResolver extends AbstractMetadataJavadocResolver { + + /** + * 默认元数据提供者,每次解析都会创建一个新的元数据对象 + */ + public static final Supplier DEFAULT_METADATA_PROVIDER = SaTokenSecurityMetadata::new; + + private static final String BASE_CLASS_NAME = "cn.dev33.satoken.annotation"; + private static final String SA_CHECK_ROLE_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckRole"; + private static final String SA_CHECK_PERMISSION_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckPermission"; + private static final String SA_IGNORE_CLASS_NAME = BASE_CLASS_NAME + ".SaIgnore"; + private static final String SA_CHECK_LOGIN_NAME = BASE_CLASS_NAME + ".SaCheckLogin"; + + private static final Class SA_CHECK_ROLE_CLASS; + private static final Class SA_CHECK_PERMISSION_CLASS; + private static final Class SA_IGNORE_CLASS; + private static final Class SA_CHECK_LOGIN_CLASS; + + + static { + // 通过类加载器去加载注解类Class实例 + SA_CHECK_ROLE_CLASS = (Class) ClassLoaderUtil.loadClass(SA_CHECK_ROLE_CLASS_NAME, false); + SA_CHECK_PERMISSION_CLASS = (Class) ClassLoaderUtil.loadClass(SA_CHECK_PERMISSION_CLASS_NAME, false); + SA_IGNORE_CLASS = (Class) ClassLoaderUtil.loadClass(SA_IGNORE_CLASS_NAME, false); + SA_CHECK_LOGIN_CLASS = (Class) ClassLoaderUtil.loadClass(SA_CHECK_LOGIN_NAME, false); + if (log.isDebugEnabled()) { + log.debug("SaTokenAnnotationJavadocResolver init success, load annotation class: {}", List.of(SA_CHECK_ROLE_CLASS, SA_CHECK_PERMISSION_CLASS, SA_IGNORE_CLASS, SA_CHECK_LOGIN_CLASS)); + } + } + + public SaTokenAnnotationMetadataJavadocResolver() { + this(DEFAULT_METADATA_PROVIDER); + } + + public SaTokenAnnotationMetadataJavadocResolver(Supplier metadataProvider) { + super(metadataProvider); + } + + public SaTokenAnnotationMetadataJavadocResolver(int order) { + this(DEFAULT_METADATA_PROVIDER,order); + } + + public SaTokenAnnotationMetadataJavadocResolver(Supplier metadataProvider, int order) { + super(metadataProvider,order); + } + + @Override + public boolean supports(HandlerMethod handlerMethod) { + return hasAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS) || hasAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS) || hasAnnotation(handlerMethod, SA_IGNORE_CLASS); + } + + @Override + public String resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata) { + // 检查是否忽略校验 + if(hasAnnotation(handlerMethod, SA_IGNORE_CLASS_NAME)){ + metadata.setIgnore(true); + return metadata.toMarkdownString(); + } + + // 解析权限校验 + resolvePermissionCheck(handlerMethod, metadata); + + // 解析角色校验 + resolveRoleCheck(handlerMethod, metadata); + return metadata.toMarkdownString(); + } + + /** + * 解析权限校验 + */ + private void resolvePermissionCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) { + // 解析获取方法上的注解角色信息 + if (hasMethodAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) { + Map annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS); + resolvePermissionAnnotation(metadata, annotationValueMap); + } + // 解析获取类上的注解角色信息 + if (hasClassAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) { + Map annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS); + resolvePermissionAnnotation(metadata, annotationValueMap); + } + } + + /** + * 解析权限注解 + */ + private void resolvePermissionAnnotation(SaTokenSecurityMetadata metadata, Map annotationValueMap) { + try { + // 反射获取注解属性 + Object value = annotationValueMap.get( "value"); + Object mode = annotationValueMap.get( "mode"); + Object type = annotationValueMap.get( "type"); + Object orRole = annotationValueMap.get( "orRole"); + + String[] values = Convert.toStrArray(value); + String modeStr = mode != null ? mode.toString() : "AND"; + String typeStr = type != null ? type.toString() : ""; + String[] orRoles = Convert.toStrArray(orRole); + + metadata.addPermission(values, modeStr, typeStr, orRoles); + } catch (Exception ignore) { + // 忽略解析错误 + } + } + + /** + * 解析角色校验 + */ + private void resolveRoleCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) { + // 解析获取方法上的注解角色信息 + if (hasMethodAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) { + Map annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS); + resolveRoleAnnotation(metadata, annotationValueMap); + } + // 解析获取类上的注解角色信息 + if (hasClassAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) { + Map annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS); + resolveRoleAnnotation(metadata, annotationValueMap); + } + } + + /** + * 解析角色注解 + */ + private void resolveRoleAnnotation(SaTokenSecurityMetadata metadata, Map annotationValueMap) { + try { + // 反射获取注解属性 + Object value = annotationValueMap.get("value"); + Object mode = annotationValueMap.get("mode"); + Object type = annotationValueMap.get("type"); + + String[] values = Convert.toStrArray(value); + String modeStr = mode != null ? mode.toString() : "AND"; + String typeStr = type != null ? type.toString() : ""; + + metadata.addRole(values, modeStr, typeStr); + } catch (Exception ignore) { + // 忽略解析错误 + } + } + +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java index 56b73694d..32be73227 100644 --- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java @@ -12,6 +12,7 @@ import io.swagger.v3.oas.models.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.dromara.common.core.utils.StreamUtils; +import org.dromara.common.doc.core.resolver.JavadocResolver; import org.springdoc.core.customizers.OpenApiBuilderCustomizer; import org.springdoc.core.customizers.ServerBaseUrlCustomizer; import org.springdoc.core.properties.SpringDocConfigProperties; @@ -83,6 +84,11 @@ public class OpenApiHandler extends OpenAPIService { */ private final PropertyResolverUtils propertyResolverUtils; + /** + * Javadoc解析器接口 + */ + private final List javadocResolvers; + /** * The javadoc provider. */ @@ -123,7 +129,8 @@ public class OpenApiHandler extends OpenAPIService { SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, Optional> openApiBuilderCustomizers, Optional> serverBaseUrlCustomizers, - Optional javadocProvider) { + Optional javadocProvider, + List javadocResolvers) { super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); if (openAPI.isPresent()) { this.openAPI = openAPI.get(); @@ -140,6 +147,7 @@ public class OpenApiHandler extends OpenAPIService { this.openApiBuilderCustomisers = openApiBuilderCustomizers; this.serverBaseUrlCustomizers = serverBaseUrlCustomizers; this.javadocProvider = javadocProvider; + this.javadocResolvers = javadocResolvers == null ? new ArrayList<>() : javadocResolvers; if (springDocConfigProperties.isUseFqn()) TypeNameResolver.std.setUseFqn(true); } @@ -220,6 +228,22 @@ public class OpenApiHandler extends OpenAPIService { securityParser.buildSecurityRequirement(securityRequirements, operation); } + if (javadocProvider.isPresent()) { + String description = javadocProvider.get().getMethodJavadocDescription(handlerMethod.getMethod()); + String summary = javadocProvider.get().getFirstSentence(description); + if (StringUtils.isNotBlank(description)){ + operation.setSummary(summary); + } + // 调用解析器提取JavaDoc中的权限信息 + if (javadocResolvers != null && !javadocResolvers.isEmpty()) { + for (JavadocResolver resolver : javadocResolvers) { + String desc = resolver.resolve(handlerMethod, operation); + description = description + desc; + } + operation.setDescription(description); + } + } + return operation; } diff --git a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java index bcc2f4c9f..617fb7979 100644 --- a/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java +++ b/ruoyi-common/ruoyi-common-encrypt/src/main/java/org/dromara/common/encrypt/interceptor/MybatisEncryptInterceptor.java @@ -6,10 +6,7 @@ import cn.hutool.core.util.ObjectUtil; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.parameter.ParameterHandler; -import org.apache.ibatis.plugin.Interceptor; -import org.apache.ibatis.plugin.Intercepts; -import org.apache.ibatis.plugin.Invocation; -import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.plugin.*; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.encrypt.annotation.EncryptField; import org.dromara.common.encrypt.core.EncryptContext; @@ -42,19 +39,19 @@ public class MybatisEncryptInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { - return invocation; - } - - @Override - public Object plugin(Object target) { + Object target = invocation.getTarget(); if (target instanceof ParameterHandler parameterHandler) { - // 进行加密操作 Object parameterObject = parameterHandler.getParameterObject(); if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) { this.encryptHandler(parameterObject); } } - return target; + return invocation.proceed(); + } + + @Override + public Object plugin(Object target) { + return Plugin.wrap(target, this); } /** diff --git a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java index 76959259e..4c11af845 100644 --- a/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java +++ b/ruoyi-common/ruoyi-common-sensitive/src/main/java/org/dromara/common/sensitive/handler/SensitiveHandler.java @@ -23,9 +23,24 @@ import java.util.Objects; @Slf4j public class SensitiveHandler extends ValueSerializer { - private SensitiveStrategy strategy; - private String[] roleKey; - private String[] perms; + private final SensitiveStrategy strategy; + private final String[] roleKey; + private final String[] perms; + + /** + * 提供给 jackson 创建上下文序列化器时使用 不然会报错 + */ + public SensitiveHandler() { + this.strategy = null; + this.roleKey = null; + this.perms = null; + } + + public SensitiveHandler(SensitiveStrategy strategy, String[] strings, String[] perms) { + this.strategy = strategy; + this.roleKey = strings; + this.perms = perms; + } @Override public void serialize(String value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException { @@ -46,10 +61,7 @@ public class SensitiveHandler extends ValueSerializer { public ValueSerializer createContextual(SerializationContext ctxt, BeanProperty property) { Sensitive annotation = property.getAnnotation(Sensitive.class); if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) { - this.strategy = annotation.strategy(); - this.roleKey = annotation.roleKey(); - this.perms = annotation.perms(); - return this; + return new SensitiveHandler(annotation.strategy(), annotation.roleKey(), annotation.perms()); } return super.createContextual(ctxt, property); } diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java index c084ea1a0..50dce798a 100644 --- a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java +++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/constant/TransConstant.java @@ -13,7 +13,7 @@ public interface TransConstant { String USER_ID_TO_NAME = "user_id_to_name"; /** - * 用户id转用户名称 + * 用户id转用户昵称 */ String USER_ID_TO_NICKNAME = "user_id_to_nickname"; diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java index a8511055b..3393d03ee 100644 --- a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java +++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/handler/TranslationHandler.java @@ -29,7 +29,18 @@ public class TranslationHandler extends ValueSerializer { */ public static final Map> TRANSLATION_MAPPER = new ConcurrentHashMap<>(); - private Translation translation; + private final Translation translation; + + /** + * 提供给 jackson 创建上下文序列化器时使用 不然会报错 + */ + public TranslationHandler() { + this.translation = null; + } + + public TranslationHandler(Translation translation) { + this.translation = translation; + } @Override public void serialize(Object value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException { @@ -61,8 +72,7 @@ public class TranslationHandler extends ValueSerializer { public ValueSerializer createContextual(SerializationContext ctxt, BeanProperty property) { Translation translation = property.getAnnotation(Translation.class); if (Objects.nonNull(translation)) { - this.translation = translation; - return this; + return new TranslationHandler(translation); } return super.createContextual(ctxt, property); } diff --git a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java index d1720f714..06be37fcc 100644 --- a/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java +++ b/ruoyi-common/ruoyi-common-translation/src/main/java/org/dromara/common/translation/core/impl/NicknameTranslationImpl.java @@ -7,7 +7,7 @@ import org.dromara.common.translation.constant.TransConstant; import org.dromara.common.translation.core.TranslationInterface; /** - * 用户名称翻译实现 + * 用户昵称翻译实现 * * @author may */ diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java new file mode 100644 index 000000000..66fd3cd84 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java @@ -0,0 +1,198 @@ +package org.dromara.demo.controller; + +import cn.dev33.satoken.annotation.*; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * SaToken 权限测试 接口文档输出测试 + * + * @author AprilWind + */ +@Slf4j +@RestController +@RequestMapping("/demo/saTokenDoc") +public class SaTokenTestController { + + // ====================== 基础场景:单一校验规则 ====================== + + /** + * 场景1:仅登录校验(无角色/权限限制,只需登录态) + */ + @SaCheckLogin + @GetMapping("/basic/loginOnly") + public R loginOnly() { + log.info("【场景1】仅登录校验通过"); + return R.ok("仅登录校验通过,无需角色/权限"); + } + + /** + * 场景2:单一角色校验(AND模式,默认) + */ + @SaCheckRole("admin") + @GetMapping("/basic/singleRole") + public R singleRole() { + log.info("【场景2】单一角色(admin)校验通过"); + return R.ok("拥有admin角色,校验通过"); + } + + /** + * 场景3:单一权限校验(AND模式,默认) + */ + @SaCheckPermission("system:user:view") + @GetMapping("/basic/singlePermission") + public R singlePermission() { + log.info("【场景3】单一权限(system:user:view)校验通过"); + return R.ok("拥有system:user:view权限,校验通过"); + } + + /** + * 场景4:忽略所有权限校验(SaIgnore优先级最高) + */ + @SaIgnore + @SaCheckRole("none_exist") // 该注解会被忽略 + @GetMapping("/basic/ignoreAll") + public R ignoreAll() { + log.info("【场景4】SaIgnore忽略所有权限校验"); + return R.ok("SaIgnore生效,所有权限校验被忽略"); + } + + // ====================== 进阶场景:多条件组合(AND/OR) ====================== + + /** + * 场景5:多角色AND模式(必须同时拥有所有角色) + */ + @SaCheckRole(value = {"admin", "operator"}, mode = SaMode.AND) + @GetMapping("/advance/multiRoleAnd") + public R multiRoleAnd() { + log.info("【场景5】多角色AND模式(admin+operator)校验通过"); + return R.ok("同时拥有admin和operator角色,校验通过"); + } + + /** + * 场景6:多角色OR模式(拥有任一角色即可) + */ + @SaCheckRole(value = {"admin", "test"}, mode = SaMode.OR) + @GetMapping("/advance/multiRoleOr") + public R multiRoleOr() { + log.info("【场景6】多角色OR模式(admin|test)校验通过"); + return R.ok("拥有admin或test角色,校验通过"); + } + + /** + * 场景7:多权限AND模式(必须同时拥有所有权限) + */ + @SaCheckPermission(value = {"system:user:edit", "system:log:view"}, mode = SaMode.AND) + @GetMapping("/advance/multiPermAnd") + public R multiPermAnd() { + log.info("【场景7】多权限AND模式(system:user:edit+system:log:view)校验通过"); + return R.ok("同时拥有system:user:edit和system:log:view权限,校验通过"); + } + + /** + * 场景8:多权限OR模式(拥有任一权限即可) + */ + @SaCheckPermission(value = {"system:user:add", "system:user:delete"}, mode = SaMode.OR) + @GetMapping("/advance/multiPermOr") + public R multiPermOr() { + log.info("【场景8】多权限OR模式(system:user:add|system:user:delete)校验通过"); + return R.ok("拥有system:user:add或system:user:delete权限,校验通过"); + } + + // ====================== 高级场景:通配符/混合组合 ====================== + + /** + * 场景9:权限通配符匹配(前缀匹配) + * 拥有system:user:* 即可匹配所有用户模块权限 + */ + @SaCheckPermission("system:user:*") + @GetMapping("/advanced/permWildcardPrefix") + public R permWildcardPrefix() { + log.info("【场景9】权限通配符(system:user:*)校验通过"); + return R.ok("拥有system:user:*前缀权限,校验通过"); + } + + /** + * 场景10:角色通配符匹配(前缀匹配) + * 拥有admin_* 即可匹配所有admin开头的角色 + */ + @SaCheckRole("admin_*") + @GetMapping("/advanced/roleWildcardPrefix") + public R roleWildcardPrefix() { + log.info("【场景10】角色通配符(admin_*)校验通过"); + return R.ok("拥有admin_*前缀角色,校验通过"); + } + + /** + * 场景11:权限+角色混合AND模式(所有条件必须满足) + * 需同时满足:拥有admin角色 + 拥有system:user:all权限 + */ + @SaCheckRole("admin") + @SaCheckPermission("system:user:all") + @GetMapping("/advanced/mixRolePermAnd") + public R mixRolePermAnd() { + log.info("【场景11】角色+权限混合AND(admin+system:user:all)校验通过"); + return R.ok("拥有admin角色且拥有system:user:all权限,校验通过"); + } + + /** + * 场景12:权限+角色混合OR模式(任一条件满足即可) + * 满足任一:拥有super_admin角色 | 拥有system:manage权限 + */ + @SaCheckRole(value = {"super_admin"}, mode = SaMode.OR) + @SaCheckPermission(value = {"system:manage"}, mode = SaMode.OR) + @GetMapping("/advanced/mixRolePermOr") + public R mixRolePermOr() { + log.info("【场景12】角色+权限混合OR(super_admin|system:manage)校验通过"); + return R.ok("拥有super_admin角色或system:manage权限,校验通过"); + } + + /** + * 场景13:orRole参数(权限校验失败时,兜底角色校验) + * 核心逻辑:无system:user:export权限时,检查是否有admin/operator角色 + */ + @SaCheckPermission(value = "system:user:export", orRole = {"admin", "operator"}) + @GetMapping("/advanced/permWithOrRole") + public R permWithOrRole() { + log.info("【场景13】权限+orRole兜底校验通过"); + return R.ok("拥有system:user:export权限,或拥有admin/operator角色,校验通过"); + } + + // ====================== 特殊场景:临时权限/注解覆盖 ====================== + + /** + * 场景14:SaIgnore局部覆盖(方法注解覆盖类注解,若有) + * 假设类上有@SaCheckLogin,方法上@SaIgnore会覆盖 + */ + @SaIgnore + @GetMapping("/special/ignoreOverride") + public R ignoreOverride() { + log.info("【场景14】SaIgnore覆盖类级别权限注解"); + return R.ok("方法级SaIgnore覆盖类级别权限校验"); + } + + /** + * 场景15:临时权限校验(SaCheckPermission逻辑:临时权限>永久权限) + * 注:临时权限需通过SaToken API手动设置,如 SaHolder.getStpLogic().setTempPermission("system:temp:test") + */ + @SaCheckPermission("system:temp:test") + @GetMapping("/special/tempPermission") + public R tempPermission() { + log.info("【场景15】临时权限(system:temp:test)校验通过"); + return R.ok("临时权限校验通过(需先通过API设置临时权限)"); + } + + /** + * 场景16:登录类型指定(多端登录场景,如PC/APP/小程序) + * 注:需配合SaToken多账号体系配置 + */ + @SaCheckLogin(type = "PC") // 仅校验PC端的登录态 + @GetMapping("/special/loginTypeSpecify") + public R loginTypeSpecify() { + log.info("【场景16】指定登录类型(PC)校验通过"); + return R.ok("仅PC端登录态校验通过"); + } +} diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java index 022aaf2a1..3a58caf9f 100644 --- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java @@ -35,8 +35,8 @@ public class ExportDemoVo implements Serializable { /** * 用户昵称 */ - @ExcelProperty(value = "用户名", index = 0) - @NotEmpty(message = "用户名不能为空", groups = AddGroup.class) + @ExcelProperty(value = "用户昵称", index = 0) + @NotEmpty(message = "用户昵称不能为空", groups = AddGroup.class) private String nickName; /** diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java index 3e7bdf7bb..455f8d9e6 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/dromara/generator/service/GenTableServiceImpl.java @@ -523,6 +523,9 @@ public class GenTableServiceImpl implements IGenTableService { * @param table 业务表信息 */ public void setPkColumn(GenTable table) { + if (CollUtil.isEmpty(table.getColumns())) { + throw new ServiceException("表【" + table.getTableName() + "】字段为空,请检查表结构"); + } for (GenTableColumn column : table.getColumns()) { if (column.isPk()) { table.setPkColumn(column); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysDictDataController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysDictDataController.java index 90ddd0108..337f8da9f 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysDictDataController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysDictDataController.java @@ -83,7 +83,7 @@ public class SysDictDataController extends BaseController { } /** - * 新增字典类型 + * 新增字典数据 */ @SaCheckPermission("system:dict:add") @Log(title = "字典数据", businessType = BusinessType.INSERT) @@ -98,7 +98,7 @@ public class SysDictDataController extends BaseController { } /** - * 修改保存字典类型 + * 修改保存字典数据 */ @SaCheckPermission("system:dict:edit") @Log(title = "字典数据", businessType = BusinessType.UPDATE) @@ -113,12 +113,12 @@ public class SysDictDataController extends BaseController { } /** - * 删除字典类型 + * 删除字典数据 * * @param dictCodes 字典code串 */ @SaCheckPermission("system:dict:remove") - @Log(title = "字典类型", businessType = BusinessType.DELETE) + @Log(title = "字典数据", businessType = BusinessType.DELETE) @DeleteMapping("/{dictCodes}") public R remove(@PathVariable Long[] dictCodes) { dictDataService.deleteDictDataByIds(Arrays.asList(dictCodes)); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUserOnline.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUserOnline.java index ba30eb609..f3b5f87a3 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUserOnline.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysUserOnline.java @@ -21,7 +21,7 @@ public class SysUserOnline { private String deptName; /** - * 用户名称 + * 用户账号 */ private String userName; diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java index ee140bd72..deef33ac2 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserExportVo.java @@ -32,13 +32,13 @@ public class SysUserExportVo implements Serializable { /** * 用户账号 */ - @ExcelProperty(value = "登录名称") + @ExcelProperty(value = "用户账号") private String userName; /** * 用户昵称 */ - @ExcelProperty(value = "用户名称") + @ExcelProperty(value = "用户昵称") private String nickName; /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java index 100f5b7e4..507f3444a 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysUserImportVo.java @@ -38,13 +38,13 @@ public class SysUserImportVo implements Serializable { /** * 用户账号 */ - @ExcelProperty(value = "登录名称") + @ExcelProperty(value = "用户账号") private String userName; /** * 用户昵称 */ - @ExcelProperty(value = "用户名称") + @ExcelProperty(value = "用户昵称") private String nickName; /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java index ed6463619..dce1ed3e0 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java @@ -101,7 +101,7 @@ public interface ISysUserService { String selectUserPostGroup(Long userId); /** - * 校验用户名称是否唯一 + * 校验用户账号是否唯一 * * @param user 用户信息 * @return 结果 diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java index 6d4d9fea9..a7bc06949 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java @@ -229,6 +229,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService @Override public String getDictLabel(String dictType, String dictValue, String separator) { List datas = SpringUtils.getAopProxy(this).selectDictDataByType(dictType); + if (CollUtil.isEmpty(datas)) { + return StringUtils.EMPTY; + } Map map = StreamUtils.toMap(datas, SysDictDataVo::getDictValue, SysDictDataVo::getDictLabel); if (StringUtils.containsAny(dictValue, separator)) { return Arrays.stream(dictValue.split(separator)) @@ -250,6 +253,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService @Override public String getDictValue(String dictType, String dictLabel, String separator) { List datas = SpringUtils.getAopProxy(this).selectDictDataByType(dictType); + if (CollUtil.isEmpty(datas)) { + return StringUtils.EMPTY; + } Map map = StreamUtils.toMap(datas, SysDictDataVo::getDictLabel, SysDictDataVo::getDictValue); if (StringUtils.containsAny(dictLabel, separator)) { return Arrays.stream(dictLabel.split(separator)) @@ -269,6 +275,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService @Override public Map getAllDictByDictType(String dictType) { List list = SpringUtils.getAopProxy(this).selectDictDataByType(dictType); + if (CollUtil.isEmpty(list)) { + return new HashMap<>(); + } // 保证顺序 LinkedHashMap map = new LinkedHashMap<>(); for (SysDictDataVo vo : list) { @@ -286,6 +295,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService @Override public DictTypeDTO getDictType(String dictType) { SysDictTypeVo vo = SpringUtils.getAopProxy(this).selectDictTypeByType(dictType); + if (ObjectUtil.isNull(vo)) { + return null; + } return BeanUtil.toBean(vo, DictTypeDTO.class); } @@ -298,6 +310,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService @Override public List getDictData(String dictType) { List list = SpringUtils.getAopProxy(this).selectDictDataByType(dictType); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } return BeanUtil.copyToList(list, DictDataDTO.class); } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java index c44824e30..746905fc7 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysMenuServiceImpl.java @@ -120,7 +120,7 @@ public class SysMenuServiceImpl implements ISysMenuService { /** * 根据用户ID查询菜单 * - * @param userId 用户名称 + * @param userId 用户ID * @return 菜单列表 */ @Override diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java index 466d41d39..62b3df9f7 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java @@ -232,7 +232,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService { } /** - * 校验用户名称是否唯一 + * 校验用户账号是否唯一 * * @param user 用户信息 * @return 结果 @@ -496,6 +496,11 @@ public class SysUserServiceImpl implements ISysUserService, UserService { roleList.remove(SystemConstants.SUPER_ADMIN_ID); } + // 移除超管角色后若无剩余角色,说明仅选了超管角色且不允许分配,显式报错 + if (roleList.isEmpty()) { + throw new ServiceException("不允许为普通用户分配超级管理员角色,请至少选择一个其他角色"); + } + // 校验是否有权限访问这些角色(含数据权限控制) if (roleMapper.selectRoleCount(roleList) != roleList.size()) { throw new ServiceException("没有权限访问角色的数据"); @@ -593,10 +598,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService { } /** - * 通过用户ID查询用户账户 + * 通过用户ID查询用户昵称 * * @param userId 用户ID - * @return 用户账户 + * @return 用户昵称 */ @Override @Cacheable(cacheNames = CacheNames.SYS_NICKNAME, key = "#userId") @@ -607,10 +612,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService { } /** - * 通过用户ID查询用户账户 + * 通过用户ID查询用户昵称 * * @param userIds 用户ID 多个用逗号隔开 - * @return 用户账户 + * @return 用户昵称 */ @Override public String selectNicknameByIds(String userIds) { @@ -750,13 +755,13 @@ public class SysUserServiceImpl implements ISysUserService, UserService { } /** - * 根据用户 ID 列表查询用户名称映射关系 + * 根据用户 ID 列表查询用户昵称映射关系 * * @param userIds 用户 ID 列表 - * @return Map,其中 key 为用户 ID,value 为对应的用户名称 + * @return Map,其中 key 为用户 ID,value 为对应的用户昵称 */ @Override - public Map selectUserNamesByIds(List userIds) { + public Map selectUserNicksByIds(List userIds) { if (CollUtil.isEmpty(userIds)) { return Collections.emptyMap(); } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java index 88372f0b7..203f3f667 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java @@ -23,26 +23,6 @@ public interface FlowConstant { */ String INITIATOR_DEPT_ID = "initiatorDeptId"; - /** - * 委托 - */ - String DELEGATE_TASK = "delegateTask"; - - /** - * 转办 - */ - String TRANSFER_TASK = "transferTask"; - - /** - * 加签 - */ - String ADD_SIGNATURE = "addSignature"; - - /** - * 减签 - */ - String REDUCTION_SIGNATURE = "reductionSignature"; - /** * 流程分类Id转名称 */ diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskOperationEnum.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskOperationEnum.java new file mode 100644 index 000000000..8c050a28f --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskOperationEnum.java @@ -0,0 +1,53 @@ +package org.dromara.workflow.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 任务操作类型枚举 + * + * @author may + */ +@Getter +@AllArgsConstructor +public enum TaskOperationEnum { + + /** + * 委派 + */ + DELEGATE_TASK("delegateTask", "委派"), + + /** + * 转办 + */ + TRANSFER_TASK("transferTask", "转办"), + + /** + * 加签 + */ + ADD_SIGNATURE("addSignature", "加签"), + + /** + * 减签 + */ + REDUCTION_SIGNATURE("reductionSignature", "减签"); + + private final String code; + private final String desc; + + private static final Map CODE_MAP = Arrays.stream(values()) + .collect(Collectors.toConcurrentMap(TaskOperationEnum::getCode, Function.identity())); + + /** + * 根据 code 获取枚举 + */ + public static TaskOperationEnum getByCode(String code) { + return CODE_MAP.get(code); + } + +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/BackProcessBo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/BackProcessBo.java index a67a1f78e..a9674b0b3 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/BackProcessBo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/BackProcessBo.java @@ -61,7 +61,8 @@ public class BackProcessBo implements Serializable { public Map getVariables() { if (variables == null) { - return new HashMap<>(16); + variables = new HashMap<>(16); + return variables; } variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); return variables; diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowCopyBo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowCopyBo.java index a45e52109..894174929 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowCopyBo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowCopyBo.java @@ -23,8 +23,8 @@ public class FlowCopyBo implements Serializable { private Long userId; /** - * 用户名称 + * 用户昵称 */ - private String userName; + private String nickName; } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowNextNodeBo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowNextNodeBo.java index 12f0653ef..de75ba621 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowNextNodeBo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowNextNodeBo.java @@ -30,7 +30,8 @@ public class FlowNextNodeBo implements Serializable { public Map getVariables() { if (variables == null) { - return new HashMap<>(16); + variables = new HashMap<>(16); + return variables; } variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); return variables; diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java index b31f4fa14..98f33a007 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java @@ -53,7 +53,8 @@ public class StartProcessBo implements Serializable { public Map getVariables() { if (variables == null) { - return new HashMap<>(16); + variables = new HashMap<>(16); + return variables; } variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); return variables; diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskOperationBo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskOperationBo.java index 4348e310c..0846e73b4 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskOperationBo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskOperationBo.java @@ -40,6 +40,11 @@ public class TaskOperationBo implements Serializable { @NotNull(message = "任务id不能为空") private Long taskId; + /** + * 消息类型 + */ + private List messageType; + /** * 意见或备注信息(可选) */ diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowCopyVo.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowCopyVo.java index 67ef9e2c3..c58071570 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowCopyVo.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/FlowCopyVo.java @@ -24,10 +24,10 @@ public class FlowCopyVo implements Serializable { private Long userId; /** - * 用户名称 + * 用户昵称 */ @Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "userId") - private String userName; + private String nickName; public FlowCopyVo(Long userId) { this.userId = userId; diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java index efd24ffd6..d8ba911f2 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/WorkflowGlobalListener.java @@ -80,11 +80,13 @@ public class WorkflowGlobalListener implements GlobalListener { NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable); Set copyList = nodeExt.getCopySettings(); if (CollUtil.isNotEmpty(copyList)) { + List userIds = StreamUtils.toList(copyList, Convert::toLong); + Map nickNameMap = userService.selectUserNicksByIds(userIds); List list = StreamUtils.toList(copyList, x -> { FlowCopyBo bo = new FlowCopyBo(); Long id = Convert.toLong(x); bo.setUserId(id); - bo.setUserName(userService.selectUserNameById(id)); + bo.setNickName(nickNameMap.getOrDefault(id, StringUtils.EMPTY)); return bo; }); variable.put(FlowConstant.FLOW_COPY_LIST, list); @@ -159,7 +161,7 @@ public class WorkflowGlobalListener implements GlobalListener { flowTask.setPermissionList(List.of(userIdArray)); // 移除已处理的状态变量 variable.remove(nodeKey); - FlowEngine.insService().removeVariables(flowTask.getInstanceId(),nodeKey); + FlowEngine.insService().removeVariables(flowTask.getInstanceId(), nodeKey); } } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwCommonServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwCommonServiceImpl.java index 6f9ea3028..77389bcaa 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwCommonServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwCommonServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjectUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.domain.dto.UserDTO; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StringUtils; @@ -126,6 +127,9 @@ public class FlwCommonServiceImpl implements IFlwCommonService { @Override public String applyNodeCode(Long definitionId) { List firstBetweenNode = FlowEngine.nodeService().getFirstBetweenNode(definitionId, new HashMap<>()); + if (CollUtil.isEmpty(firstBetweenNode)) { + throw new ServiceException("流程定义缺少申请人节点,请检查流程定义配置"); + } return firstBetweenNode.get(0).getNodeCode(); } } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java index 4de669da7..897d8a6e2 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwInstanceServiceImpl.java @@ -111,8 +111,14 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { @Override public FlowInstanceVo queryByBusinessId(Long businessId) { FlowInstance instance = this.selectInstByBusinessId(Convert.toStr(businessId)); + if (ObjectUtil.isNull(instance)) { + throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE); + } FlowInstanceVo instanceVo = BeanUtil.toBean(instance, FlowInstanceVo.class); Definition definition = defService.getById(instanceVo.getDefinitionId()); + if (ObjectUtil.isNull(definition)) { + throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF); + } instanceVo.setFlowName(definition.getFlowName()); instanceVo.setFlowCode(definition.getFlowCode()); instanceVo.setVersion(definition.getVersion()); @@ -383,6 +389,9 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService { @Override public Map instanceVariable(Long instanceId) { FlowInstance flowInstance = flowInstanceMapper.selectById(instanceId); + if (ObjectUtil.isNull(flowInstance)) { + throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE); + } Map variableMap = Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap()); List> variableList = variableMap.entrySet().stream() .map(entry -> Map.of("key", entry.getKey(), "value", entry.getValue())) diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwSpelServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwSpelServiceImpl.java index 3790cdfd4..090ac81db 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwSpelServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwSpelServiceImpl.java @@ -1,6 +1,7 @@ package org.dromara.workflow.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; @@ -9,6 +10,7 @@ import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.domain.dto.TaskAssigneeDTO; import org.dromara.common.core.domain.model.TaskAssigneeBody; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StringUtils; @@ -125,7 +127,14 @@ public class FlwSpelServiceImpl implements IFlwSpelService { * 保存前的数据校验 */ private void validEntityBeforeSave(FlowSpel entity){ - //TODO 做一些数据校验,如唯一约束 + if (StringUtils.isNotBlank(entity.getViewSpel())) { + boolean exists = baseMapper.exists(new LambdaQueryWrapper() + .eq(FlowSpel::getViewSpel, entity.getViewSpel()) + .ne(ObjectUtil.isNotNull(entity.getId()), FlowSpel::getId, entity.getId())); + if (exists) { + throw new ServiceException("SpEL表达式已存在,请勿重复添加"); + } + } } /** @@ -137,7 +146,7 @@ public class FlwSpelServiceImpl implements IFlwSpelService { */ @Override public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if(isValid){ + if (isValid){ //TODO 做一些业务上的校验,判断是否需要校验 } return baseMapper.deleteByIds(ids) > 0; diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java index 9e9c091ad..8edccd03e 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskAssigneeServiceImpl.java @@ -244,7 +244,7 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand List longIds = StreamUtils.toList(ids, Convert::toLong); Map rawMap = switch (type) { - case USER -> userService.selectUserNamesByIds(longIds); + case USER -> userService.selectUserNicksByIds(longIds); case ROLE -> roleService.selectRoleNamesByIds(longIds); case DEPT -> deptService.selectDeptNamesByIds(longIds); case POST -> postService.selectPostNamesByIds(longIds); diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java index 5fd3838ac..e2eedf684 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwTaskServiceImpl.java @@ -44,8 +44,8 @@ import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper; import org.dromara.warm.flow.orm.mapper.FlowNodeMapper; import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; import org.dromara.workflow.common.ConditionalOnEnable; -import org.dromara.workflow.common.constant.FlowConstant; import org.dromara.workflow.common.enums.TaskAssigneeType; +import org.dromara.workflow.common.enums.TaskOperationEnum; import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.domain.FlowInstanceBizExt; import org.dromara.workflow.domain.bo.*; @@ -127,6 +127,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService { // 已存在流程 BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus()); List taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId())); + if (CollUtil.isEmpty(taskList)) { + throw new ServiceException("流程实例缺少任务,请检查流程定义配置"); + } taskService.mergeVariable(flowInstance, variables); insService.updateById(flowInstance); StartProcessReturnDTO dto = new StartProcessReturnDTO(); @@ -143,9 +146,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService { throw new ServiceException("流程【" + startProcessBo.getFlowCode() + "】未发布,请先在流程设计器中发布流程定义"); } Dict dict = JsonUtils.parseMap(definition.getExt()); - boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(FlowConstant.AUTO_PASS); - variables.put(FlowConstant.AUTO_PASS, autoPass); - variables.put(FlowConstant.BUSINESS_CODE, this.generateBusinessCode(bizExt)); + boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(AUTO_PASS); + variables.put(AUTO_PASS, autoPass); + variables.put(BUSINESS_CODE, this.generateBusinessCode(bizExt)); FlowParams flowParams = FlowParams.build() .handler(startProcessBo.getHandler()) .flowCode(startProcessBo.getFlowCode()) @@ -156,6 +159,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService { this.buildFlowInstanceBizExt(instance, bizExt); // 申请人执行流程 List taskList = taskService.list(new FlowTask().setInstanceId(instance.getId())); + if (CollUtil.isEmpty(taskList)) { + throw new ServiceException("流程启动失败,未生成任务"); + } if (taskList.size() > 1) { throw new ServiceException("请检查流程第一个环节是否为申请人!"); } @@ -207,11 +213,11 @@ public class FlwTaskServiceImpl implements IFlwTaskService { List flowCopyList = completeTaskBo.getFlowCopyList(); // 设置抄送人 Map variables = completeTaskBo.getVariables(); - variables.put(FlowConstant.FLOW_COPY_LIST, flowCopyList); + variables.put(FLOW_COPY_LIST, flowCopyList); // 消息类型 - variables.put(FlowConstant.MESSAGE_TYPE, messageType); + variables.put(MESSAGE_TYPE, messageType); // 消息通知 - variables.put(FlowConstant.MESSAGE_NOTICE, notice); + variables.put(MESSAGE_NOTICE, notice); FlowTask flowTask = flowTaskMapper.selectById(taskId); @@ -219,9 +225,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService { throw new ServiceException("流程任务不存在或任务已审批!"); } Instance ins = insService.getById(flowTask.getInstanceId()); + if (ObjectUtil.isNull(ins)) { + throw new ServiceException("流程实例不存在"); + } // 检查流程状态是否为草稿、已撤销或已退回状态,若是则执行流程提交监听 if (BusinessStatusEnum.isDraftOrCancelOrBack(ins.getFlowStatus())) { - variables.put(FlowConstant.SUBMIT, true); + variables.put(SUBMIT, true); } // 设置弹窗处理人 Map assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), ins.getVariableMap()); @@ -274,9 +283,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService { flowParams. message("流程引擎自动审批!"). variable(Map.of( - FlowConstant.SUBMIT, false, - FlowConstant.FLOW_COPY_LIST, Collections.emptyList(), - FlowConstant.MESSAGE_NOTICE, StringUtils.EMPTY)); + SUBMIT, false, + FLOW_COPY_LIST, Collections.emptyList(), + MESSAGE_NOTICE, StringUtils.EMPTY)); skipTask(task.getId(), flowParams, instanceId, true); } } @@ -341,7 +350,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService { FlowParams flowParams = FlowParams.build() .skipType(SkipType.NONE.getKey()) .hisStatus(TaskStatusEnum.COPY.getStatus()) - .message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getUserName)); + .message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getNickName)); HisTask hisTask = hisTaskService.setSkipHisTask(task, flowNode, flowParams); hisTask.setCreateTime(updateTime); hisTask.setUpdateTime(updateTime); @@ -482,15 +491,18 @@ public class FlwTaskServiceImpl implements IFlwTaskService { throw new ServiceException("任务不存在!"); } Instance inst = insService.getById(task.getInstanceId()); + if (ObjectUtil.isNull(inst)) { + throw new ServiceException("流程实例不存在"); + } BusinessStatusEnum.checkBackStatus(inst.getFlowStatus()); Long definitionId = task.getDefinitionId(); String applyNodeCode = flwCommonService.applyNodeCode(definitionId); Map variable = new HashMap<>(); // 消息类型 - variable.put(FlowConstant.MESSAGE_TYPE, messageType); + variable.put(MESSAGE_TYPE, messageType); // 消息通知 - variable.put(FlowConstant.MESSAGE_NOTICE, notice); + variable.put(MESSAGE_NOTICE, notice); FlowParams flowParams = FlowParams.build() .nodeCode(bo.getNodeCode()) @@ -513,6 +525,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService { @Override public List getBackTaskNode(Long taskId, String nowNodeCode) { FlowTask task = flowTaskMapper.selectById(taskId); + if (ObjectUtil.isNull(task)) { + throw new ServiceException("任务不存在!"); + } List nodeCodes = nodeService.getByNodeCodes(Collections.singletonList(nowNodeCode), task.getDefinitionId()); if (!CollUtil.isNotEmpty(nodeCodes)) { return nodeCodes; @@ -597,7 +612,13 @@ public class FlwTaskServiceImpl implements IFlwTaskService { } FlowTaskVo flowTaskVo = BeanUtil.toBean(task, FlowTaskVo.class); Instance instance = insService.getById(task.getInstanceId()); + if (ObjectUtil.isNull(instance)) { + throw new ServiceException("流程实例不存在"); + } Definition definition = defService.getById(task.getDefinitionId()); + if (ObjectUtil.isNull(definition)) { + throw new ServiceException("流程定义不存在"); + } flowTaskVo.setFlowStatus(instance.getFlowStatus()); flowTaskVo.setVersion(definition.getVersion()); flowTaskVo.setFlowCode(definition.getFlowCode()); @@ -640,11 +661,23 @@ public class FlwTaskServiceImpl implements IFlwTaskService { Long taskId = bo.getTaskId(); Map variables = bo.getVariables(); Task task = taskService.getById(taskId); + if (ObjectUtil.isNull(task)) { + throw new ServiceException("任务不存在!"); + } Instance instance = insService.getById(task.getInstanceId()); + if (ObjectUtil.isNull(instance)) { + throw new ServiceException("流程实例不存在"); + } Definition definition = defService.getById(task.getDefinitionId()); + if (ObjectUtil.isNull(definition)) { + throw new ServiceException("流程定义不存在"); + } Map mergeVariable = MapUtil.mergeAll(instance.getVariableMap(), variables); // 获取下一节点列表 List nextNodeList = nodeService.getNextNodeList(task.getDefinitionId(), task.getNodeCode(), null, SkipType.PASS.getKey(), mergeVariable); + if (CollUtil.isEmpty(nextNodeList)) { + return new ArrayList<>(); + } List nextFlowNodes = BeanUtil.copyToList(nextNodeList, FlowNode.class); // 只获取中间节点 nextFlowNodes = StreamUtils.filter(nextFlowNodes, node -> NodeType.BETWEEN.getKey().equals(node.getNodeType())); @@ -719,13 +752,19 @@ public class FlwTaskServiceImpl implements IFlwTaskService { @Override @Transactional(rollbackFor = Exception.class) public boolean taskOperation(TaskOperationBo bo, String taskOperation) { + TaskOperationEnum op = TaskOperationEnum.getByCode(taskOperation); + if (op == null) { + log.error("Invalid operation type:{} ", taskOperation); + throw new ServiceException("Invalid operation type " + taskOperation); + } + FlowParams flowParams = FlowParams.build().message(bo.getMessage()); if (LoginHelper.isSuperAdmin()) { flowParams.ignore(true); } // 根据操作类型构建 FlowParams - switch (taskOperation) { + switch (op) { case DELEGATE_TASK, TRANSFER_TASK -> { ValidatorUtils.validate(bo, AddGroup.class); flowParams.addHandlers(Collections.singletonList(bo.getUserId())); @@ -738,47 +777,63 @@ public class FlwTaskServiceImpl implements IFlwTaskService { ValidatorUtils.validate(bo, EditGroup.class); flowParams.reductionHandlers(bo.getUserIds()); } - default -> { - log.error("Invalid operation type:{} ", taskOperation); - throw new ServiceException("Invalid operation type " + taskOperation); - } } Long taskId = bo.getTaskId(); Task task = taskService.getById(taskId); + if (ObjectUtil.isNull(task)) { + throw new ServiceException("任务不存在!"); + } FlowNode flowNode = getByNodeCode(task.getNodeCode(), task.getDefinitionId()); - if (ADD_SIGNATURE.equals(taskOperation) || REDUCTION_SIGNATURE.equals(taskOperation)) { + if (ObjectUtil.isNull(flowNode)) { + throw new ServiceException("流程节点不存在"); + } + if (op == TaskOperationEnum.ADD_SIGNATURE || op == TaskOperationEnum.REDUCTION_SIGNATURE) { if (CooperateType.isOrSign(flowNode.getNodeRatio())) { throw new ServiceException(task.getNodeName() + "不是会签或票签节点!"); } } + // 设置任务状态并执行对应的任务操作 - switch (taskOperation) { - //委派任务 + boolean result = false; + switch (op) { case DELEGATE_TASK -> { flowParams.hisStatus(TaskStatusEnum.DEPUTE.getStatus()); - return taskService.depute(taskId, flowParams); + result = taskService.depute(taskId, flowParams); } - //转办任务 case TRANSFER_TASK -> { flowParams.hisStatus(TaskStatusEnum.TRANSFER.getStatus()); - return taskService.transfer(taskId, flowParams); + result = taskService.transfer(taskId, flowParams); } - //加签,增加办理人 case ADD_SIGNATURE -> { flowParams.hisStatus(TaskStatusEnum.SIGN.getStatus()); - return taskService.addSignature(taskId, flowParams); + result = taskService.addSignature(taskId, flowParams); } - //减签,减少办理人 case REDUCTION_SIGNATURE -> { flowParams.hisStatus(TaskStatusEnum.SIGN_OFF.getStatus()); - return taskService.reductionSignature(taskId, flowParams); - } - default -> { - log.error("Invalid operation type:{} ", taskOperation); - throw new ServiceException("Invalid operation type " + taskOperation); + result = taskService.reductionSignature(taskId, flowParams); } } + + // 操作执行成功后再发送消息 + if (result && CollUtil.isNotEmpty(bo.getMessageType())) { + List userIdList = new ArrayList<>(); + if (StrUtil.isNotBlank(bo.getUserId())) { + userIdList.add(Convert.toLong(bo.getUserId())); + } + if (CollUtil.isNotEmpty(bo.getUserIds())) { + userIdList.addAll(StreamUtils.toList(bo.getUserIds(), Convert::toLong)); + } + if (CollUtil.isNotEmpty(userIdList)) { + flwCommonService.sendMessage( + bo.getMessageType(), + StringUtils.isNotBlank(bo.getMessage()) ? bo.getMessage() : "单据「" + op.getDesc() + "」通知", + "单据「" + op.getDesc() + "」提醒", + userService.selectListByIds(userIdList) + ); + } + } + return result; } /**