Commit e2edd7a49f3c9bb864ebd61c1656944bf541d168
Committed by
Gitee
!1404 feat:【framework 框架】增加 api 加解密能力
Merge pull request !1404 from 芋道源码/develop
Showing
49 changed files
with
1110 additions
and
199 deletions
sql/mysql/ruoyi-vue-pro.sql
| @@ -1259,14 +1259,16 @@ CREATE TABLE `system_mail_log` ( | @@ -1259,14 +1259,16 @@ CREATE TABLE `system_mail_log` ( | ||
| 1259 | `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', | 1259 | `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', |
| 1260 | `user_id` bigint NULL DEFAULT NULL COMMENT '用户编号', | 1260 | `user_id` bigint NULL DEFAULT NULL COMMENT '用户编号', |
| 1261 | `user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型', | 1261 | `user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型', |
| 1262 | - `to_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址', | 1262 | + `to_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址', |
| 1263 | + `cc_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '抄送邮箱地址', | ||
| 1264 | + `bcc_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密送邮箱地址', | ||
| 1263 | `account_id` bigint NOT NULL COMMENT '邮箱账号编号', | 1265 | `account_id` bigint NOT NULL COMMENT '邮箱账号编号', |
| 1264 | `from_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送邮箱地址', | 1266 | `from_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送邮箱地址', |
| 1265 | `template_id` bigint NOT NULL COMMENT '模板编号', | 1267 | `template_id` bigint NOT NULL COMMENT '模板编号', |
| 1266 | `template_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', | 1268 | `template_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', |
| 1267 | `template_nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模版发送人名称', | 1269 | `template_nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模版发送人名称', |
| 1268 | `template_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件标题', | 1270 | `template_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件标题', |
| 1269 | - `template_content` varchar(10240) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件内容', | 1271 | + `template_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件内容', |
| 1270 | `template_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件参数', | 1272 | `template_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件参数', |
| 1271 | `send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态', | 1273 | `send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态', |
| 1272 | `send_time` datetime NULL DEFAULT NULL COMMENT '发送时间', | 1274 | `send_time` datetime NULL DEFAULT NULL COMMENT '发送时间', |
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
| @@ -15,6 +15,8 @@ public interface WebFilterOrderEnum { | @@ -15,6 +15,8 @@ public interface WebFilterOrderEnum { | ||
| 15 | 15 | ||
| 16 | int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; | 16 | int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; |
| 17 | 17 | ||
| 18 | + int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1; | ||
| 19 | + | ||
| 18 | // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 | 20 | // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 |
| 19 | 21 | ||
| 20 | int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 | 22 | int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.config; | ||
| 2 | + | ||
| 3 | +import jakarta.validation.constraints.NotEmpty; | ||
| 4 | +import jakarta.validation.constraints.NotNull; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| 7 | +import org.springframework.validation.annotation.Validated; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * HTTP API 加解密配置 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@ConfigurationProperties(prefix = "yudao.api-encrypt") | ||
| 15 | +@Validated | ||
| 16 | +@Data | ||
| 17 | +public class ApiEncryptProperties { | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 是否开启 | ||
| 21 | + */ | ||
| 22 | + @NotNull(message = "是否开启不能为空") | ||
| 23 | + private Boolean enable; | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 请求头(响应头)名称 | ||
| 27 | + * | ||
| 28 | + * 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密 | ||
| 29 | + * 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密 | ||
| 30 | + */ | ||
| 31 | + @NotEmpty(message = "请求头(响应头)名称不能为空") | ||
| 32 | + private String header = "X-Api-Encrypt"; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 对称加密算法,用于请求/响应的加解密 | ||
| 36 | + * | ||
| 37 | + * 目前支持 | ||
| 38 | + * 【对称加密】: | ||
| 39 | + * 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES} | ||
| 40 | + * 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低) | ||
| 41 | + * 【非对称加密】 | ||
| 42 | + * 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA} | ||
| 43 | + * 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低) | ||
| 44 | + * | ||
| 45 | + * @see <a href="https://help.aliyun.com/zh/ssl-certificate/what-are-a-public-key-and-a-private-key">什么是公钥和私钥?</a> | ||
| 46 | + */ | ||
| 47 | + @NotEmpty(message = "对称加密算法不能为空") | ||
| 48 | + private String algorithm; | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 请求的解密密钥 | ||
| 52 | + * | ||
| 53 | + * 注意: | ||
| 54 | + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 | ||
| 55 | + * 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!) | ||
| 56 | + */ | ||
| 57 | + @NotEmpty(message = "请求的解密密钥不能为空") | ||
| 58 | + private String requestKey; | ||
| 59 | + | ||
| 60 | + /** | ||
| 61 | + * 响应的加密密钥 | ||
| 62 | + * | ||
| 63 | + * 注意: | ||
| 64 | + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 | ||
| 65 | + * 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!) | ||
| 66 | + */ | ||
| 67 | + @NotEmpty(message = "响应的加密密钥不能为空") | ||
| 68 | + private String responseKey; | ||
| 69 | + | ||
| 70 | +} |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.config; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.encrypt.core.filter.ApiEncryptFilter; | ||
| 5 | +import cn.iocoder.yudao.framework.web.config.WebProperties; | ||
| 6 | +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.springframework.boot.autoconfigure.AutoConfiguration; | ||
| 9 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| 10 | +import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| 11 | +import org.springframework.boot.web.servlet.FilterRegistrationBean; | ||
| 12 | +import org.springframework.context.annotation.Bean; | ||
| 13 | +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; | ||
| 14 | + | ||
| 15 | +import static cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration.createFilterBean; | ||
| 16 | + | ||
| 17 | +@AutoConfiguration | ||
| 18 | +@Slf4j | ||
| 19 | +@EnableConfigurationProperties(ApiEncryptProperties.class) | ||
| 20 | +@ConditionalOnProperty(prefix = "yudao.api-encrypt", name = "enable", havingValue = "true") | ||
| 21 | +public class YudaoApiEncryptAutoConfiguration { | ||
| 22 | + | ||
| 23 | + @Bean | ||
| 24 | + public FilterRegistrationBean<ApiEncryptFilter> apiEncryptFilter(WebProperties webProperties, | ||
| 25 | + ApiEncryptProperties apiEncryptProperties, | ||
| 26 | + RequestMappingHandlerMapping requestMappingHandlerMapping, | ||
| 27 | + GlobalExceptionHandler globalExceptionHandler) { | ||
| 28 | + ApiEncryptFilter filter = new ApiEncryptFilter(webProperties, apiEncryptProperties, | ||
| 29 | + requestMappingHandlerMapping, globalExceptionHandler); | ||
| 30 | + return createFilterBean(filter, WebFilterOrderEnum.API_ENCRYPT_FILTER); | ||
| 31 | + | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | +} |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.annotation; | ||
| 2 | + | ||
| 3 | +import java.lang.annotation.*; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * HTTP API 加解密注解 | ||
| 7 | + */ | ||
| 8 | +@Documented | ||
| 9 | +@Target({ElementType.TYPE, ElementType.METHOD}) | ||
| 10 | +@Retention(RetentionPolicy.RUNTIME) | ||
| 11 | +public @interface ApiEncrypt { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 是否对请求参数进行解密,默认 true | ||
| 15 | + */ | ||
| 16 | + boolean request() default true; | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 是否对响应结果进行加密,默认 true | ||
| 20 | + */ | ||
| 21 | + boolean response() default true; | ||
| 22 | + | ||
| 23 | +} |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.filter; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.io.IoUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; | ||
| 6 | +import cn.hutool.crypto.asymmetric.KeyType; | ||
| 7 | +import cn.hutool.crypto.symmetric.SymmetricDecryptor; | ||
| 8 | +import jakarta.servlet.ReadListener; | ||
| 9 | +import jakarta.servlet.ServletInputStream; | ||
| 10 | +import jakarta.servlet.http.HttpServletRequest; | ||
| 11 | +import jakarta.servlet.http.HttpServletRequestWrapper; | ||
| 12 | + | ||
| 13 | +import java.io.BufferedReader; | ||
| 14 | +import java.io.ByteArrayInputStream; | ||
| 15 | +import java.io.IOException; | ||
| 16 | +import java.io.InputStreamReader; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * 解密请求 {@link HttpServletRequestWrapper} 实现类 | ||
| 20 | + * | ||
| 21 | + * @author 芋道源码 | ||
| 22 | + */ | ||
| 23 | +public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper { | ||
| 24 | + | ||
| 25 | + private final byte[] body; | ||
| 26 | + | ||
| 27 | + public ApiDecryptRequestWrapper(HttpServletRequest request, | ||
| 28 | + SymmetricDecryptor symmetricDecryptor, | ||
| 29 | + AsymmetricDecryptor asymmetricDecryptor) throws IOException { | ||
| 30 | + super(request); | ||
| 31 | + // 读取 body,允许 HEX、BASE64 传输 | ||
| 32 | + String requestBody = StrUtil.utf8Str( | ||
| 33 | + IoUtil.readBytes(request.getInputStream(), false)); | ||
| 34 | + | ||
| 35 | + // 解密 body | ||
| 36 | + body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody) | ||
| 37 | + : asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public BufferedReader getReader() { | ||
| 42 | + return new BufferedReader(new InputStreamReader(this.getInputStream())); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @Override | ||
| 46 | + public int getContentLength() { | ||
| 47 | + return body.length; | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @Override | ||
| 51 | + public long getContentLengthLong() { | ||
| 52 | + return body.length; | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + @Override | ||
| 56 | + public ServletInputStream getInputStream() { | ||
| 57 | + ByteArrayInputStream stream = new ByteArrayInputStream(body); | ||
| 58 | + return new ServletInputStream() { | ||
| 59 | + | ||
| 60 | + @Override | ||
| 61 | + public int read() { | ||
| 62 | + return stream.read(); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + @Override | ||
| 66 | + public int available() { | ||
| 67 | + return body.length; | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + @Override | ||
| 71 | + public boolean isFinished() { | ||
| 72 | + return false; | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + @Override | ||
| 76 | + public boolean isReady() { | ||
| 77 | + return false; | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + @Override | ||
| 81 | + public void setReadListener(ReadListener readListener) { | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + }; | ||
| 85 | + } | ||
| 86 | +} |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.filter; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | +import cn.hutool.crypto.SecureUtil; | ||
| 5 | +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; | ||
| 6 | +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; | ||
| 7 | +import cn.hutool.crypto.symmetric.SymmetricDecryptor; | ||
| 8 | +import cn.hutool.crypto.symmetric.SymmetricEncryptor; | ||
| 9 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 10 | +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | ||
| 11 | +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||
| 12 | +import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties; | ||
| 13 | +import cn.iocoder.yudao.framework.encrypt.core.annotation.ApiEncrypt; | ||
| 14 | +import cn.iocoder.yudao.framework.web.config.WebProperties; | ||
| 15 | +import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; | ||
| 16 | +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; | ||
| 17 | +import jakarta.servlet.FilterChain; | ||
| 18 | +import jakarta.servlet.ServletException; | ||
| 19 | +import jakarta.servlet.http.HttpServletRequest; | ||
| 20 | +import jakarta.servlet.http.HttpServletResponse; | ||
| 21 | +import lombok.extern.slf4j.Slf4j; | ||
| 22 | +import org.springframework.http.HttpMethod; | ||
| 23 | +import org.springframework.web.method.HandlerMethod; | ||
| 24 | +import org.springframework.web.servlet.HandlerExecutionChain; | ||
| 25 | +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; | ||
| 26 | + | ||
| 27 | +import java.io.IOException; | ||
| 28 | + | ||
| 29 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | ||
| 30 | + | ||
| 31 | +/** | ||
| 32 | + * API 加密过滤器,处理 {@link ApiEncrypt} 注解。 | ||
| 33 | + * | ||
| 34 | + * 1. 解密请求参数 | ||
| 35 | + * 2. 加密响应结果 | ||
| 36 | + * | ||
| 37 | + * 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢? | ||
| 38 | + * 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!! | ||
| 39 | + * | ||
| 40 | + * @author 芋道源码 | ||
| 41 | + */ | ||
| 42 | +@Slf4j | ||
| 43 | +public class ApiEncryptFilter extends ApiRequestFilter { | ||
| 44 | + | ||
| 45 | + private final ApiEncryptProperties apiEncryptProperties; | ||
| 46 | + | ||
| 47 | + private final RequestMappingHandlerMapping requestMappingHandlerMapping; | ||
| 48 | + | ||
| 49 | + private final GlobalExceptionHandler globalExceptionHandler; | ||
| 50 | + | ||
| 51 | + private final SymmetricDecryptor requestSymmetricDecryptor; | ||
| 52 | + private final AsymmetricDecryptor requestAsymmetricDecryptor; | ||
| 53 | + | ||
| 54 | + private final SymmetricEncryptor responseSymmetricEncryptor; | ||
| 55 | + private final AsymmetricEncryptor responseAsymmetricEncryptor; | ||
| 56 | + | ||
| 57 | + public ApiEncryptFilter(WebProperties webProperties, | ||
| 58 | + ApiEncryptProperties apiEncryptProperties, | ||
| 59 | + RequestMappingHandlerMapping requestMappingHandlerMapping, | ||
| 60 | + GlobalExceptionHandler globalExceptionHandler) { | ||
| 61 | + super(webProperties); | ||
| 62 | + this.apiEncryptProperties = apiEncryptProperties; | ||
| 63 | + this.requestMappingHandlerMapping = requestMappingHandlerMapping; | ||
| 64 | + this.globalExceptionHandler = globalExceptionHandler; | ||
| 65 | + if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) { | ||
| 66 | + this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey())); | ||
| 67 | + this.requestAsymmetricDecryptor = null; | ||
| 68 | + this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey())); | ||
| 69 | + this.responseAsymmetricEncryptor = null; | ||
| 70 | + } else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) { | ||
| 71 | + this.requestSymmetricDecryptor = null; | ||
| 72 | + this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null); | ||
| 73 | + this.responseSymmetricEncryptor = null; | ||
| 74 | + this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey()); | ||
| 75 | + } else { | ||
| 76 | + // 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。 | ||
| 77 | + throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm()); | ||
| 78 | + } | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + @Override | ||
| 82 | + @SuppressWarnings("NullableProblems") | ||
| 83 | + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) | ||
| 84 | + throws ServletException, IOException { | ||
| 85 | + // 获取 @ApiEncrypt 注解 | ||
| 86 | + ApiEncrypt apiEncrypt = getApiEncrypt(request); | ||
| 87 | + boolean requestEnable = apiEncrypt != null && apiEncrypt.request(); | ||
| 88 | + boolean responseEnable = apiEncrypt != null && apiEncrypt.response(); | ||
| 89 | + String encryptHeader = request.getHeader(apiEncryptProperties.getHeader()); | ||
| 90 | + if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) { | ||
| 91 | + chain.doFilter(request, response); | ||
| 92 | + return; | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + // 1. 解密请求 | ||
| 96 | + if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()), | ||
| 97 | + HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) { | ||
| 98 | + try { | ||
| 99 | + if (StrUtil.isNotBlank(encryptHeader)) { | ||
| 100 | + request = new ApiDecryptRequestWrapper(request, | ||
| 101 | + requestSymmetricDecryptor, requestAsymmetricDecryptor); | ||
| 102 | + } else if (requestEnable) { | ||
| 103 | + throw invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头"); | ||
| 104 | + } | ||
| 105 | + } catch (Exception ex) { | ||
| 106 | + CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex); | ||
| 107 | + ServletUtils.writeJSON(response, result); | ||
| 108 | + return; | ||
| 109 | + } | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + // 2. 执行过滤器链 | ||
| 113 | + if (responseEnable) { | ||
| 114 | + // 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!! | ||
| 115 | + response = new ApiEncryptResponseWrapper(response); | ||
| 116 | + } | ||
| 117 | + chain.doFilter(request, response); | ||
| 118 | + | ||
| 119 | + // 3. 加密响应(真正执行) | ||
| 120 | + if (responseEnable) { | ||
| 121 | + ((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties, | ||
| 122 | + responseSymmetricEncryptor, responseAsymmetricEncryptor); | ||
| 123 | + } | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + /** | ||
| 127 | + * 获取 @ApiEncrypt 注解 | ||
| 128 | + * | ||
| 129 | + * @param request 请求 | ||
| 130 | + */ | ||
| 131 | + private ApiEncrypt getApiEncrypt(HttpServletRequest request) { | ||
| 132 | + try { | ||
| 133 | + HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request); | ||
| 134 | + if (mappingHandler == null) { | ||
| 135 | + return null; | ||
| 136 | + } | ||
| 137 | + Object handler = mappingHandler.getHandler(); | ||
| 138 | + if (handler instanceof HandlerMethod handlerMethod) { | ||
| 139 | + ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class); | ||
| 140 | + if (annotation == null) { | ||
| 141 | + annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class); | ||
| 142 | + } | ||
| 143 | + return annotation; | ||
| 144 | + } | ||
| 145 | + } catch (Exception e) { | ||
| 146 | + log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]", | ||
| 147 | + request.getRequestURI(), request.getMethod(), e); | ||
| 148 | + } | ||
| 149 | + return null; | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | +} |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt.core.filter; | ||
| 2 | + | ||
| 3 | +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; | ||
| 4 | +import cn.hutool.crypto.asymmetric.KeyType; | ||
| 5 | +import cn.hutool.crypto.symmetric.SymmetricEncryptor; | ||
| 6 | +import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties; | ||
| 7 | +import jakarta.servlet.ServletOutputStream; | ||
| 8 | +import jakarta.servlet.WriteListener; | ||
| 9 | +import jakarta.servlet.http.HttpServletResponse; | ||
| 10 | +import jakarta.servlet.http.HttpServletResponseWrapper; | ||
| 11 | + | ||
| 12 | +import java.io.ByteArrayOutputStream; | ||
| 13 | +import java.io.IOException; | ||
| 14 | +import java.io.OutputStreamWriter; | ||
| 15 | +import java.io.PrintWriter; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * 加密响应 {@link HttpServletResponseWrapper} 实现类 | ||
| 19 | + * | ||
| 20 | + * @author 芋道源码 | ||
| 21 | + */ | ||
| 22 | +public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper { | ||
| 23 | + | ||
| 24 | + private final ByteArrayOutputStream byteArrayOutputStream; | ||
| 25 | + private final ServletOutputStream servletOutputStream; | ||
| 26 | + private final PrintWriter printWriter; | ||
| 27 | + | ||
| 28 | + public ApiEncryptResponseWrapper(HttpServletResponse response) { | ||
| 29 | + super(response); | ||
| 30 | + this.byteArrayOutputStream = new ByteArrayOutputStream(); | ||
| 31 | + this.servletOutputStream = this.getOutputStream(); | ||
| 32 | + this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream)); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + public void encrypt(ApiEncryptProperties properties, | ||
| 36 | + SymmetricEncryptor symmetricEncryptor, | ||
| 37 | + AsymmetricEncryptor asymmetricEncryptor) throws IOException { | ||
| 38 | + // 1.1 清空 body | ||
| 39 | + HttpServletResponse response = (HttpServletResponse) this.getResponse(); | ||
| 40 | + response.resetBuffer(); | ||
| 41 | + // 1.2 获取 body | ||
| 42 | + this.flushBuffer(); | ||
| 43 | + byte[] body = byteArrayOutputStream.toByteArray(); | ||
| 44 | + | ||
| 45 | + // 2. 加密 body | ||
| 46 | + String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body) | ||
| 47 | + : asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey); | ||
| 48 | + response.getWriter().write(encryptedBody); | ||
| 49 | + | ||
| 50 | + // 3. 添加加密 header 标识 | ||
| 51 | + this.addHeader(properties.getHeader(), "true"); | ||
| 52 | + // 特殊:特殊:https://juejin.cn/post/6867327674675625992 | ||
| 53 | + this.addHeader("Access-Control-Expose-Headers", properties.getHeader()); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public PrintWriter getWriter() { | ||
| 58 | + return printWriter; | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + @Override | ||
| 62 | + public void flushBuffer() throws IOException { | ||
| 63 | + if (servletOutputStream != null) { | ||
| 64 | + servletOutputStream.flush(); | ||
| 65 | + } | ||
| 66 | + if (printWriter != null) { | ||
| 67 | + printWriter.flush(); | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + @Override | ||
| 72 | + public void reset() { | ||
| 73 | + byteArrayOutputStream.reset(); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + @Override | ||
| 77 | + public ServletOutputStream getOutputStream() { | ||
| 78 | + return new ServletOutputStream() { | ||
| 79 | + | ||
| 80 | + @Override | ||
| 81 | + public boolean isReady() { | ||
| 82 | + return false; | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + @Override | ||
| 86 | + public void setWriteListener(WriteListener writeListener) { | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + @Override | ||
| 90 | + public void write(int b) { | ||
| 91 | + byteArrayOutputStream.write(b); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + @Override | ||
| 95 | + @SuppressWarnings("NullableProblems") | ||
| 96 | + public void write(byte[] b) throws IOException { | ||
| 97 | + byteArrayOutputStream.write(b); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + @Override | ||
| 101 | + @SuppressWarnings("NullableProblems") | ||
| 102 | + public void write(byte[] b, int off, int len) { | ||
| 103 | + byteArrayOutputStream.write(b, off, len); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + }; | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | +} | ||
| 0 | \ No newline at end of file | 110 | \ No newline at end of file |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java
0 → 100644
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java
| 1 | package cn.iocoder.yudao.framework.web.core.filter; | 1 | package cn.iocoder.yudao.framework.web.core.filter; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | 3 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| 4 | - | ||
| 5 | import jakarta.servlet.ReadListener; | 4 | import jakarta.servlet.ReadListener; |
| 6 | import jakarta.servlet.ServletInputStream; | 5 | import jakarta.servlet.ServletInputStream; |
| 7 | import jakarta.servlet.http.HttpServletRequest; | 6 | import jakarta.servlet.http.HttpServletRequest; |
| 8 | import jakarta.servlet.http.HttpServletRequestWrapper; | 7 | import jakarta.servlet.http.HttpServletRequestWrapper; |
| 8 | + | ||
| 9 | import java.io.BufferedReader; | 9 | import java.io.BufferedReader; |
| 10 | import java.io.ByteArrayInputStream; | 10 | import java.io.ByteArrayInputStream; |
| 11 | -import java.io.IOException; | ||
| 12 | import java.io.InputStreamReader; | 11 | import java.io.InputStreamReader; |
| 13 | 12 | ||
| 14 | /** | 13 | /** |
| @@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { | @@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { | ||
| 29 | } | 28 | } |
| 30 | 29 | ||
| 31 | @Override | 30 | @Override |
| 32 | - public BufferedReader getReader() throws IOException { | 31 | + public BufferedReader getReader() { |
| 33 | return new BufferedReader(new InputStreamReader(this.getInputStream())); | 32 | return new BufferedReader(new InputStreamReader(this.getInputStream())); |
| 34 | } | 33 | } |
| 35 | 34 | ||
| 36 | @Override | 35 | @Override |
| 37 | - public ServletInputStream getInputStream() throws IOException { | 36 | + public int getContentLength() { |
| 37 | + return body.length; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public long getContentLengthLong() { | ||
| 42 | + return body.length; | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @Override | ||
| 46 | + public ServletInputStream getInputStream() { | ||
| 38 | final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); | 47 | final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); |
| 39 | // 返回 ServletInputStream | 48 | // 返回 ServletInputStream |
| 40 | return new ServletInputStream() { | 49 | return new ServletInputStream() { |
yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
| @@ -3,4 +3,5 @@ cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration | @@ -3,4 +3,5 @@ cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration | ||
| 3 | cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration | 3 | cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration |
| 4 | cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration | 4 | cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration |
| 5 | cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration | 5 | cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration |
| 6 | -cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration | ||
| 7 | \ No newline at end of file | 6 | \ No newline at end of file |
| 7 | +cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration | ||
| 8 | +cn.iocoder.yudao.framework.encrypt.config.YudaoApiEncryptAutoConfiguration | ||
| 8 | \ No newline at end of file | 9 | \ No newline at end of file |
yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.encrypt; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.RandomUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import cn.hutool.crypto.SecureUtil; | ||
| 6 | +import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm; | ||
| 7 | +import cn.hutool.crypto.asymmetric.KeyType; | ||
| 8 | +import cn.hutool.crypto.asymmetric.RSA; | ||
| 9 | +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; | ||
| 10 | +import org.junit.jupiter.api.Test; | ||
| 11 | + | ||
| 12 | +import java.util.Objects; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 各种 API 加解密的测试类:不是单测,而是方便大家生成密钥、加密、解密等操作。 | ||
| 16 | + * | ||
| 17 | + * @author 芋道源码 | ||
| 18 | + */ | ||
| 19 | +@SuppressWarnings("ConstantValue") | ||
| 20 | +public class ApiEncryptTest { | ||
| 21 | + | ||
| 22 | + @Test | ||
| 23 | + public void testGenerateAsymmetric() { | ||
| 24 | + String asymmetricAlgorithm = AsymmetricAlgorithm.RSA.getValue(); | ||
| 25 | +// String asymmetricAlgorithm = "SM2"; | ||
| 26 | +// String asymmetricAlgorithm = SM4.ALGORITHM_NAME; | ||
| 27 | +// String asymmetricAlgorithm = SymmetricAlgorithm.AES.getValue(); | ||
| 28 | + String requestClientKey = null; | ||
| 29 | + String requestServerKey = null; | ||
| 30 | + String responseClientKey = null; | ||
| 31 | + String responseServerKey = null; | ||
| 32 | + if (Objects.equals(asymmetricAlgorithm, AsymmetricAlgorithm.RSA.getValue())) { | ||
| 33 | + // 请求的密钥 | ||
| 34 | + RSA requestRsa = SecureUtil.rsa(); | ||
| 35 | + requestClientKey = requestRsa.getPublicKeyBase64(); | ||
| 36 | + requestServerKey = requestRsa.getPrivateKeyBase64(); | ||
| 37 | + // 响应的密钥 | ||
| 38 | + RSA responseRsa = new RSA(); | ||
| 39 | + responseClientKey = responseRsa.getPrivateKeyBase64(); | ||
| 40 | + responseServerKey = responseRsa.getPublicKeyBase64(); | ||
| 41 | + } else if (Objects.equals(asymmetricAlgorithm, SymmetricAlgorithm.AES.getValue())) { | ||
| 42 | + // AES 密钥可选 32、24、16 位 | ||
| 43 | + // 请求的密钥(前后端密钥一致) | ||
| 44 | + requestClientKey = RandomUtil.randomNumbers(32); | ||
| 45 | + requestServerKey = requestClientKey; | ||
| 46 | + // 响应的密钥(前后端密钥一致) | ||
| 47 | + responseClientKey = RandomUtil.randomNumbers(32); | ||
| 48 | + responseServerKey = responseClientKey; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 打印结果 | ||
| 52 | + System.out.println("requestClientKey = " + requestClientKey); | ||
| 53 | + System.out.println("requestServerKey = " + requestServerKey); | ||
| 54 | + System.out.println("responseClientKey = " + responseClientKey); | ||
| 55 | + System.out.println("responseServerKey = " + responseServerKey); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @Test | ||
| 59 | + public void testEncrypt_aes() { | ||
| 60 | + String key = "52549111389893486934626385991395"; | ||
| 61 | + String body = "{\n" + | ||
| 62 | + " \"username\": \"admin\",\n" + | ||
| 63 | + " \"password\": \"admin123\",\n" + | ||
| 64 | + " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" + | ||
| 65 | + " \"code\": \"1024\"\n" + | ||
| 66 | + "}"; | ||
| 67 | + String encrypt = SecureUtil.aes(StrUtil.utf8Bytes(key)) | ||
| 68 | + .encryptBase64(body); | ||
| 69 | + System.out.println("encrypt = " + encrypt); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + @Test | ||
| 73 | + public void testEncrypt_rsa() { | ||
| 74 | + String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB"; | ||
| 75 | + String body = "{\n" + | ||
| 76 | + " \"username\": \"admin\",\n" + | ||
| 77 | + " \"password\": \"admin123\",\n" + | ||
| 78 | + " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" + | ||
| 79 | + " \"code\": \"1024\"\n" + | ||
| 80 | + "}"; | ||
| 81 | + String encrypt = SecureUtil.rsa(null, key) | ||
| 82 | + .encryptBase64(body, KeyType.PublicKey); | ||
| 83 | + System.out.println("encrypt = " + encrypt); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | +} |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
| @@ -72,6 +72,9 @@ public class BpmModelMetaInfoVO { | @@ -72,6 +72,9 @@ public class BpmModelMetaInfoVO { | ||
| 72 | @Schema(description = "允许撤销审批中的申请", example = "true") | 72 | @Schema(description = "允许撤销审批中的申请", example = "true") |
| 73 | private Boolean allowCancelRunningProcess; | 73 | private Boolean allowCancelRunningProcess; |
| 74 | 74 | ||
| 75 | + @Schema(description = "允许允许审批人撤回任务", example = "false") | ||
| 76 | + private Boolean allowWithdrawTask; | ||
| 77 | + | ||
| 75 | @Schema(description = "流程 ID 规则", example = "{}") | 78 | @Schema(description = "流程 ID 规则", example = "{}") |
| 76 | private ProcessIdRule processIdRule; | 79 | private ProcessIdRule processIdRule; |
| 77 | 80 |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java
| @@ -219,6 +219,14 @@ public class BpmTaskController { | @@ -219,6 +219,14 @@ public class BpmTaskController { | ||
| 219 | return success(true); | 219 | return success(true); |
| 220 | } | 220 | } |
| 221 | 221 | ||
| 222 | + @PutMapping("/withdraw") | ||
| 223 | + @Operation(summary = "撤回任务") | ||
| 224 | + @PreAuthorize("@ss.hasPermission('bpm:task:update')") | ||
| 225 | + public CommonResult<Boolean> withdrawTask(@RequestParam("taskId") String taskId) { | ||
| 226 | + taskService.withdrawTask(getLoginUserId(), taskId); | ||
| 227 | + return success(true); | ||
| 228 | + } | ||
| 229 | + | ||
| 222 | @GetMapping("/list-by-parent-task-id") | 230 | @GetMapping("/list-by-parent-task-id") |
| 223 | @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表 | 231 | @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表 |
| 224 | @Parameter(name = "parentTaskId", description = "父级任务编号", required = true) | 232 | @Parameter(name = "parentTaskId", description = "父级任务编号", required = true) |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java
| @@ -173,6 +173,11 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { | @@ -173,6 +173,11 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { | ||
| 173 | private Boolean allowCancelRunningProcess; | 173 | private Boolean allowCancelRunningProcess; |
| 174 | 174 | ||
| 175 | /** | 175 | /** |
| 176 | + * 是否允许审批人撤回任务 | ||
| 177 | + */ | ||
| 178 | + private Boolean allowWithdrawTask; | ||
| 179 | + | ||
| 180 | + /** | ||
| 176 | * 流程 ID 规则 | 181 | * 流程 ID 规则 |
| 177 | */ | 182 | */ |
| 178 | @TableField(typeHandler = JacksonTypeHandler.class) | 183 | @TableField(typeHandler = JacksonTypeHandler.class) |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
| @@ -60,6 +60,10 @@ public interface ErrorCodeConstants { | @@ -60,6 +60,10 @@ public interface ErrorCodeConstants { | ||
| 60 | ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在"); | 60 | ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在"); |
| 61 | ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); | 61 | ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); |
| 62 | ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); | 62 | ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); |
| 63 | + ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!"); | ||
| 64 | + ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); | ||
| 65 | + ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!"); | ||
| 66 | + ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,下一节点不满足撤回条件!"); | ||
| 63 | 67 | ||
| 64 | // ========== 动态表单模块 1-009-010-000 ========== | 68 | // ========== 动态表单模块 1-009-010-000 ========== |
| 65 | ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); | 69 | ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java
| @@ -35,6 +35,7 @@ public enum BpmReasonEnum { | @@ -35,6 +35,7 @@ public enum BpmReasonEnum { | ||
| 35 | APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"), | 35 | APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"), |
| 36 | APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"), | 36 | APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"), |
| 37 | CANCEL_BY_PROCESS_CLEAN("进程清理自动取消"), | 37 | CANCEL_BY_PROCESS_CLEAN("进程清理自动取消"), |
| 38 | + CANCEL_BY_WITHDRAW("前一任务撤回,系统自动取消"), | ||
| 38 | ; | 39 | ; |
| 39 | 40 | ||
| 40 | private final String reason; | 41 | private final String reason; |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
| @@ -852,7 +852,7 @@ public class BpmnModelUtils { | @@ -852,7 +852,7 @@ public class BpmnModelUtils { | ||
| 852 | } else if (flowNode instanceof ScriptTask) { | 852 | } else if (flowNode instanceof ScriptTask) { |
| 853 | skipExpression = ((ScriptTask) flowNode).getSkipExpression(); | 853 | skipExpression = ((ScriptTask) flowNode).getSkipExpression(); |
| 854 | } | 854 | } |
| 855 | - | 855 | + |
| 856 | if (StrUtil.isEmpty(skipExpression)) { | 856 | if (StrUtil.isEmpty(skipExpression)) { |
| 857 | return false; | 857 | return false; |
| 858 | } | 858 | } |
| @@ -909,6 +909,49 @@ public class BpmnModelUtils { | @@ -909,6 +909,49 @@ public class BpmnModelUtils { | ||
| 909 | } | 909 | } |
| 910 | 910 | ||
| 911 | /** | 911 | /** |
| 912 | + * 查找起始节点下一个用户任务列表列表 | ||
| 913 | + * | ||
| 914 | + * @param source 起始节点 | ||
| 915 | + * @return 结果 | ||
| 916 | + */ | ||
| 917 | + public static List<UserTask> getNextUserTasks(FlowElement source) { | ||
| 918 | + return getNextUserTasks(source, null, null); | ||
| 919 | + } | ||
| 920 | + | ||
| 921 | + /** | ||
| 922 | + * 查找起始节点下一个用户任务列表列表 | ||
| 923 | + * @param source 起始节点 | ||
| 924 | + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 | ||
| 925 | + * @param userTaskList 用户任务列表 | ||
| 926 | + * @return 结果 | ||
| 927 | + */ | ||
| 928 | + public static List<UserTask> getNextUserTasks(FlowElement source, Set<String> hasSequenceFlow, List<UserTask> userTaskList) { | ||
| 929 | + hasSequenceFlow = Optional.ofNullable(hasSequenceFlow).orElse(new HashSet<>()); | ||
| 930 | + userTaskList = Optional.ofNullable(userTaskList).orElse(new ArrayList<>()); | ||
| 931 | + // 获取出口连线 | ||
| 932 | + List<SequenceFlow> sequenceFlows = getElementOutgoingFlows(source); | ||
| 933 | + if (!sequenceFlows.isEmpty()) { | ||
| 934 | + for (SequenceFlow sequenceFlow : sequenceFlows) { | ||
| 935 | + // 如果发现连线重复,说明循环了,跳过这个循环 | ||
| 936 | + if (hasSequenceFlow.contains(sequenceFlow.getId())) { | ||
| 937 | + continue; | ||
| 938 | + } | ||
| 939 | + // 添加已经走过的连线 | ||
| 940 | + hasSequenceFlow.add(sequenceFlow.getId()); | ||
| 941 | + FlowElement targetFlowElement = sequenceFlow.getTargetFlowElement(); | ||
| 942 | + if (targetFlowElement instanceof UserTask) { | ||
| 943 | + // 若节点为用户任务,加入到结果列表中 | ||
| 944 | + userTaskList.add((UserTask) targetFlowElement); | ||
| 945 | + } else { | ||
| 946 | + // 若节点非用户任务,继续递归查找下一个节点 | ||
| 947 | + getNextUserTasks(targetFlowElement, hasSequenceFlow, userTaskList); | ||
| 948 | + } | ||
| 949 | + } | ||
| 950 | + } | ||
| 951 | + return userTaskList; | ||
| 952 | + } | ||
| 953 | + | ||
| 954 | + /** | ||
| 912 | * 处理排它网关 | 955 | * 处理排它网关 |
| 913 | * | 956 | * |
| 914 | * @param gateway 排他网关 | 957 | * @param gateway 排他网关 |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
| @@ -250,6 +250,14 @@ public interface BpmTaskService { | @@ -250,6 +250,14 @@ public interface BpmTaskService { | ||
| 250 | */ | 250 | */ |
| 251 | void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO); | 251 | void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO); |
| 252 | 252 | ||
| 253 | + /** | ||
| 254 | + * 撤回任务 | ||
| 255 | + * | ||
| 256 | + * @param userId 用户编号 | ||
| 257 | + * @param taskId 任务编号 | ||
| 258 | + */ | ||
| 259 | + void withdrawTask(Long userId, String taskId); | ||
| 260 | + | ||
| 253 | // ========== Event 事件相关方法 ========== | 261 | // ========== Event 事件相关方法 ========== |
| 254 | 262 | ||
| 255 | /** | 263 | /** |
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
| @@ -196,7 +196,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -196,7 +196,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 196 | /** | 196 | /** |
| 197 | * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务 | 197 | * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务 |
| 198 | * | 198 | * |
| 199 | - * @param userId 用户编号 | 199 | + * @param userId 用户编号 |
| 200 | * @param processInstanceId 流程编号 | 200 | * @param processInstanceId 流程编号 |
| 201 | * @return 任务 | 201 | * @return 任务 |
| 202 | */ | 202 | */ |
| @@ -599,15 +599,15 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -599,15 +599,15 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 599 | 599 | ||
| 600 | /** | 600 | /** |
| 601 | * 校验选择的下一个节点的审批人,是否合法 | 601 | * 校验选择的下一个节点的审批人,是否合法 |
| 602 | - * | 602 | + * <p> |
| 603 | * 1. 是否有漏选:没有选择审批人 | 603 | * 1. 是否有漏选:没有选择审批人 |
| 604 | * 2. 是否有多选:非下一个节点 | 604 | * 2. 是否有多选:非下一个节点 |
| 605 | * | 605 | * |
| 606 | * @param taskDefinitionKey 当前任务节点标识 | 606 | * @param taskDefinitionKey 当前任务节点标识 |
| 607 | - * @param variables 流程变量 | ||
| 608 | - * @param bpmnModel 流程模型 | ||
| 609 | - * @param nextAssignees 下一个节点审批人集合(参数) | ||
| 610 | - * @param processInstance 流程实例 | 607 | + * @param variables 流程变量 |
| 608 | + * @param bpmnModel 流程模型 | ||
| 609 | + * @param nextAssignees 下一个节点审批人集合(参数) | ||
| 610 | + * @param processInstance 流程实例 | ||
| 611 | */ | 611 | */ |
| 612 | @SuppressWarnings("unchecked") | 612 | @SuppressWarnings("unchecked") |
| 613 | private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel, | 613 | private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel, |
| @@ -659,7 +659,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -659,7 +659,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 659 | approveUserSelectAssignees = new HashMap<>(); | 659 | approveUserSelectAssignees = new HashMap<>(); |
| 660 | } | 660 | } |
| 661 | approveUserSelectAssignees.put(nextFlowNode.getId(), assignees); | 661 | approveUserSelectAssignees.put(nextFlowNode.getId(), assignees); |
| 662 | - Map<String,List<Long>> existingApproveUserSelectAssignees = (Map<String,List<Long>>) variables.get( | 662 | + Map<String, List<Long>> existingApproveUserSelectAssignees = (Map<String, List<Long>>) variables.get( |
| 663 | BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); | 663 | BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); |
| 664 | if (CollUtil.isNotEmpty(existingApproveUserSelectAssignees)) { | 664 | if (CollUtil.isNotEmpty(existingApproveUserSelectAssignees)) { |
| 665 | approveUserSelectAssignees.putAll(existingApproveUserSelectAssignees); | 665 | approveUserSelectAssignees.putAll(existingApproveUserSelectAssignees); |
| @@ -1177,6 +1177,63 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -1177,6 +1177,63 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 1177 | processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId()); | 1177 | processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId()); |
| 1178 | } | 1178 | } |
| 1179 | 1179 | ||
| 1180 | + @Override | ||
| 1181 | + @Transactional(rollbackFor = Exception.class) | ||
| 1182 | + public void withdrawTask(Long userId, String taskId) { | ||
| 1183 | + // 1.1 查询本人已办任务 | ||
| 1184 | + HistoricTaskInstance taskInstance = historyService.createHistoricTaskInstanceQuery() | ||
| 1185 | + .taskId(taskId).taskAssignee(userId.toString()).finished().singleResult(); | ||
| 1186 | + if (ObjUtil.isNull(taskInstance)) { | ||
| 1187 | + throw exception(TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS); | ||
| 1188 | + } | ||
| 1189 | + // 1.2 校验流程是否结束 | ||
| 1190 | + ProcessInstance processInstance = processInstanceService.getProcessInstance(taskInstance.getProcessInstanceId()); | ||
| 1191 | + if (ObjUtil.isNull(processInstance)) { | ||
| 1192 | + throw exception(TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING); | ||
| 1193 | + } | ||
| 1194 | + // 1.3 判断此流程是否允许撤回 | ||
| 1195 | + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo( | ||
| 1196 | + processInstance.getProcessDefinitionId()); | ||
| 1197 | + if (ObjUtil.isNull(processDefinitionInfo) || !Boolean.TRUE.equals(processDefinitionInfo.getAllowWithdrawTask())) { | ||
| 1198 | + throw exception(TASK_WITHDRAW_FAIL_NOT_ALLOW); | ||
| 1199 | + } | ||
| 1200 | + // 1.4 判断下一个节点是否被审批过,如果是则无法撤回 | ||
| 1201 | + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskInstance.getProcessDefinitionId()); | ||
| 1202 | + UserTask userTask = (UserTask) BpmnModelUtils.getFlowElementById(bpmnModel, taskInstance.getTaskDefinitionKey()); | ||
| 1203 | + List<String> nextUserTaskKeys = convertList(BpmnModelUtils.getNextUserTasks(userTask), UserTask::getId); | ||
| 1204 | + if (CollUtil.isEmpty(nextUserTaskKeys)) { | ||
| 1205 | + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); | ||
| 1206 | + } | ||
| 1207 | + // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以;包括 todo 和 done 那边的查询哇??? 是的! | ||
| 1208 | + long nextUserTaskFinishedCount = historyService.createHistoricTaskInstanceQuery() | ||
| 1209 | + .processInstanceId(processInstance.getProcessInstanceId()).taskDefinitionKeys(nextUserTaskKeys) | ||
| 1210 | + .taskCreatedAfter(taskInstance.getEndTime()).finished().count(); | ||
| 1211 | + if (nextUserTaskFinishedCount > 0) { | ||
| 1212 | + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); | ||
| 1213 | + } | ||
| 1214 | + // 1.5 获取需要撤回的运行任务 | ||
| 1215 | + List<Task> runningTasks = taskService.createTaskQuery().processInstanceId(processInstance.getProcessInstanceId()) | ||
| 1216 | + .taskDefinitionKeys(nextUserTaskKeys).active().list(); | ||
| 1217 | + if (CollUtil.isEmpty(runningTasks)) { | ||
| 1218 | + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); | ||
| 1219 | + } | ||
| 1220 | + | ||
| 1221 | + // 2.1 取消当前任务 | ||
| 1222 | + List<String> withdrawExecutionIds = new ArrayList<>(); | ||
| 1223 | + for (Task task : runningTasks) { | ||
| 1224 | + // 标记撤回任务为取消 | ||
| 1225 | + taskService.addComment(task.getId(), taskInstance.getProcessInstanceId(), BpmCommentTypeEnum.CANCEL.getType(), | ||
| 1226 | + BpmCommentTypeEnum.CANCEL.formatComment("前一节点撤回")); | ||
| 1227 | + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_WITHDRAW.getReason()); | ||
| 1228 | + withdrawExecutionIds.add(task.getExecutionId()); | ||
| 1229 | + } | ||
| 1230 | + // 2.2 执行撤回操作 | ||
| 1231 | + runtimeService.createChangeActivityStateBuilder() | ||
| 1232 | + .processInstanceId(processInstance.getProcessInstanceId()) | ||
| 1233 | + .moveExecutionsToSingleActivityId(withdrawExecutionIds, taskInstance.getTaskDefinitionKey()) | ||
| 1234 | + .changeState(); | ||
| 1235 | + } | ||
| 1236 | + | ||
| 1180 | /** | 1237 | /** |
| 1181 | * 校验任务是否能被减签 | 1238 | * 校验任务是否能被减签 |
| 1182 | * | 1239 | * |
| @@ -1223,7 +1280,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -1223,7 +1280,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 1223 | } | 1280 | } |
| 1224 | 1281 | ||
| 1225 | // 2. 任务前置通知 | 1282 | // 2. 任务前置通知 |
| 1226 | - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())){ | 1283 | + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())) { |
| 1227 | BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskBeforeTriggerSetting(); | 1284 | BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskBeforeTriggerSetting(); |
| 1228 | BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, | 1285 | BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, |
| 1229 | setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); | 1286 | setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); |
| @@ -1350,7 +1407,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -1350,7 +1407,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 1350 | .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) | 1407 | .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) |
| 1351 | .finished(); | 1408 | .finished(); |
| 1352 | if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType()) | 1409 | if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType()) |
| 1353 | - && sameAssigneeQuery.count() > 0) { | 1410 | + && sameAssigneeQuery.count() > 0) { |
| 1354 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) | 1411 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) |
| 1355 | .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName())); | 1412 | .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName())); |
| 1356 | return; | 1413 | return; |
| @@ -1362,7 +1419,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -1362,7 +1419,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 1362 | return; | 1419 | return; |
| 1363 | } | 1420 | } |
| 1364 | List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 | 1421 | List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 |
| 1365 | - BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), | 1422 | + BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), |
| 1366 | SequenceFlow::getSourceRef); | 1423 | SequenceFlow::getSourceRef); |
| 1367 | if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) { | 1424 | if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) { |
| 1368 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) | 1425 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) |
| @@ -1387,7 +1444,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -1387,7 +1444,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 1387 | PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); | 1444 | PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); |
| 1388 | if (userTaskElement.getId().equals(START_USER_NODE_ID) | 1445 | if (userTaskElement.getId().equals(START_USER_NODE_ID) |
| 1389 | && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 | 1446 | && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 |
| 1390 | - || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 | 1447 | + || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 |
| 1391 | && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { | 1448 | && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { |
| 1392 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) | 1449 | getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) |
| 1393 | .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); | 1450 | .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); |
| @@ -1456,7 +1513,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | @@ -1456,7 +1513,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { | ||
| 1456 | } | 1513 | } |
| 1457 | 1514 | ||
| 1458 | // 任务后置通知 | 1515 | // 任务后置通知 |
| 1459 | - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())){ | 1516 | + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())) { |
| 1460 | BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskAfterTriggerSetting(); | 1517 | BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskAfterTriggerSetting(); |
| 1461 | BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, | 1518 | BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, |
| 1462 | setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); | 1519 | setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); |
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java
| @@ -14,7 +14,7 @@ public interface LogRecordConstants { | @@ -14,7 +14,7 @@ public interface LogRecordConstants { | ||
| 14 | String CRM_CLUE_CREATE_SUB_TYPE = "创建线索"; | 14 | String CRM_CLUE_CREATE_SUB_TYPE = "创建线索"; |
| 15 | String CRM_CLUE_CREATE_SUCCESS = "创建了线索{{#clue.name}}"; | 15 | String CRM_CLUE_CREATE_SUCCESS = "创建了线索{{#clue.name}}"; |
| 16 | String CRM_CLUE_UPDATE_SUB_TYPE = "更新线索"; | 16 | String CRM_CLUE_UPDATE_SUB_TYPE = "更新线索"; |
| 17 | - String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReq}}"; | 17 | + String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReqVO}}"; |
| 18 | String CRM_CLUE_DELETE_SUB_TYPE = "删除线索"; | 18 | String CRM_CLUE_DELETE_SUB_TYPE = "删除线索"; |
| 19 | String CRM_CLUE_DELETE_SUCCESS = "删除了线索【{{#clueName}}】"; | 19 | String CRM_CLUE_DELETE_SUCCESS = "删除了线索【{{#clueName}}】"; |
| 20 | String CRM_CLUE_TRANSFER_SUB_TYPE = "转移线索"; | 20 | String CRM_CLUE_TRANSFER_SUB_TYPE = "转移线索"; |
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java
| @@ -106,7 +106,7 @@ public class CrmClueServiceImpl implements CrmClueService { | @@ -106,7 +106,7 @@ public class CrmClueServiceImpl implements CrmClueService { | ||
| 106 | 106 | ||
| 107 | // 3. 记录操作日志上下文 | 107 | // 3. 记录操作日志上下文 |
| 108 | updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 | 108 | updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 |
| 109 | - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class)); | 109 | + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmClueSaveReqVO.class)); |
| 110 | LogRecordContext.putVariable("clueName", oldClue.getName()); | 110 | LogRecordContext.putVariable("clueName", oldClue.getName()); |
| 111 | } | 111 | } |
| 112 | 112 |
yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java
| @@ -28,6 +28,9 @@ public class MpMessagePageReqVO extends PageParam { | @@ -28,6 +28,9 @@ public class MpMessagePageReqVO extends PageParam { | ||
| 28 | @Schema(description = "公众号粉丝标识", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") | 28 | @Schema(description = "公众号粉丝标识", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") |
| 29 | private String openid; | 29 | private String openid; |
| 30 | 30 | ||
| 31 | + @Schema(description = "公众号粉丝 UserId", example = "1") | ||
| 32 | + private String userId; | ||
| 33 | + | ||
| 31 | @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) | 34 | @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
| 32 | @Schema(description = "创建时间") | 35 | @Schema(description = "创建时间") |
| 33 | private LocalDateTime[] createTime; | 36 | private LocalDateTime[] createTime; |
yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java
| @@ -15,6 +15,7 @@ public interface MpMessageMapper extends BaseMapperX<MpMessageDO> { | @@ -15,6 +15,7 @@ public interface MpMessageMapper extends BaseMapperX<MpMessageDO> { | ||
| 15 | .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) | 15 | .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) |
| 16 | .eqIfPresent(MpMessageDO::getType, reqVO.getType()) | 16 | .eqIfPresent(MpMessageDO::getType, reqVO.getType()) |
| 17 | .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenid()) | 17 | .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenid()) |
| 18 | + .eqIfPresent(MpMessageDO::getUserId, reqVO.getUserId()) | ||
| 18 | .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime()) | 19 | .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime()) |
| 19 | .orderByDesc(MpMessageDO::getId)); | 20 | .orderByDesc(MpMessageDO::getId)); |
| 20 | } | 21 | } |
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
| @@ -353,7 +353,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa | @@ -353,7 +353,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa | ||
| 353 | } else if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { | 353 | } else if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { |
| 354 | // 由于 rsaCertCheckV1 的第二个参数是 path,所以不能这么调用!!!通过阅读源码,发现可以采用如下方式! | 354 | // 由于 rsaCertCheckV1 的第二个参数是 path,所以不能这么调用!!!通过阅读源码,发现可以采用如下方式! |
| 355 | X509Certificate cert = AntCertificationUtil.getCertFromContent(config.getAlipayPublicCertContent()); | 355 | X509Certificate cert = AntCertificationUtil.getCertFromContent(config.getAlipayPublicCertContent()); |
| 356 | - String publicKey = Base64.encodeBase64String(cert.getEncoded()); | 356 | + String publicKey = Base64.encodeBase64String(cert.getPublicKey().getEncoded()); |
| 357 | verify = AlipaySignature.rsaCheckV1(params, publicKey, | 357 | verify = AlipaySignature.rsaCheckV1(params, publicKey, |
| 358 | StandardCharsets.UTF_8.name(), config.getSignType()); | 358 | StandardCharsets.UTF_8.name(), config.getSignType()); |
| 359 | } else { | 359 | } else { |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java
| @@ -21,13 +21,15 @@ public class MailSendApiImpl implements MailSendApi { | @@ -21,13 +21,15 @@ public class MailSendApiImpl implements MailSendApi { | ||
| 21 | 21 | ||
| 22 | @Override | 22 | @Override |
| 23 | public Long sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) { | 23 | public Long sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) { |
| 24 | - return mailSendService.sendSingleMailToAdmin(reqDTO.getMail(), reqDTO.getUserId(), | 24 | + return mailSendService.sendSingleMailToAdmin(reqDTO.getUserId(), |
| 25 | + reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(), | ||
| 25 | reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); | 26 | reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); |
| 26 | } | 27 | } |
| 27 | 28 | ||
| 28 | @Override | 29 | @Override |
| 29 | public Long sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) { | 30 | public Long sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) { |
| 30 | - return mailSendService.sendSingleMailToMember(reqDTO.getMail(), reqDTO.getUserId(), | 31 | + return mailSendService.sendSingleMailToMember(reqDTO.getUserId(), |
| 32 | + reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(), | ||
| 31 | reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); | 33 | reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); |
| 32 | } | 34 | } |
| 33 | 35 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java
| @@ -4,6 +4,8 @@ import lombok.Data; | @@ -4,6 +4,8 @@ import lombok.Data; | ||
| 4 | 4 | ||
| 5 | import jakarta.validation.constraints.Email; | 5 | import jakarta.validation.constraints.Email; |
| 6 | import jakarta.validation.constraints.NotNull; | 6 | import jakarta.validation.constraints.NotNull; |
| 7 | + | ||
| 8 | +import java.util.List; | ||
| 7 | import java.util.Map; | 9 | import java.util.Map; |
| 8 | 10 | ||
| 9 | /** | 11 | /** |
| @@ -16,13 +18,24 @@ public class MailSendSingleToUserReqDTO { | @@ -16,13 +18,24 @@ public class MailSendSingleToUserReqDTO { | ||
| 16 | 18 | ||
| 17 | /** | 19 | /** |
| 18 | * 用户编号 | 20 | * 用户编号 |
| 21 | + * | ||
| 22 | + * 如果非空,则加载对应用户的邮箱,添加到 {@link #toMails} 中 | ||
| 19 | */ | 23 | */ |
| 20 | private Long userId; | 24 | private Long userId; |
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 收件邮箱 | ||
| 28 | + */ | ||
| 29 | + private List<@Email String> toMails; | ||
| 21 | /** | 30 | /** |
| 22 | - * 邮箱 | 31 | + * 抄送邮箱 |
| 23 | */ | 32 | */ |
| 24 | |||
| 25 | - private String mail; | 33 | + private List<@Email String> ccMails; |
| 34 | + /** | ||
| 35 | + * 密送邮箱 | ||
| 36 | + */ | ||
| 37 | + private List<@Email String> bccMails; | ||
| 38 | + | ||
| 26 | 39 | ||
| 27 | /** | 40 | /** |
| 28 | * 邮件模板编号 | 41 | * 邮件模板编号 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http
| @@ -11,6 +11,24 @@ tag: Yunai.local | @@ -11,6 +11,24 @@ tag: Yunai.local | ||
| 11 | "code": "1024" | 11 | "code": "1024" |
| 12 | } | 12 | } |
| 13 | 13 | ||
| 14 | +### 请求 /login 接口【加密 AES】 => 成功 | ||
| 15 | +POST {{baseUrl}}/system/auth/login | ||
| 16 | +Content-Type: application/json | ||
| 17 | +tenant-id: {{adminTenantId}} | ||
| 18 | +tag: Yunai.local | ||
| 19 | +X-API-ENCRYPT: true | ||
| 20 | + | ||
| 21 | +WvSX9MOrenyGfBhEM0g1/hHgq8ocktMZ9OwAJ6MOG5FUrzYF/rG5JF1eMptQM1wT73VgDS05l/37WeRtad+JrqChAul/sR/SdOsUKqjBhvvQx1JVhzxr6s8uUP67aKTSZ6Psv7O32ELxXrzSaQvG5CInzz3w6sLtbNNLd1kXe6Q= | ||
| 22 | + | ||
| 23 | +### 请求 /login 接口【加密 RSA】 => 成功 | ||
| 24 | +POST {{baseUrl}}/system/auth/login | ||
| 25 | +Content-Type: application/json | ||
| 26 | +tenant-id: {{adminTenantId}} | ||
| 27 | +tag: Yunai.local | ||
| 28 | +X-API-ENCRYPT: true | ||
| 29 | + | ||
| 30 | +e7QZTork9ZV5CmgZvSd+cHZk3xdUxKtowLM02kOha+gxHK2H/daU8nVBYS3+bwuDRy5abf+Pz1QJJGVAEd27wwrXBmupOOA/bhpuzzDwcRuJRD+z+YgiNoEXFDRHERxPYlPqAe9zAHtihD0ceub1AjybQsEsROew4C3Q602XYW0= | ||
| 31 | + | ||
| 14 | ### 请求 /login 接口 => 成功(无验证码) | 32 | ### 请求 /login 接口 => 成功(无验证码) |
| 15 | POST {{baseUrl}}/system/auth/login | 33 | POST {{baseUrl}}/system/auth/login |
| 16 | Content-Type: application/json | 34 | Content-Type: application/json |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java
| @@ -56,6 +56,15 @@ public class DeptController { | @@ -56,6 +56,15 @@ public class DeptController { | ||
| 56 | return success(true); | 56 | return success(true); |
| 57 | } | 57 | } |
| 58 | 58 | ||
| 59 | + @DeleteMapping("/delete-list") | ||
| 60 | + @Operation(summary = "批量删除部门") | ||
| 61 | + @Parameter(name = "ids", description = "编号列表", required = true) | ||
| 62 | + @PreAuthorize("@ss.hasPermission('system:dept:delete')") | ||
| 63 | + public CommonResult<Boolean> deleteDeptList(@RequestParam("ids") List<Long> ids) { | ||
| 64 | + deptService.deleteDeptList(ids); | ||
| 65 | + return success(true); | ||
| 66 | + } | ||
| 67 | + | ||
| 59 | @GetMapping("/list") | 68 | @GetMapping("/list") |
| 60 | @Operation(summary = "获取部门列表") | 69 | @Operation(summary = "获取部门列表") |
| 61 | @PreAuthorize("@ss.hasPermission('system:dept:query')") | 70 | @PreAuthorize("@ss.hasPermission('system:dept:query')") |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java
| @@ -91,7 +91,8 @@ public class MailTemplateController { | @@ -91,7 +91,8 @@ public class MailTemplateController { | ||
| 91 | @Operation(summary = "发送短信") | 91 | @Operation(summary = "发送短信") |
| 92 | @PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')") | 92 | @PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')") |
| 93 | public CommonResult<Long> sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) { | 93 | public CommonResult<Long> sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) { |
| 94 | - return success(mailSendService.sendSingleMailToAdmin(sendReqVO.getMail(), getLoginUserId(), | 94 | + return success(mailSendService.sendSingleMailToAdmin(getLoginUserId(), |
| 95 | + sendReqVO.getToMails(), sendReqVO.getCcMails(), sendReqVO.getBccMails(), | ||
| 95 | sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); | 96 | sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); |
| 96 | } | 97 | } |
| 97 | 98 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java
| @@ -2,13 +2,11 @@ package cn.iocoder.yudao.module.system.controller.admin.mail.vo.log; | @@ -2,13 +2,11 @@ package cn.iocoder.yudao.module.system.controller.admin.mail.vo.log; | ||
| 2 | 2 | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import lombok.Data; | 4 | import lombok.Data; |
| 5 | -import org.springframework.format.annotation.DateTimeFormat; | ||
| 6 | 5 | ||
| 7 | import java.time.LocalDateTime; | 6 | import java.time.LocalDateTime; |
| 7 | +import java.util.List; | ||
| 8 | import java.util.Map; | 8 | import java.util.Map; |
| 9 | 9 | ||
| 10 | -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; | ||
| 11 | - | ||
| 12 | @Schema(description = "管理后台 - 邮件日志 Response VO") | 10 | @Schema(description = "管理后台 - 邮件日志 Response VO") |
| 13 | @Data | 11 | @Data |
| 14 | public class MailLogRespVO { | 12 | public class MailLogRespVO { |
| @@ -22,8 +20,14 @@ public class MailLogRespVO { | @@ -22,8 +20,14 @@ public class MailLogRespVO { | ||
| 22 | @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") | 20 | @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") |
| 23 | private Byte userType; | 21 | private Byte userType; |
| 24 | 22 | ||
| 25 | - @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "76854@qq.com") | ||
| 26 | - private String toMail; | 23 | + @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user1@example.com, user2@example.com") |
| 24 | + private List<String> toMails; | ||
| 25 | + | ||
| 26 | + @Schema(description = "抄送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user3@example.com, user4@example.com") | ||
| 27 | + private List<String> ccMails; | ||
| 28 | + | ||
| 29 | + @Schema(description = "密送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user5@example.com, user6@example.com") | ||
| 30 | + private List<String> bccMails; | ||
| 27 | 31 | ||
| 28 | @Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107") | 32 | @Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107") |
| 29 | private Long accountId; | 33 | private Long accountId; |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java
| @@ -5,15 +5,22 @@ import lombok.Data; | @@ -5,15 +5,22 @@ import lombok.Data; | ||
| 5 | 5 | ||
| 6 | import jakarta.validation.constraints.NotEmpty; | 6 | import jakarta.validation.constraints.NotEmpty; |
| 7 | import jakarta.validation.constraints.NotNull; | 7 | import jakarta.validation.constraints.NotNull; |
| 8 | +import java.util.List; | ||
| 8 | import java.util.Map; | 9 | import java.util.Map; |
| 9 | 10 | ||
| 10 | @Schema(description = "管理后台 - 邮件发送 Req VO") | 11 | @Schema(description = "管理后台 - 邮件发送 Req VO") |
| 11 | @Data | 12 | @Data |
| 12 | public class MailTemplateSendReqVO { | 13 | public class MailTemplateSendReqVO { |
| 13 | 14 | ||
| 14 | - @Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "7685413@qq.com") | 15 | + @Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user1@example.com, user2@example.com]") |
| 15 | @NotEmpty(message = "接收邮箱不能为空") | 16 | @NotEmpty(message = "接收邮箱不能为空") |
| 16 | - private String mail; | 17 | + private List<String> toMails; |
| 18 | + | ||
| 19 | + @Schema(description = "抄送邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user3@example.com, user4@example.com]") | ||
| 20 | + private List<String> ccMails; | ||
| 21 | + | ||
| 22 | + @Schema(description = "密送邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user5@example.com, user6@example.com]") | ||
| 23 | + private List<String> bccMails; | ||
| 17 | 24 | ||
| 18 | @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") | 25 | @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") |
| 19 | @NotNull(message = "模板编码不能为空") | 26 | @NotNull(message = "模板编码不能为空") |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.mail; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.mail; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | 3 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; | ||
| 5 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; | 6 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; |
| 6 | import cn.iocoder.yudao.module.system.enums.mail.MailSendStatusEnum; | 7 | import cn.iocoder.yudao.module.system.enums.mail.MailSendStatusEnum; |
| 7 | import com.baomidou.mybatisplus.annotation.KeySequence; | 8 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| @@ -12,6 +13,7 @@ import lombok.*; | @@ -12,6 +13,7 @@ import lombok.*; | ||
| 12 | 13 | ||
| 13 | import java.io.Serializable; | 14 | import java.io.Serializable; |
| 14 | import java.time.LocalDateTime; | 15 | import java.time.LocalDateTime; |
| 16 | +import java.util.List; | ||
| 15 | import java.util.Map; | 17 | import java.util.Map; |
| 16 | 18 | ||
| 17 | /** | 19 | /** |
| @@ -47,10 +49,22 @@ public class MailLogDO extends BaseDO implements Serializable { | @@ -47,10 +49,22 @@ public class MailLogDO extends BaseDO implements Serializable { | ||
| 47 | * 枚举 {@link UserTypeEnum} | 49 | * 枚举 {@link UserTypeEnum} |
| 48 | */ | 50 | */ |
| 49 | private Integer userType; | 51 | private Integer userType; |
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 接收邮箱地址 | ||
| 55 | + */ | ||
| 56 | + @TableField(typeHandler = StringListTypeHandler.class) | ||
| 57 | + private List<String> toMails; | ||
| 50 | /** | 58 | /** |
| 51 | * 接收邮箱地址 | 59 | * 接收邮箱地址 |
| 52 | */ | 60 | */ |
| 53 | - private String toMail; | 61 | + @TableField(typeHandler = StringListTypeHandler.class) |
| 62 | + private List<String> ccMails; | ||
| 63 | + /** | ||
| 64 | + * 密送邮箱地址 | ||
| 65 | + */ | ||
| 66 | + @TableField(typeHandler = StringListTypeHandler.class) | ||
| 67 | + private List<String> bccMails; | ||
| 54 | 68 | ||
| 55 | /** | 69 | /** |
| 56 | * 邮箱账号编号 | 70 | * 邮箱账号编号 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java
| 1 | package cn.iocoder.yudao.module.system.dal.mysql.mail; | 1 | package cn.iocoder.yudao.module.system.dal.mysql.mail; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | 5 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | 6 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 7 | +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; | ||
| 6 | import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; | 8 | import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; |
| 7 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; | 9 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; |
| 8 | import org.apache.ibatis.annotations.Mapper; | 10 | import org.apache.ibatis.annotations.Mapper; |
| @@ -14,11 +16,12 @@ public interface MailLogMapper extends BaseMapperX<MailLogDO> { | @@ -14,11 +16,12 @@ public interface MailLogMapper extends BaseMapperX<MailLogDO> { | ||
| 14 | return selectPage(reqVO, new LambdaQueryWrapperX<MailLogDO>() | 16 | return selectPage(reqVO, new LambdaQueryWrapperX<MailLogDO>() |
| 15 | .eqIfPresent(MailLogDO::getUserId, reqVO.getUserId()) | 17 | .eqIfPresent(MailLogDO::getUserId, reqVO.getUserId()) |
| 16 | .eqIfPresent(MailLogDO::getUserType, reqVO.getUserType()) | 18 | .eqIfPresent(MailLogDO::getUserType, reqVO.getUserType()) |
| 17 | - .likeIfPresent(MailLogDO::getToMail, reqVO.getToMail()) | ||
| 18 | .eqIfPresent(MailLogDO::getAccountId, reqVO.getAccountId()) | 19 | .eqIfPresent(MailLogDO::getAccountId, reqVO.getAccountId()) |
| 19 | .eqIfPresent(MailLogDO::getTemplateId, reqVO.getTemplateId()) | 20 | .eqIfPresent(MailLogDO::getTemplateId, reqVO.getTemplateId()) |
| 20 | .eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus()) | 21 | .eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus()) |
| 21 | .betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime()) | 22 | .betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime()) |
| 23 | + .apply(StrUtil.isNotBlank(reqVO.getToMail()), | ||
| 24 | + MyBatisUtils.findInSet("to_mails", reqVO.getToMail())) | ||
| 22 | .orderByDesc(MailLogDO::getId)); | 25 | .orderByDesc(MailLogDO::getId)); |
| 23 | } | 26 | } |
| 24 | 27 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
| @@ -136,15 +136,20 @@ public class AliyunSmsClient extends AbstractSmsClient { | @@ -136,15 +136,20 @@ public class AliyunSmsClient extends AbstractSmsClient { | ||
| 136 | .map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))) | 136 | .map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))) |
| 137 | .collect(Collectors.joining("&")); | 137 | .collect(Collectors.joining("&")); |
| 138 | 138 | ||
| 139 | - // 2.1 请求 Header | 139 | + // 2. 请求 Body |
| 140 | + String requestBody = ""; // 短信 API 为 RPC 接口,query parameters 在 uri 中拼接,因此 request body 如果没有特殊要求,设置为空 | ||
| 141 | + String hashedRequestPayload = DigestUtil.sha256Hex(requestBody); | ||
| 142 | + | ||
| 143 | + // 3.1 请求 Header | ||
| 140 | TreeMap<String, String> headers = new TreeMap<>(); | 144 | TreeMap<String, String> headers = new TreeMap<>(); |
| 141 | headers.put("host", HOST); | 145 | headers.put("host", HOST); |
| 142 | headers.put("x-acs-version", VERSION); | 146 | headers.put("x-acs-version", VERSION); |
| 143 | headers.put("x-acs-action", apiName); | 147 | headers.put("x-acs-action", apiName); |
| 144 | headers.put("x-acs-date", FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT")).format(new Date())); | 148 | headers.put("x-acs-date", FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT")).format(new Date())); |
| 145 | headers.put("x-acs-signature-nonce", IdUtil.randomUUID()); | 149 | headers.put("x-acs-signature-nonce", IdUtil.randomUUID()); |
| 150 | + headers.put("x-acs-content-sha256", hashedRequestPayload); | ||
| 146 | 151 | ||
| 147 | - // 2.2 构建签名 Header | 152 | + // 3.2 构建签名 Header |
| 148 | StringBuilder canonicalHeaders = new StringBuilder(); // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起 | 153 | StringBuilder canonicalHeaders = new StringBuilder(); // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起 |
| 149 | StringBuilder signedHeadersBuilder = new StringBuilder(); // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔 | 154 | StringBuilder signedHeadersBuilder = new StringBuilder(); // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔 |
| 150 | headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") | 155 | headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") |
| @@ -157,13 +162,13 @@ public class AliyunSmsClient extends AbstractSmsClient { | @@ -157,13 +162,13 @@ public class AliyunSmsClient extends AbstractSmsClient { | ||
| 157 | }); | 162 | }); |
| 158 | String signedHeaders = signedHeadersBuilder.substring(0, signedHeadersBuilder.length() - 1); | 163 | String signedHeaders = signedHeadersBuilder.substring(0, signedHeadersBuilder.length() - 1); |
| 159 | 164 | ||
| 160 | - // 3. 请求 Body | ||
| 161 | - String requestBody = ""; // 短信 API 为 RPC 接口,query parameters 在 uri 中拼接,因此 request body 如果没有特殊要求,设置为空。 | ||
| 162 | - String hashedRequestBody = DigestUtil.sha256Hex(requestBody); | ||
| 163 | - | ||
| 164 | // 4. 构建 Authorization 签名 | 165 | // 4. 构建 Authorization 签名 |
| 165 | - String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" | ||
| 166 | - + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; | 166 | + String canonicalRequest = "POST" + "\n" + |
| 167 | + "/" + "\n" + | ||
| 168 | + queryString + "\n" + | ||
| 169 | + canonicalHeaders + "\n" + | ||
| 170 | + signedHeaders + "\n" + | ||
| 171 | + hashedRequestPayload; | ||
| 167 | String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest); | 172 | String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest); |
| 168 | String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest; | 173 | String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest; |
| 169 | String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 | 174 | String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 |
| @@ -184,7 +189,7 @@ public class AliyunSmsClient extends AbstractSmsClient { | @@ -184,7 +189,7 @@ public class AliyunSmsClient extends AbstractSmsClient { | ||
| 184 | @SneakyThrows | 189 | @SneakyThrows |
| 185 | private static String percentCode(String str) { | 190 | private static String percentCode(String str) { |
| 186 | Assert.notNull(str, "str 不能为空"); | 191 | Assert.notNull(str, "str 不能为空"); |
| 187 | - return HttpUtils.encodeUtf8(str) | 192 | + return URLEncoder.encode(str, StandardCharsets.UTF_8.name()) |
| 188 | .replace("+", "%20") // 加号 "+" 被替换为 "%20" | 193 | .replace("+", "%20") // 加号 "+" 被替换为 "%20" |
| 189 | .replace("*", "%2A") // 星号 "*" 被替换为 "%2A" | 194 | .replace("*", "%2A") // 星号 "*" 被替换为 "%2A" |
| 190 | .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" | 195 | .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java
| @@ -5,6 +5,9 @@ import lombok.Data; | @@ -5,6 +5,9 @@ import lombok.Data; | ||
| 5 | import jakarta.validation.constraints.NotEmpty; | 5 | import jakarta.validation.constraints.NotEmpty; |
| 6 | import jakarta.validation.constraints.NotNull; | 6 | import jakarta.validation.constraints.NotNull; |
| 7 | 7 | ||
| 8 | +import java.util.Collection; | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 8 | /** | 11 | /** |
| 9 | * 邮箱发送消息 | 12 | * 邮箱发送消息 |
| 10 | * | 13 | * |
| @@ -21,8 +24,16 @@ public class MailSendMessage { | @@ -21,8 +24,16 @@ public class MailSendMessage { | ||
| 21 | /** | 24 | /** |
| 22 | * 接收邮件地址 | 25 | * 接收邮件地址 |
| 23 | */ | 26 | */ |
| 24 | - @NotNull(message = "接收邮件地址不能为空") | ||
| 25 | - private String mail; | 27 | + @NotEmpty(message = "接收邮件地址不能为空") |
| 28 | + private Collection<String> toMails; | ||
| 29 | + /** | ||
| 30 | + * 抄送邮件地址 | ||
| 31 | + */ | ||
| 32 | + private Collection<String> ccMails; | ||
| 33 | + /** | ||
| 34 | + * 密送邮件地址 | ||
| 35 | + */ | ||
| 36 | + private Collection<String> bccMails; | ||
| 26 | /** | 37 | /** |
| 27 | * 邮件账号编号 | 38 | * 邮件账号编号 |
| 28 | */ | 39 | */ |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java
| @@ -7,6 +7,11 @@ import org.springframework.stereotype.Component; | @@ -7,6 +7,11 @@ import org.springframework.stereotype.Component; | ||
| 7 | 7 | ||
| 8 | import jakarta.annotation.Resource; | 8 | import jakarta.annotation.Resource; |
| 9 | 9 | ||
| 10 | +import java.util.Collection; | ||
| 11 | +import java.util.List; | ||
| 12 | + | ||
| 13 | +import static java.util.Collections.singletonList; | ||
| 14 | + | ||
| 10 | /** | 15 | /** |
| 11 | * Mail 邮件相关消息的 Producer | 16 | * Mail 邮件相关消息的 Producer |
| 12 | * | 17 | * |
| @@ -24,17 +29,22 @@ public class MailProducer { | @@ -24,17 +29,22 @@ public class MailProducer { | ||
| 24 | * 发送 {@link MailSendMessage} 消息 | 29 | * 发送 {@link MailSendMessage} 消息 |
| 25 | * | 30 | * |
| 26 | * @param sendLogId 发送日志编码 | 31 | * @param sendLogId 发送日志编码 |
| 27 | - * @param mail 接收邮件地址 | 32 | + * @param toMails 接收邮件地址 |
| 33 | + * @param ccMails 抄送邮件地址 | ||
| 34 | + * @param bccMails 密送邮件地址 | ||
| 28 | * @param accountId 邮件账号编号 | 35 | * @param accountId 邮件账号编号 |
| 29 | - * @param nickname 邮件发件人 | ||
| 30 | - * @param title 邮件标题 | ||
| 31 | - * @param content 邮件内容 | 36 | + * @param nickname 邮件发件人 |
| 37 | + * @param title 邮件标题 | ||
| 38 | + * @param content 邮件内容 | ||
| 32 | */ | 39 | */ |
| 33 | - public void sendMailSendMessage(Long sendLogId, String mail, Long accountId, | ||
| 34 | - String nickname, String title, String content) { | 40 | + public void sendMailSendMessage(Long sendLogId, |
| 41 | + Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails, | ||
| 42 | + Long accountId, String nickname, String title, String content) { | ||
| 35 | MailSendMessage message = new MailSendMessage() | 43 | MailSendMessage message = new MailSendMessage() |
| 36 | - .setLogId(sendLogId).setMail(mail).setAccountId(accountId) | ||
| 37 | - .setNickname(nickname).setTitle(title).setContent(content); | 44 | + .setLogId(sendLogId) |
| 45 | + .setToMails(toMails).setCcMails(ccMails).setBccMails(bccMails) | ||
| 46 | + .setAccountId(accountId).setNickname(nickname) | ||
| 47 | + .setTitle(title).setContent(content); | ||
| 38 | applicationContext.publishEvent(message); | 48 | applicationContext.publishEvent(message); |
| 39 | } | 49 | } |
| 40 | 50 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java
| @@ -37,6 +37,13 @@ public interface DeptService { | @@ -37,6 +37,13 @@ public interface DeptService { | ||
| 37 | void deleteDept(Long id); | 37 | void deleteDept(Long id); |
| 38 | 38 | ||
| 39 | /** | 39 | /** |
| 40 | + * 批量删除部门 | ||
| 41 | + * | ||
| 42 | + * @param ids 部门编号数组 | ||
| 43 | + */ | ||
| 44 | + void deleteDeptList(List<Long> ids); | ||
| 45 | + | ||
| 46 | + /** | ||
| 40 | * 获得部门信息 | 47 | * 获得部门信息 |
| 41 | * | 48 | * |
| 42 | * @param id 部门编号 | 49 | * @param id 部门编号 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java
| @@ -88,6 +88,21 @@ public class DeptServiceImpl implements DeptService { | @@ -88,6 +88,21 @@ public class DeptServiceImpl implements DeptService { | ||
| 88 | deptMapper.deleteById(id); | 88 | deptMapper.deleteById(id); |
| 89 | } | 89 | } |
| 90 | 90 | ||
| 91 | + @Override | ||
| 92 | + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, | ||
| 93 | + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 | ||
| 94 | + public void deleteDeptList(List<Long> ids) { | ||
| 95 | + // 校验是否有子部门 | ||
| 96 | + for (Long id : ids) { | ||
| 97 | + if (deptMapper.selectCountByParentId(id) > 0) { | ||
| 98 | + throw exception(DEPT_EXITS_CHILDREN); | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + // 批量删除部门 | ||
| 103 | + deptMapper.deleteByIds(ids); | ||
| 104 | + } | ||
| 105 | + | ||
| 91 | @VisibleForTesting | 106 | @VisibleForTesting |
| 92 | void validateDeptExists(Long id) { | 107 | void validateDeptExists(Long id) { |
| 93 | if (id == null) { | 108 | if (id == null) { |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java
| @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; | @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; | ||
| 6 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; | 6 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; |
| 7 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; | 7 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; |
| 8 | 8 | ||
| 9 | +import java.util.Collection; | ||
| 10 | +import java.util.List; | ||
| 9 | import java.util.Map; | 11 | import java.util.Map; |
| 10 | 12 | ||
| 11 | /** | 13 | /** |
| @@ -35,18 +37,21 @@ public interface MailLogService { | @@ -35,18 +37,21 @@ public interface MailLogService { | ||
| 35 | /** | 37 | /** |
| 36 | * 创建邮件日志 | 38 | * 创建邮件日志 |
| 37 | * | 39 | * |
| 38 | - * @param userId 用户编码 | ||
| 39 | - * @param userType 用户类型 | ||
| 40 | - * @param toMail 收件人邮件 | ||
| 41 | - * @param account 邮件账号信息 | ||
| 42 | - * @param template 模版信息 | 40 | + * @param userId 用户编码 |
| 41 | + * @param userType 用户类型 | ||
| 42 | + * @param toMails 收件人邮件 | ||
| 43 | + * @param ccMails 收件人邮件 | ||
| 44 | + * @param bccMails 收件人邮件 | ||
| 45 | + * @param account 邮件账号信息 | ||
| 46 | + * @param template 模版信息 | ||
| 43 | * @param templateContent 模版内容 | 47 | * @param templateContent 模版内容 |
| 44 | - * @param templateParams 模版参数 | ||
| 45 | - * @param isSend 是否发送成功 | 48 | + * @param templateParams 模版参数 |
| 49 | + * @param isSend 是否发送成功 | ||
| 46 | * @return 日志编号 | 50 | * @return 日志编号 |
| 47 | */ | 51 | */ |
| 48 | - Long createMailLog(Long userId, Integer userType, String toMail, | ||
| 49 | - MailAccountDO account, MailTemplateDO template , | 52 | + Long createMailLog(Long userId, Integer userType, |
| 53 | + Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails, | ||
| 54 | + MailAccountDO account, MailTemplateDO template, | ||
| 50 | String templateContent, Map<String, Object> templateParams, Boolean isSend); | 55 | String templateContent, Map<String, Object> templateParams, Boolean isSend); |
| 51 | 56 | ||
| 52 | /** | 57 | /** |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java
| 1 | package cn.iocoder.yudao.module.system.service.mail; | 1 | package cn.iocoder.yudao.module.system.service.mail; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.ListUtil; | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; | 5 | import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; |
| 5 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; | 6 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; |
| @@ -12,8 +13,7 @@ import org.springframework.validation.annotation.Validated; | @@ -12,8 +13,7 @@ import org.springframework.validation.annotation.Validated; | ||
| 12 | 13 | ||
| 13 | import jakarta.annotation.Resource; | 14 | import jakarta.annotation.Resource; |
| 14 | import java.time.LocalDateTime; | 15 | import java.time.LocalDateTime; |
| 15 | -import java.util.Map; | ||
| 16 | -import java.util.Objects; | 16 | +import java.util.*; |
| 17 | 17 | ||
| 18 | import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; | 18 | import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; |
| 19 | 19 | ||
| @@ -41,7 +41,8 @@ public class MailLogServiceImpl implements MailLogService { | @@ -41,7 +41,8 @@ public class MailLogServiceImpl implements MailLogService { | ||
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | @Override | 43 | @Override |
| 44 | - public Long createMailLog(Long userId, Integer userType, String toMail, | 44 | + public Long createMailLog(Long userId, Integer userType, |
| 45 | + Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails, | ||
| 45 | MailAccountDO account, MailTemplateDO template, | 46 | MailAccountDO account, MailTemplateDO template, |
| 46 | String templateContent, Map<String, Object> templateParams, Boolean isSend) { | 47 | String templateContent, Map<String, Object> templateParams, Boolean isSend) { |
| 47 | MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); | 48 | MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); |
| @@ -49,7 +50,8 @@ public class MailLogServiceImpl implements MailLogService { | @@ -49,7 +50,8 @@ public class MailLogServiceImpl implements MailLogService { | ||
| 49 | logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() | 50 | logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() |
| 50 | : MailSendStatusEnum.IGNORE.getStatus()) | 51 | : MailSendStatusEnum.IGNORE.getStatus()) |
| 51 | // 用户信息 | 52 | // 用户信息 |
| 52 | - .userId(userId).userType(userType).toMail(toMail) | 53 | + .userId(userId).userType(userType) |
| 54 | + .toMails(ListUtil.toList(toMails)).ccMails(ListUtil.toList(ccMails)).bccMails(ListUtil.toList(bccMails)) | ||
| 53 | .accountId(account.getId()).fromMail(account.getMail()) | 55 | .accountId(account.getId()).fromMail(account.getMail()) |
| 54 | // 模板相关字段 | 56 | // 模板相关字段 |
| 55 | .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) | 57 | .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java
| 1 | package cn.iocoder.yudao.module.system.service.mail; | 1 | package cn.iocoder.yudao.module.system.service.mail; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 3 | import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage; | 4 | import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage; |
| 4 | 5 | ||
| 6 | +import java.util.Collection; | ||
| 5 | import java.util.Map; | 7 | import java.util.Map; |
| 6 | 8 | ||
| 7 | /** | 9 | /** |
| @@ -15,38 +17,53 @@ public interface MailSendService { | @@ -15,38 +17,53 @@ public interface MailSendService { | ||
| 15 | /** | 17 | /** |
| 16 | * 发送单条邮件给管理后台的用户 | 18 | * 发送单条邮件给管理后台的用户 |
| 17 | * | 19 | * |
| 18 | - * @param mail 邮箱 | ||
| 19 | * @param userId 用户编码 | 20 | * @param userId 用户编码 |
| 21 | + * @param toMails 收件邮箱 | ||
| 22 | + * @param ccMails 抄送邮箱 | ||
| 23 | + * @param bccMails 密送邮箱 | ||
| 20 | * @param templateCode 邮件模版编码 | 24 | * @param templateCode 邮件模版编码 |
| 21 | * @param templateParams 邮件模版参数 | 25 | * @param templateParams 邮件模版参数 |
| 22 | * @return 发送日志编号 | 26 | * @return 发送日志编号 |
| 23 | */ | 27 | */ |
| 24 | - Long sendSingleMailToAdmin(String mail, Long userId, | ||
| 25 | - String templateCode, Map<String, Object> templateParams); | 28 | + default Long sendSingleMailToAdmin(Long userId, |
| 29 | + Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails, | ||
| 30 | + String templateCode, Map<String, Object> templateParams) { | ||
| 31 | + return sendSingleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.ADMIN.getValue(), | ||
| 32 | + templateCode, templateParams); | ||
| 33 | + } | ||
| 26 | 34 | ||
| 27 | /** | 35 | /** |
| 28 | * 发送单条邮件给用户 APP 的用户 | 36 | * 发送单条邮件给用户 APP 的用户 |
| 29 | * | 37 | * |
| 30 | - * @param mail 邮箱 | ||
| 31 | * @param userId 用户编码 | 38 | * @param userId 用户编码 |
| 39 | + * @param toMails 收件邮箱 | ||
| 40 | + * @param ccMails 抄送邮箱 | ||
| 41 | + * @param bccMails 密送邮箱 | ||
| 32 | * @param templateCode 邮件模版编码 | 42 | * @param templateCode 邮件模版编码 |
| 33 | * @param templateParams 邮件模版参数 | 43 | * @param templateParams 邮件模版参数 |
| 34 | * @return 发送日志编号 | 44 | * @return 发送日志编号 |
| 35 | */ | 45 | */ |
| 36 | - Long sendSingleMailToMember(String mail, Long userId, | ||
| 37 | - String templateCode, Map<String, Object> templateParams); | 46 | + default Long sendSingleMailToMember(Long userId, |
| 47 | + Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails, | ||
| 48 | + String templateCode, Map<String, Object> templateParams) { | ||
| 49 | + return sendSingleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.MEMBER.getValue(), | ||
| 50 | + templateCode, templateParams); | ||
| 51 | + } | ||
| 38 | 52 | ||
| 39 | /** | 53 | /** |
| 40 | - * 发送单条邮件给用户 | 54 | + * 发送单条邮件 |
| 41 | * | 55 | * |
| 42 | - * @param mail 邮箱 | ||
| 43 | - * @param userId 用户编码 | 56 | + * @param toMails 收件邮箱 |
| 57 | + * @param ccMails 抄送邮箱 | ||
| 58 | + * @param bccMails 密送邮箱 | ||
| 59 | + * @param userId 用户编号 | ||
| 44 | * @param userType 用户类型 | 60 | * @param userType 用户类型 |
| 45 | * @param templateCode 邮件模版编码 | 61 | * @param templateCode 邮件模版编码 |
| 46 | * @param templateParams 邮件模版参数 | 62 | * @param templateParams 邮件模版参数 |
| 47 | * @return 发送日志编号 | 63 | * @return 发送日志编号 |
| 48 | */ | 64 | */ |
| 49 | - Long sendSingleMail(String mail, Long userId, Integer userType, | 65 | + Long sendSingleMail(Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails, |
| 66 | + Long userId, Integer userType, | ||
| 50 | String templateCode, Map<String, Object> templateParams); | 67 | String templateCode, Map<String, Object> templateParams); |
| 51 | 68 | ||
| 52 | /** | 69 | /** |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java
| 1 | package cn.iocoder.yudao.module.system.service.mail; | 1 | package cn.iocoder.yudao.module.system.service.mail; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.core.lang.Validator; | ||
| 3 | import cn.hutool.core.util.StrUtil; | 5 | import cn.hutool.core.util.StrUtil; |
| 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 6 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | 7 | import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; |
| @@ -13,10 +15,13 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService; | @@ -13,10 +15,13 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService; | ||
| 13 | import com.google.common.annotations.VisibleForTesting; | 15 | import com.google.common.annotations.VisibleForTesting; |
| 14 | import jakarta.annotation.Resource; | 16 | import jakarta.annotation.Resource; |
| 15 | import lombok.extern.slf4j.Slf4j; | 17 | import lombok.extern.slf4j.Slf4j; |
| 16 | -import org.dromara.hutool.extra.mail.*; | 18 | +import org.dromara.hutool.extra.mail.MailAccount; |
| 19 | +import org.dromara.hutool.extra.mail.MailUtil; | ||
| 17 | import org.springframework.stereotype.Service; | 20 | import org.springframework.stereotype.Service; |
| 18 | import org.springframework.validation.annotation.Validated; | 21 | import org.springframework.validation.annotation.Validated; |
| 19 | 22 | ||
| 23 | +import java.util.Collection; | ||
| 24 | +import java.util.LinkedHashSet; | ||
| 20 | import java.util.Map; | 25 | import java.util.Map; |
| 21 | 26 | ||
| 22 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 27 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| @@ -49,56 +54,67 @@ public class MailSendServiceImpl implements MailSendService { | @@ -49,56 +54,67 @@ public class MailSendServiceImpl implements MailSendService { | ||
| 49 | private MailProducer mailProducer; | 54 | private MailProducer mailProducer; |
| 50 | 55 | ||
| 51 | @Override | 56 | @Override |
| 52 | - public Long sendSingleMailToAdmin(String mail, Long userId, | ||
| 53 | - String templateCode, Map<String, Object> templateParams) { | ||
| 54 | - // 如果 mail 为空,则加载用户编号对应的邮箱 | ||
| 55 | - if (StrUtil.isEmpty(mail)) { | ||
| 56 | - AdminUserDO user = adminUserService.getUser(userId); | ||
| 57 | - if (user != null) { | ||
| 58 | - mail = user.getEmail(); | ||
| 59 | - } | ||
| 60 | - } | ||
| 61 | - // 执行发送 | ||
| 62 | - return sendSingleMail(mail, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); | ||
| 63 | - } | ||
| 64 | - | ||
| 65 | - @Override | ||
| 66 | - public Long sendSingleMailToMember(String mail, Long userId, | ||
| 67 | - String templateCode, Map<String, Object> templateParams) { | ||
| 68 | - // 如果 mail 为空,则加载用户编号对应的邮箱 | ||
| 69 | - if (StrUtil.isEmpty(mail)) { | ||
| 70 | - mail = memberService.getMemberUserEmail(userId); | ||
| 71 | - } | ||
| 72 | - // 执行发送 | ||
| 73 | - return sendSingleMail(mail, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); | ||
| 74 | - } | ||
| 75 | - | ||
| 76 | - @Override | ||
| 77 | - public Long sendSingleMail(String mail, Long userId, Integer userType, | 57 | + public Long sendSingleMail(Collection<String> toMails, Collection<String> ccMails, Collection<String> bccMails, |
| 58 | + Long userId, Integer userType, | ||
| 78 | String templateCode, Map<String, Object> templateParams) { | 59 | String templateCode, Map<String, Object> templateParams) { |
| 79 | - // 校验邮箱模版是否合法 | 60 | + // 1.1 校验邮箱模版是否合法 |
| 80 | MailTemplateDO template = validateMailTemplate(templateCode); | 61 | MailTemplateDO template = validateMailTemplate(templateCode); |
| 81 | - // 校验邮箱账号是否合法 | 62 | + // 1.2 校验邮箱账号是否合法 |
| 82 | MailAccountDO account = validateMailAccount(template.getAccountId()); | 63 | MailAccountDO account = validateMailAccount(template.getAccountId()); |
| 83 | - | ||
| 84 | - // 校验邮箱是否存在 | ||
| 85 | - mail = validateMail(mail); | 64 | + // 1.3 校验邮件参数是否缺失 |
| 86 | validateTemplateParams(template, templateParams); | 65 | validateTemplateParams(template, templateParams); |
| 87 | 66 | ||
| 67 | + // 2. 组装邮箱 | ||
| 68 | + String userMail = getUserMail(userId, userType); | ||
| 69 | + Collection<String> toMailSet = new LinkedHashSet<>(); | ||
| 70 | + Collection<String> ccMailSet = new LinkedHashSet<>(); | ||
| 71 | + Collection<String> bccMailSet = new LinkedHashSet<>(); | ||
| 72 | + if (Validator.isEmail(userMail)) { | ||
| 73 | + toMailSet.add(userMail); | ||
| 74 | + } | ||
| 75 | + if (CollUtil.isNotEmpty(toMails)) { | ||
| 76 | + toMails.stream().filter(Validator::isEmail).forEach(toMailSet::add); | ||
| 77 | + } | ||
| 78 | + if (CollUtil.isNotEmpty(ccMails)) { | ||
| 79 | + ccMails.stream().filter(Validator::isEmail).forEach(ccMailSet::add); | ||
| 80 | + } | ||
| 81 | + if (CollUtil.isNotEmpty(bccMails)) { | ||
| 82 | + bccMails.stream().filter(Validator::isEmail).forEach(bccMailSet::add); | ||
| 83 | + } | ||
| 84 | + if (CollUtil.isEmpty(toMailSet)) { | ||
| 85 | + throw exception(MAIL_SEND_MAIL_NOT_EXISTS); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 | 88 | // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 |
| 89 | Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); | 89 | Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); |
| 90 | String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); | 90 | String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); |
| 91 | String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); | 91 | String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); |
| 92 | - Long sendLogId = mailLogService.createMailLog(userId, userType, mail, | 92 | + Long sendLogId = mailLogService.createMailLog(userId, userType, toMailSet, ccMailSet, bccMailSet, |
| 93 | account, template, content, templateParams, isSend); | 93 | account, template, content, templateParams, isSend); |
| 94 | // 发送 MQ 消息,异步执行发送短信 | 94 | // 发送 MQ 消息,异步执行发送短信 |
| 95 | if (isSend) { | 95 | if (isSend) { |
| 96 | - mailProducer.sendMailSendMessage(sendLogId, mail, account.getId(), | ||
| 97 | - template.getNickname(), title, content); | 96 | + mailProducer.sendMailSendMessage(sendLogId, toMailSet, ccMailSet, bccMailSet, |
| 97 | + account.getId(), template.getNickname(), title, content); | ||
| 98 | } | 98 | } |
| 99 | return sendLogId; | 99 | return sendLogId; |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | + private String getUserMail(Long userId, Integer userType) { | ||
| 103 | + if (userId == null || userType == null) { | ||
| 104 | + return null; | ||
| 105 | + } | ||
| 106 | + if (UserTypeEnum.ADMIN.getValue().equals(userType)) { | ||
| 107 | + AdminUserDO user = adminUserService.getUser(userId); | ||
| 108 | + if (user != null) { | ||
| 109 | + return user.getEmail(); | ||
| 110 | + } | ||
| 111 | + } | ||
| 112 | + if (UserTypeEnum.MEMBER.getValue().equals(userType)) { | ||
| 113 | + return memberService.getMemberUserEmail(userId); | ||
| 114 | + } | ||
| 115 | + return null; | ||
| 116 | + } | ||
| 117 | + | ||
| 102 | @Override | 118 | @Override |
| 103 | public void doSendMail(MailSendMessage message) { | 119 | public void doSendMail(MailSendMessage message) { |
| 104 | // 1. 创建发送账号 | 120 | // 1. 创建发送账号 |
| @@ -106,7 +122,7 @@ public class MailSendServiceImpl implements MailSendService { | @@ -106,7 +122,7 @@ public class MailSendServiceImpl implements MailSendService { | ||
| 106 | MailAccount mailAccount = buildMailAccount(account, message.getNickname()); | 122 | MailAccount mailAccount = buildMailAccount(account, message.getNickname()); |
| 107 | // 2. 发送邮件 | 123 | // 2. 发送邮件 |
| 108 | try { | 124 | try { |
| 109 | - String messageId = MailUtil.send(mailAccount, message.getMail(), | 125 | + String messageId = MailUtil.send(mailAccount, message.getToMails(), message.getCcMails(), message.getBccMails(), |
| 110 | message.getTitle(), message.getContent(), true); | 126 | message.getTitle(), message.getContent(), true); |
| 111 | // 3. 更新结果(成功) | 127 | // 3. 更新结果(成功) |
| 112 | mailLogService.updateMailSendResult(message.getLogId(), messageId, null); | 128 | mailLogService.updateMailSendResult(message.getLogId(), messageId, null); |
| @@ -146,16 +162,8 @@ public class MailSendServiceImpl implements MailSendService { | @@ -146,16 +162,8 @@ public class MailSendServiceImpl implements MailSendService { | ||
| 146 | return account; | 162 | return account; |
| 147 | } | 163 | } |
| 148 | 164 | ||
| 149 | - @VisibleForTesting | ||
| 150 | - String validateMail(String mail) { | ||
| 151 | - if (StrUtil.isEmpty(mail)) { | ||
| 152 | - throw exception(MAIL_SEND_MAIL_NOT_EXISTS); | ||
| 153 | - } | ||
| 154 | - return mail; | ||
| 155 | - } | ||
| 156 | - | ||
| 157 | /** | 165 | /** |
| 158 | - * 校验邮件参数是否确实 | 166 | + * 校验邮件参数是否缺失 |
| 159 | * | 167 | * |
| 160 | * @param template 邮箱模板 | 168 | * @param template 邮箱模板 |
| 161 | * @param templateParams 参数列表 | 169 | * @param templateParams 参数列表 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java
| @@ -255,9 +255,6 @@ public class MenuServiceImpl implements MenuService { | @@ -255,9 +255,6 @@ public class MenuServiceImpl implements MenuService { | ||
| 255 | return; | 255 | return; |
| 256 | } | 256 | } |
| 257 | // 如果 id 为空,说明不用比较是否为相同 id 的菜单 | 257 | // 如果 id 为空,说明不用比较是否为相同 id 的菜单 |
| 258 | - if (id == null) { | ||
| 259 | - throw exception(MENU_NAME_DUPLICATE); | ||
| 260 | - } | ||
| 261 | if (!menu.getId().equals(id)) { | 258 | if (!menu.getId().equals(id)) { |
| 262 | throw exception(MENU_NAME_DUPLICATE); | 259 | throw exception(MENU_NAME_DUPLICATE); |
| 263 | } | 260 | } |
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java
| @@ -105,12 +105,40 @@ public class DeptServiceImplTest extends BaseDbUnitTest { | @@ -105,12 +105,40 @@ public class DeptServiceImplTest extends BaseDbUnitTest { | ||
| 105 | } | 105 | } |
| 106 | 106 | ||
| 107 | @Test | 107 | @Test |
| 108 | - public void testValidateDeptExists_notFound() { | 108 | + public void testDeleteDeptList_success() { |
| 109 | + // mock 数据 | ||
| 110 | + DeptDO deptDO1 = randomPojo(DeptDO.class); | ||
| 111 | + deptMapper.insert(deptDO1); | ||
| 112 | + DeptDO deptDO2 = randomPojo(DeptDO.class); | ||
| 113 | + deptMapper.insert(deptDO2); | ||
| 109 | // 准备参数 | 114 | // 准备参数 |
| 110 | - Long id = randomLongId(); | 115 | + List<Long> ids = Arrays.asList(deptDO1.getId(), deptDO2.getId()); |
| 116 | + | ||
| 117 | + // 调用 | ||
| 118 | + deptService.deleteDeptList(ids); | ||
| 119 | + // 校验数据不存在了 | ||
| 120 | + assertNull(deptMapper.selectById(deptDO1.getId())); | ||
| 121 | + assertNull(deptMapper.selectById(deptDO2.getId())); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + @Test | ||
| 125 | + public void testDeleteDeptList_exitsChildren() { | ||
| 126 | + // mock 数据 | ||
| 127 | + DeptDO parentDept = randomPojo(DeptDO.class); | ||
| 128 | + deptMapper.insert(parentDept); | ||
| 129 | + DeptDO childrenDeptDO = randomPojo(DeptDO.class, o -> { | ||
| 130 | + o.setParentId(parentDept.getId()); | ||
| 131 | + o.setStatus(randomCommonStatus()); | ||
| 132 | + }); | ||
| 133 | + deptMapper.insert(childrenDeptDO); | ||
| 134 | + DeptDO anotherDept = randomPojo(DeptDO.class); | ||
| 135 | + deptMapper.insert(anotherDept); | ||
| 136 | + | ||
| 137 | + // 准备参数(包含有子部门的 parentDept) | ||
| 138 | + List<Long> ids = Arrays.asList(parentDept.getId(), anotherDept.getId()); | ||
| 111 | 139 | ||
| 112 | // 调用, 并断言异常 | 140 | // 调用, 并断言异常 |
| 113 | - assertServiceException(() -> deptService.validateDeptExists(id), DEPT_NOT_FOUND); | 141 | + assertServiceException(() -> deptService.deleteDeptList(ids), DEPT_EXITS_CHILDREN); |
| 114 | } | 142 | } |
| 115 | 143 | ||
| 116 | @Test | 144 | @Test |
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java
| @@ -10,10 +10,12 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; | @@ -10,10 +10,12 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; | ||
| 10 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; | 10 | import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; |
| 11 | import cn.iocoder.yudao.module.system.dal.mysql.mail.MailLogMapper; | 11 | import cn.iocoder.yudao.module.system.dal.mysql.mail.MailLogMapper; |
| 12 | import cn.iocoder.yudao.module.system.enums.mail.MailSendStatusEnum; | 12 | import cn.iocoder.yudao.module.system.enums.mail.MailSendStatusEnum; |
| 13 | +import org.assertj.core.util.Lists; | ||
| 13 | import org.junit.jupiter.api.Test; | 14 | import org.junit.jupiter.api.Test; |
| 14 | import org.springframework.context.annotation.Import; | 15 | import org.springframework.context.annotation.Import; |
| 15 | 16 | ||
| 16 | import jakarta.annotation.Resource; | 17 | import jakarta.annotation.Resource; |
| 18 | +import java.util.Collection; | ||
| 17 | import java.util.Map; | 19 | import java.util.Map; |
| 18 | 20 | ||
| 19 | import static cn.hutool.core.util.RandomUtil.randomEle; | 21 | import static cn.hutool.core.util.RandomUtil.randomEle; |
| @@ -43,7 +45,9 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | @@ -43,7 +45,9 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | ||
| 43 | // 准备参数 | 45 | // 准备参数 |
| 44 | Long userId = randomLongId(); | 46 | Long userId = randomLongId(); |
| 45 | Integer userType = randomEle(UserTypeEnum.values()).getValue(); | 47 | Integer userType = randomEle(UserTypeEnum.values()).getValue(); |
| 46 | - String toMail = randomEmail(); | 48 | + Collection<String> toMails = Lists.newArrayList(randomEmail(), randomEmail()); |
| 49 | + Collection<String> ccMails = Lists.newArrayList(randomEmail()); | ||
| 50 | + Collection<String> bccMails = Lists.newArrayList(randomEmail()); | ||
| 47 | MailAccountDO account = randomPojo(MailAccountDO.class); | 51 | MailAccountDO account = randomPojo(MailAccountDO.class); |
| 48 | MailTemplateDO template = randomPojo(MailTemplateDO.class); | 52 | MailTemplateDO template = randomPojo(MailTemplateDO.class); |
| 49 | String templateContent = randomString(); | 53 | String templateContent = randomString(); |
| @@ -52,14 +56,20 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | @@ -52,14 +56,20 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | ||
| 52 | // mock 方法 | 56 | // mock 方法 |
| 53 | 57 | ||
| 54 | // 调用 | 58 | // 调用 |
| 55 | - Long logId = mailLogService.createMailLog(userId, userType, toMail, account, template, templateContent, templateParams, isSend); | 59 | + Long logId = mailLogService.createMailLog(userId, userType, toMails, ccMails, bccMails, |
| 60 | + account, template, templateContent, templateParams, isSend); | ||
| 56 | // 断言 | 61 | // 断言 |
| 57 | MailLogDO log = mailLogMapper.selectById(logId); | 62 | MailLogDO log = mailLogMapper.selectById(logId); |
| 58 | assertNotNull(log); | 63 | assertNotNull(log); |
| 59 | assertEquals(MailSendStatusEnum.INIT.getStatus(), log.getSendStatus()); | 64 | assertEquals(MailSendStatusEnum.INIT.getStatus(), log.getSendStatus()); |
| 60 | assertEquals(userId, log.getUserId()); | 65 | assertEquals(userId, log.getUserId()); |
| 61 | assertEquals(userType, log.getUserType()); | 66 | assertEquals(userType, log.getUserType()); |
| 62 | - assertEquals(toMail, log.getToMail()); | 67 | + assertEquals(toMails.size(), log.getToMails().size()); |
| 68 | + assertTrue(log.getToMails().containsAll(toMails)); | ||
| 69 | + assertEquals(ccMails.size(), log.getCcMails().size()); | ||
| 70 | + assertTrue(log.getCcMails().containsAll(ccMails)); | ||
| 71 | + assertEquals(bccMails.size(), log.getBccMails().size()); | ||
| 72 | + assertTrue(log.getBccMails().containsAll(bccMails)); | ||
| 63 | assertEquals(account.getId(), log.getAccountId()); | 73 | assertEquals(account.getId(), log.getAccountId()); |
| 64 | assertEquals(account.getMail(), log.getFromMail()); | 74 | assertEquals(account.getMail(), log.getFromMail()); |
| 65 | assertEquals(template.getId(), log.getTemplateId()); | 75 | assertEquals(template.getId(), log.getTemplateId()); |
| @@ -136,7 +146,9 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | @@ -136,7 +146,9 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | ||
| 136 | MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> { // 等会查询到 | 146 | MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> { // 等会查询到 |
| 137 | o.setUserId(1L); | 147 | o.setUserId(1L); |
| 138 | o.setUserType(UserTypeEnum.ADMIN.getValue()); | 148 | o.setUserType(UserTypeEnum.ADMIN.getValue()); |
| 139 | - o.setToMail("768@qq.com"); | 149 | + o.setToMails(Lists.newArrayList("768@qq.com")); |
| 150 | + o.setCcMails(Lists.newArrayList()); | ||
| 151 | + o.setBccMails(Lists.newArrayList()); | ||
| 140 | o.setAccountId(10L); | 152 | o.setAccountId(10L); |
| 141 | o.setTemplateId(100L); | 153 | o.setTemplateId(100L); |
| 142 | o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); | 154 | o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); |
| @@ -148,8 +160,8 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | @@ -148,8 +160,8 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | ||
| 148 | mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L))); | 160 | mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L))); |
| 149 | // 测试 userType 不匹配 | 161 | // 测试 userType 不匹配 |
| 150 | mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); | 162 | mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); |
| 151 | - // 测试 toMail 不匹配 | ||
| 152 | - mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMail("788@.qq.com"))); | 163 | + // 测试 toMails 不匹配(特殊:find_in_set 无法单测) |
| 164 | +// mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMails(Lists.newArrayList("788@qq.com")))); | ||
| 153 | // 测试 accountId 不匹配 | 165 | // 测试 accountId 不匹配 |
| 154 | mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L))); | 166 | mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L))); |
| 155 | // 测试 templateId 不匹配 | 167 | // 测试 templateId 不匹配 |
| @@ -162,7 +174,7 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | @@ -162,7 +174,7 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { | ||
| 162 | MailLogPageReqVO reqVO = new MailLogPageReqVO(); | 174 | MailLogPageReqVO reqVO = new MailLogPageReqVO(); |
| 163 | reqVO.setUserId(1L); | 175 | reqVO.setUserId(1L); |
| 164 | reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); | 176 | reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); |
| 165 | - reqVO.setToMail("768"); | 177 | +// reqVO.setToMail("768@qq.com"); |
| 166 | reqVO.setAccountId(10L); | 178 | reqVO.setAccountId(10L); |
| 167 | reqVO.setTemplateId(100L); | 179 | reqVO.setTemplateId(100L); |
| 168 | reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus()); | 180 | reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus()); |
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java
| @@ -20,6 +20,7 @@ import org.mockito.InjectMocks; | @@ -20,6 +20,7 @@ import org.mockito.InjectMocks; | ||
| 20 | import org.mockito.Mock; | 20 | import org.mockito.Mock; |
| 21 | import org.mockito.MockedStatic; | 21 | import org.mockito.MockedStatic; |
| 22 | 22 | ||
| 23 | +import java.util.Collection; | ||
| 23 | import java.util.HashMap; | 24 | import java.util.HashMap; |
| 24 | import java.util.Map; | 25 | import java.util.Map; |
| 25 | 26 | ||
| @@ -65,14 +66,18 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -65,14 +66,18 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 65 | } | 66 | } |
| 66 | 67 | ||
| 67 | @Test | 68 | @Test |
| 68 | - public void testSendSingleMailToAdmin() { | 69 | + public void testSendSingleMail_success() { |
| 69 | // 准备参数 | 70 | // 准备参数 |
| 70 | Long userId = randomLongId(); | 71 | Long userId = randomLongId(); |
| 71 | String templateCode = RandomUtils.randomString(); | 72 | String templateCode = RandomUtils.randomString(); |
| 72 | Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") | 73 | Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") |
| 73 | .put("op", "login").build(); | 74 | .put("op", "login").build(); |
| 75 | + Collection<String> toMails = Lists.newArrayList("admin@test.com"); | ||
| 76 | + Collection<String> ccMails = Lists.newArrayList("cc@test.com"); | ||
| 77 | + Collection<String> bccMails = Lists.newArrayList("bcc@test.com"); | ||
| 78 | + | ||
| 74 | // mock adminUserService 的方法 | 79 | // mock adminUserService 的方法 |
| 75 | - AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300")); | 80 | + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setEmail("admin@example.com")); |
| 76 | when(adminUserService.getUser(eq(userId))).thenReturn(user); | 81 | when(adminUserService.getUser(eq(userId))).thenReturn(user); |
| 77 | 82 | ||
| 78 | // mock MailTemplateService 的方法 | 83 | // mock MailTemplateService 的方法 |
| @@ -93,61 +98,27 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -93,61 +98,27 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 93 | when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); | 98 | when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); |
| 94 | // mock MailLogService 的方法 | 99 | // mock MailLogService 的方法 |
| 95 | Long mailLogId = randomLongId(); | 100 | Long mailLogId = randomLongId(); |
| 96 | - when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(user.getEmail()), | 101 | + when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), |
| 102 | + argThat(toMailSet -> toMailSet.contains(user.getEmail()) && toMailSet.contains("admin@test.com")), | ||
| 103 | + argThat(ccMailSet -> ccMailSet.contains("cc@test.com")), | ||
| 104 | + argThat(bccMailSet -> bccMailSet.contains("bcc@test.com")), | ||
| 97 | eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); | 105 | eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); |
| 98 | 106 | ||
| 99 | // 调用 | 107 | // 调用 |
| 100 | - Long resultMailLogId = mailSendService.sendSingleMailToAdmin(null, userId, templateCode, templateParams); | 108 | + Long resultMailLogId = mailSendService.sendSingleMail(toMails, ccMails, bccMails, userId, |
| 109 | + UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); | ||
| 101 | // 断言 | 110 | // 断言 |
| 102 | assertEquals(mailLogId, resultMailLogId); | 111 | assertEquals(mailLogId, resultMailLogId); |
| 103 | // 断言调用 | 112 | // 断言调用 |
| 104 | - verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(user.getEmail()), | ||
| 105 | - eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); | ||
| 106 | - } | ||
| 107 | - | ||
| 108 | - @Test | ||
| 109 | - public void testSendSingleMailToMember() { | ||
| 110 | - // 准备参数 | ||
| 111 | - Long userId = randomLongId(); | ||
| 112 | - String templateCode = RandomUtils.randomString(); | ||
| 113 | - Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") | ||
| 114 | - .put("op", "login").build(); | ||
| 115 | - // mock memberService 的方法 | ||
| 116 | - String mail = randomEmail(); | ||
| 117 | - when(memberService.getMemberUserEmail(eq(userId))).thenReturn(mail); | ||
| 118 | - | ||
| 119 | - // mock MailTemplateService 的方法 | ||
| 120 | - MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { | ||
| 121 | - o.setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 122 | - o.setContent("验证码为{code}, 操作为{op}"); | ||
| 123 | - o.setParams(Lists.newArrayList("code", "op")); | ||
| 124 | - }); | ||
| 125 | - when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); | ||
| 126 | - String title = RandomUtils.randomString(); | ||
| 127 | - when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) | ||
| 128 | - .thenReturn(title); | ||
| 129 | - String content = RandomUtils.randomString(); | ||
| 130 | - when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) | ||
| 131 | - .thenReturn(content); | ||
| 132 | - // mock MailAccountService 的方法 | ||
| 133 | - MailAccountDO account = randomPojo(MailAccountDO.class); | ||
| 134 | - when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); | ||
| 135 | - // mock MailLogService 的方法 | ||
| 136 | - Long mailLogId = randomLongId(); | ||
| 137 | - when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(mail), | ||
| 138 | - eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); | ||
| 139 | - | ||
| 140 | - // 调用 | ||
| 141 | - Long resultMailLogId = mailSendService.sendSingleMailToMember(null, userId, templateCode, templateParams); | ||
| 142 | - // 断言 | ||
| 143 | - assertEquals(mailLogId, resultMailLogId); | ||
| 144 | - // 断言调用 | ||
| 145 | - verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), | 113 | + verify(mailProducer).sendMailSendMessage(eq(mailLogId), |
| 114 | + argThat(toMailSet -> toMailSet.contains(user.getEmail()) && toMailSet.contains("admin@test.com")), | ||
| 115 | + argThat(ccMailSet -> ccMailSet.contains("cc@test.com")), | ||
| 116 | + argThat(bccMailSet -> bccMailSet.contains("bcc@test.com")), | ||
| 146 | eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); | 117 | eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); |
| 147 | } | 118 | } |
| 148 | 119 | ||
| 149 | /** | 120 | /** |
| 150 | - * 发送成功,当短信模板开启时 | 121 | + * 发送成功,当邮件模板开启时 |
| 151 | */ | 122 | */ |
| 152 | @Test | 123 | @Test |
| 153 | public void testSendSingleMail_successWhenMailTemplateEnable() { | 124 | public void testSendSingleMail_successWhenMailTemplateEnable() { |
| @@ -158,6 +129,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -158,6 +129,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 158 | String templateCode = RandomUtils.randomString(); | 129 | String templateCode = RandomUtils.randomString(); |
| 159 | Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") | 130 | Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") |
| 160 | .put("op", "login").build(); | 131 | .put("op", "login").build(); |
| 132 | + Collection<String> toMails = Lists.newArrayList(mail); | ||
| 133 | + | ||
| 161 | // mock MailTemplateService 的方法 | 134 | // mock MailTemplateService 的方法 |
| 162 | MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { | 135 | MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { |
| 163 | o.setStatus(CommonStatusEnum.ENABLE.getStatus()); | 136 | o.setStatus(CommonStatusEnum.ENABLE.getStatus()); |
| @@ -176,23 +149,29 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -176,23 +149,29 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 176 | when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); | 149 | when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); |
| 177 | // mock MailLogService 的方法 | 150 | // mock MailLogService 的方法 |
| 178 | Long mailLogId = randomLongId(); | 151 | Long mailLogId = randomLongId(); |
| 179 | - when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), | 152 | + when(mailLogService.createMailLog(eq(userId), eq(userType), |
| 153 | + argThat(toMailSet -> toMailSet.contains(mail)), | ||
| 154 | + argThat(Collection::isEmpty), | ||
| 155 | + argThat(Collection::isEmpty), | ||
| 180 | eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); | 156 | eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); |
| 181 | 157 | ||
| 182 | // 调用 | 158 | // 调用 |
| 183 | - Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); | 159 | + Long resultMailLogId = mailSendService.sendSingleMail(toMails, null, null, userId, userType, templateCode, templateParams); |
| 184 | // 断言 | 160 | // 断言 |
| 185 | assertEquals(mailLogId, resultMailLogId); | 161 | assertEquals(mailLogId, resultMailLogId); |
| 186 | // 断言调用 | 162 | // 断言调用 |
| 187 | - verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), | 163 | + verify(mailProducer).sendMailSendMessage(eq(mailLogId), |
| 164 | + argThat(toMailSet -> toMailSet.contains(mail)), | ||
| 165 | + argThat(Collection::isEmpty), | ||
| 166 | + argThat(Collection::isEmpty), | ||
| 188 | eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); | 167 | eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); |
| 189 | } | 168 | } |
| 190 | 169 | ||
| 191 | /** | 170 | /** |
| 192 | - * 发送成功,当短信模板关闭时 | 171 | + * 发送成功,当邮件模板关闭时 |
| 193 | */ | 172 | */ |
| 194 | @Test | 173 | @Test |
| 195 | - public void testSendSingleMail_successWhenSmsTemplateDisable() { | 174 | + public void testSendSingleMail_successWhenMailTemplateDisable() { |
| 196 | // 准备参数 | 175 | // 准备参数 |
| 197 | String mail = randomEmail(); | 176 | String mail = randomEmail(); |
| 198 | Long userId = randomLongId(); | 177 | Long userId = randomLongId(); |
| @@ -200,6 +179,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -200,6 +179,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 200 | String templateCode = RandomUtils.randomString(); | 179 | String templateCode = RandomUtils.randomString(); |
| 201 | Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") | 180 | Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") |
| 202 | .put("op", "login").build(); | 181 | .put("op", "login").build(); |
| 182 | + Collection<String> toMails = Lists.newArrayList(mail); | ||
| 183 | + | ||
| 203 | // mock MailTemplateService 的方法 | 184 | // mock MailTemplateService 的方法 |
| 204 | MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { | 185 | MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { |
| 205 | o.setStatus(CommonStatusEnum.DISABLE.getStatus()); | 186 | o.setStatus(CommonStatusEnum.DISABLE.getStatus()); |
| @@ -218,15 +199,18 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -218,15 +199,18 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 218 | when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); | 199 | when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); |
| 219 | // mock MailLogService 的方法 | 200 | // mock MailLogService 的方法 |
| 220 | Long mailLogId = randomLongId(); | 201 | Long mailLogId = randomLongId(); |
| 221 | - when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), | 202 | + when(mailLogService.createMailLog(eq(userId), eq(userType), |
| 203 | + argThat(toMailSet -> toMailSet.contains(mail)), | ||
| 204 | + argThat(Collection::isEmpty), | ||
| 205 | + argThat(Collection::isEmpty), | ||
| 222 | eq(account), eq(template), eq(content), eq(templateParams), eq(false))).thenReturn(mailLogId); | 206 | eq(account), eq(template), eq(content), eq(templateParams), eq(false))).thenReturn(mailLogId); |
| 223 | 207 | ||
| 224 | // 调用 | 208 | // 调用 |
| 225 | - Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); | 209 | + Long resultMailLogId = mailSendService.sendSingleMail(toMails, null, null, userId, userType, templateCode, templateParams); |
| 226 | // 断言 | 210 | // 断言 |
| 227 | assertEquals(mailLogId, resultMailLogId); | 211 | assertEquals(mailLogId, resultMailLogId); |
| 228 | // 断言调用 | 212 | // 断言调用 |
| 229 | - verify(mailProducer, times(0)).sendMailSendMessage(anyLong(), anyString(), | 213 | + verify(mailProducer, times(0)).sendMailSendMessage(anyLong(), any(), any(), any(), |
| 230 | anyLong(), anyString(), anyString(), anyString()); | 214 | anyLong(), anyString(), anyString(), anyString()); |
| 231 | } | 215 | } |
| 232 | 216 | ||
| @@ -255,12 +239,29 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -255,12 +239,29 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 255 | } | 239 | } |
| 256 | 240 | ||
| 257 | @Test | 241 | @Test |
| 258 | - public void testValidateMail_notExists() { | 242 | + public void testSendSingleMail_noValidEmail() { |
| 259 | // 准备参数 | 243 | // 准备参数 |
| 260 | - // mock 方法 | 244 | + Long userId = randomLongId(); |
| 245 | + String templateCode = RandomUtils.randomString(); | ||
| 246 | + Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234") | ||
| 247 | + .put("op", "login").build(); | ||
| 248 | + Collection<String> toMails = Lists.newArrayList("invalid-email"); // 非法邮箱 | ||
| 249 | + | ||
| 250 | + // mock MailTemplateService 的方法 | ||
| 251 | + MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { | ||
| 252 | + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 253 | + o.setContent("验证码为{code}, 操作为{op}"); | ||
| 254 | + o.setParams(Lists.newArrayList("code", "op")); | ||
| 255 | + }); | ||
| 256 | + when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); | ||
| 257 | + | ||
| 258 | + // mock MailAccountService 的方法 | ||
| 259 | + MailAccountDO account = randomPojo(MailAccountDO.class); | ||
| 260 | + when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); | ||
| 261 | 261 | ||
| 262 | // 调用,并断言异常 | 262 | // 调用,并断言异常 |
| 263 | - assertServiceException(() -> mailSendService.validateMail(null), | 263 | + assertServiceException(() -> mailSendService.sendSingleMail(toMails, null, null, userId, |
| 264 | + UserTypeEnum.ADMIN.getValue(), templateCode, templateParams), | ||
| 264 | MAIL_SEND_MAIL_NOT_EXISTS); | 265 | MAIL_SEND_MAIL_NOT_EXISTS); |
| 265 | } | 266 | } |
| 266 | 267 | ||
| @@ -286,7 +287,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -286,7 +287,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 286 | assertEquals(account.getPort(), mailAccount.getPort()); | 287 | assertEquals(account.getPort(), mailAccount.getPort()); |
| 287 | assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); | 288 | assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); |
| 288 | return true; | 289 | return true; |
| 289 | - }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))) | 290 | + }), eq(message.getToMails()), eq(message.getCcMails()), eq(message.getBccMails()), |
| 291 | + eq(message.getTitle()), eq(message.getContent()), eq(true))) | ||
| 290 | .thenReturn(messageId); | 292 | .thenReturn(messageId); |
| 291 | 293 | ||
| 292 | // 调用 | 294 | // 调用 |
| @@ -317,7 +319,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | @@ -317,7 +319,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { | ||
| 317 | assertEquals(account.getPort(), mailAccount.getPort()); | 319 | assertEquals(account.getPort(), mailAccount.getPort()); |
| 318 | assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); | 320 | assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); |
| 319 | return true; | 321 | return true; |
| 320 | - }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))).thenThrow(e); | 322 | + }), eq(message.getToMails()), eq(message.getCcMails()), eq(message.getBccMails()), |
| 323 | + eq(message.getTitle()), eq(message.getContent()), eq(true))).thenThrow(e); | ||
| 321 | 324 | ||
| 322 | // 调用 | 325 | // 调用 |
| 323 | mailSendService.doSendMail(message); | 326 | mailSendService.doSendMail(message); |
yudao-module-system/src/test/resources/sql/create_tables.sql
| @@ -553,7 +553,9 @@ CREATE TABLE IF NOT EXISTS "system_mail_log" ( | @@ -553,7 +553,9 @@ CREATE TABLE IF NOT EXISTS "system_mail_log" ( | ||
| 553 | "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, | 553 | "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, |
| 554 | "user_id" bigint, | 554 | "user_id" bigint, |
| 555 | "user_type" varchar, | 555 | "user_type" varchar, |
| 556 | - "to_mail" varchar NOT NULL, | 556 | + "to_mails" varchar NOT NULL, |
| 557 | + "cc_mails" varchar, | ||
| 558 | + "bcc_mails" varchar, | ||
| 557 | "account_id" bigint NOT NULL, | 559 | "account_id" bigint NOT NULL, |
| 558 | "from_mail" varchar NOT NULL, | 560 | "from_mail" varchar NOT NULL, |
| 559 | "template_id" bigint NOT NULL, | 561 | "template_id" bigint NOT NULL, |
yudao-server/src/main/resources/application.yaml
| @@ -245,6 +245,13 @@ yudao: | @@ -245,6 +245,13 @@ yudao: | ||
| 245 | security: | 245 | security: |
| 246 | permit-all_urls: | 246 | permit-all_urls: |
| 247 | - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 | 247 | - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 |
| 248 | + api-encrypt: | ||
| 249 | + enable: true # 是否开启 API 加密 | ||
| 250 | + algorithm: AES # 加密算法,支持 AES、RSA 等 | ||
| 251 | + request-key: 52549111389893486934626385991395 # 【AES 案例】请求加密的秘钥,,必须 16、24、32 位 | ||
| 252 | + response-key: 96103715984234343991809655248883 # 【AES 案例】响应加密的秘钥,AES 案例,必须 16、24、32 位 | ||
| 253 | +# request-key: MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKWzasimcZ1icsWDPVdTXcZs1DkOWjI+m9bTQU8aOqflnomkr6QO1WWeSHBHzuJGsTlV/ZY2pFfq/NstKC94hBjx7yioYJvzb2bKN/Uy4j5nM3iCF//u0RiFkkY8j0Bt/EWoFTOb6RHf8cHIAjbYYtP3pYzbpCIwryfe0g//KIuzAgMBAAECgYADDjZrYcpZjR2xr7RbXmGtzYbyUGXwZEAqa3XaWBD51J2iSyOkAlQEDjGmxGQ3vvb4qDHHadWI+3/TKNeDXJUO+xTVJrnismK5BsHyC6dfxlIK/5BAuknryTca/3UoA1yomS9ZlF3Q0wcecaDoEnSmZEaTrp9T3itPAz4KnGjv5QJBAN5mNcfu6iJ5ktNvEdzqcxkKwbXb9Nq1SLnmTvt+d5TPX7eQ9fCwtOfVu5iBLhhZzb5PJ7pSN3Zt6rl5/jPOGv0CQQC+vETX9oe1wbxZSv6/RBGy0Xow6GndbJwvd89PcAJ2h+OJXWtg/rRHB3t9EQm7iis0XbZTapj19E4U6l8EibhvAkEA1CvYpRwmHKu1SqdM+GBnW/2qHlBwwXJvpoK02TOm674HR/4w0+YRQJfkd7LOAgcyxJuJgDTNmtt0MmzS+iNoFQJAMVSUIZ77XoDq69U/qcw7H5qaFcgmiUQr6QL9tTftCyb+LGri+MUnby96OtCLSdvkbLjIDS8GvKYhA7vSM2RDNQJBAKGyVVnFFIrbK3yuwW71yvxQEGoGxlgvZSezZ4vGgqTxrr9HvAtvWLwR6rpe6ybR/x8uUtoW7NRBWgpiIFwjvY4= # 【RSA 案例】请求解密的私钥 | ||
| 254 | +# response-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDh/CHyBcS/zEfVyINVA7+c9Xxl0CPdxPMK1OIjxaLy/7BLfbkoEpI8onQtjuzfpuxCraDem9bu3BMF0pMH95HytI3Vi0kGjaV+WLIalwgc2w37oA2sbsmKzQOP7SDLO5s2QJNAD7kVwd+Q5rqaLu2MO0xVv+0IUJhn83hClC0L5wIDAQAB # 【RSA 案例】响应加密的公钥 | ||
| 248 | websocket: | 255 | websocket: |
| 249 | enable: true # websocket的开关 | 256 | enable: true # websocket的开关 |
| 250 | path: /infra/ws # 路径 | 257 | path: /infra/ws # 路径 |