From a1164f24e0434fee7f72f13b623041b7866e45b4 Mon Sep 17 00:00:00 2001 From: jinmh716 Date: Fri, 24 Jan 2025 11:19:33 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91=E9=82=AE=E4=BB=B6=E5=8F=91=E9=80=81=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=8A=84=E9=80=81=E5=92=8C=E5=AF=86=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/ruoyi-vue-pro.sql | 6 +- .../admin/mail/MailTemplateController.java | 2 +- .../admin/mail/vo/log/MailLogRespVO.java | 11 +-- .../vo/template/MailTemplateSendReqVO.java | 11 ++- .../system/dal/dataobject/mail/MailLogDO.java | 9 ++- .../mq/message/mail/MailSendMessage.java | 14 +++- .../system/mq/producer/mail/MailProducer.java | 35 +++++++-- .../system/service/mail/MailLogService.java | 20 ++++++ .../service/mail/MailLogServiceImpl.java | 27 +++++++ .../system/service/mail/MailSendService.java | 44 ++++++++++++ .../service/mail/MailSendServiceImpl.java | 71 ++++++++++++++++++- 11 files changed, 230 insertions(+), 20 deletions(-) diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 0c810bef85..0771be45af 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -1091,14 +1091,16 @@ CREATE TABLE `system_mail_log` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', `user_id` bigint NULL DEFAULT NULL COMMENT '用户编号', `user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型', - `to_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址', + `to_mail` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址', + `cc_mail` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '抄送邮箱地址', + `bcc_mail` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密送邮箱地址', `account_id` bigint NOT NULL COMMENT '邮箱账号编号', `from_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送邮箱地址', `template_id` bigint NOT NULL COMMENT '模板编号', `template_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', `template_nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模版发送人名称', `template_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件标题', - `template_content` varchar(10240) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件内容', + `template_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件内容', `template_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件参数', `send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态', `send_time` datetime NULL DEFAULT NULL COMMENT '发送时间', diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java index 22e2658ef3..57dcc41e42 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java @@ -82,7 +82,7 @@ public class MailTemplateController { @Operation(summary = "发送短信") @PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')") public CommonResult sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) { - return success(mailSendService.sendSingleMailToAdmin(sendReqVO.getMail(), getLoginUserId(), + return success(mailSendService.sendMultipleMailToAdmin(sendReqVO.getToMails(), sendReqVO.getCcMails(), sendReqVO.getBccMails(), getLoginUserId(), sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java index 49edfffefb..b53488510c 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java @@ -2,13 +2,10 @@ package cn.iocoder.yudao.module.system.controller.admin.mail.vo.log; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import java.util.Map; -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; - @Schema(description = "管理后台 - 邮件日志 Response VO") @Data public class MailLogRespVO { @@ -22,9 +19,15 @@ public class MailLogRespVO { @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") private Byte userType; - @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "76854@qq.com") + @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user1@example.com, user2@example.com") private String toMail; + @Schema(description = "抄送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user3@example.com, user4@example.com") + private String ccMail; + + @Schema(description = "密送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user5@example.com, user6@example.com") + private String bccMail; + @Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107") private Long accountId; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java index b76b7ffccd..f125d77e9a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java @@ -5,15 +5,22 @@ import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.util.List; import java.util.Map; @Schema(description = "管理后台 - 邮件发送 Req VO") @Data public class MailTemplateSendReqVO { - @Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "7685413@qq.com") + @Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user1@example.com, user2@example.com]") @NotEmpty(message = "接收邮箱不能为空") - private String mail; + private List toMails; + + @Schema(description = "抄送邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user3@example.com, user4@example.com]") + private List ccMails; + + @Schema(description = "密送邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "[user5@example.com, user6@example.com]") + private List bccMails; @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") @NotNull(message = "模板编码不能为空") diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java index 76e1e2ec5d..988273ec59 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java @@ -49,7 +49,14 @@ public class MailLogDO extends BaseDO implements Serializable { * 接收邮箱地址 */ private String toMail; - + /** + * 接收邮箱地址 + */ + private String ccMail; + /** + * 接收邮箱地址 + */ + private String bccMail; /** * 邮箱账号编号 * diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java index 8d5af7c4cf..06c4a5102b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java @@ -5,6 +5,8 @@ import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.util.List; + /** * 邮箱发送消息 * @@ -21,8 +23,16 @@ public class MailSendMessage { /** * 接收邮件地址 */ - @NotNull(message = "接收邮件地址不能为空") - private String mail; + @NotEmpty(message = "接收邮件地址不能为空") + private List toMails; + /** + * 抄送邮件地址 + */ + private List ccMails; + /** + * 密送邮件地址 + */ + private List bccMails; /** * 邮件账号编号 */ diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java index 5a44218bb2..8a485c7fe0 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java @@ -7,6 +7,10 @@ import org.springframework.stereotype.Component; import jakarta.annotation.Resource; +import java.util.List; + +import static java.util.Collections.singletonList; + /** * Mail 邮件相关消息的 Producer * @@ -24,17 +28,36 @@ public class MailProducer { * 发送 {@link MailSendMessage} 消息 * * @param sendLogId 发送日志编码 - * @param mail 接收邮件地址 + * @param mail 接收邮件地址 * @param accountId 邮件账号编号 - * @param nickname 邮件发件人 - * @param title 邮件标题 - * @param content 邮件内容 + * @param nickname 邮件发件人 + * @param title 邮件标题 + * @param content 邮件内容 */ public void sendMailSendMessage(Long sendLogId, String mail, Long accountId, String nickname, String title, String content) { + sendMailSendMessage(sendLogId, singletonList(mail), null, null, accountId, nickname, title, content); + } + + /** + * 发送 {@link MailSendMessage} 消息 + * + * @param sendLogId 发送日志编码 + * @param toMails 接收邮件地址 + * @param ccMails 抄送邮件地址 + * @param bccMails 密送邮件地址 + * @param accountId 邮件账号编号 + * @param nickname 邮件发件人 + * @param title 邮件标题 + * @param content 邮件内容 + */ + public void sendMailSendMessage(Long sendLogId, List toMails, List ccMails, List bccMails, + Long accountId, String nickname, String title, String content) { MailSendMessage message = new MailSendMessage() - .setLogId(sendLogId).setMail(mail).setAccountId(accountId) - .setNickname(nickname).setTitle(title).setContent(content); + .setLogId(sendLogId) + .setToMails(toMails).setCcMails(ccMails).setBccMails(bccMails) + .setAccountId(accountId).setNickname(nickname) + .setTitle(title).setContent(content); applicationContext.publishEvent(message); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java index 4a0b204385..168179aec7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; +import java.util.List; import java.util.Map; /** @@ -49,6 +50,25 @@ public interface MailLogService { MailAccountDO account, MailTemplateDO template , String templateContent, Map templateParams, Boolean isSend); + /** + * 创建邮件日志 + * + * @param userId 用户编码 + * @param userType 用户类型 + * @param toMails 收件人邮件 + * @param ccMails 收件人邮件 + * @param bccMails 收件人邮件 + * @param account 邮件账号信息 + * @param template 模版信息 + * @param templateContent 模版内容 + * @param templateParams 模版参数 + * @param isSend 是否发送成功 + * @return 日志编号 + */ + Long createMailLog(Long userId, Integer userType, List toMails, List ccMails, List bccMails, + MailAccountDO account, MailTemplateDO template, + String templateContent, Map templateParams, Boolean isSend); + /** * 更新邮件发送结果 * diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java index 827d0c56e9..28d233da58 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.service.mail; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; @@ -12,6 +13,7 @@ import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -61,6 +63,31 @@ public class MailLogServiceImpl implements MailLogService { return logDO.getId(); } + @Override + public Long createMailLog(Long userId, Integer userType, List toMails, List ccMails, List bccMails, + MailAccountDO account, MailTemplateDO template, + String templateContent, Map templateParams, Boolean isSend) { + String toMail = CollUtil.isEmpty(toMails) ? "" : String.join(",", toMails); + String ccMail = CollUtil.isEmpty(ccMails) ? "" : String.join(",", ccMails); + String bccMail = CollUtil.isEmpty(bccMails) ? "" : String.join(",", bccMails); + + MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); + // 根据是否要发送,设置状态 + logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() + : MailSendStatusEnum.IGNORE.getStatus()) + // 用户信息 + .userId(userId).userType(userType).toMail(toMail).ccMail(ccMail).bccMail(bccMail) + .accountId(account.getId()).fromMail(account.getMail()) + // 模板相关字段 + .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) + .templateTitle(template.getTitle()).templateContent(templateContent).templateParams(templateParams); + + // 插入数据库 + MailLogDO logDO = logDOBuilder.build(); + mailLogMapper.insert(logDO); + return logDO.getId(); + } + @Override public void updateMailSendResult(Long logId, String messageId, Exception exception) { // 1. 成功 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java index 898816868f..5300157b15 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.service.mail; import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage; +import java.util.List; import java.util.Map; /** @@ -49,6 +50,49 @@ public interface MailSendService { Long sendSingleMail(String mail, Long userId, Integer userType, String templateCode, Map templateParams); + /** + * 发送多条邮件给管理后台的用户 + * + * @param toMails 收件邮箱 + * @param ccMails 抄送邮箱 + * @param bccMails 密送邮箱 + * @param userId 用户编码 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendMultipleMailToAdmin(List toMails, List ccMails, List bccMails, + Long userId, String templateCode, Map templateParams); + + /** + * 发送多条邮件给用户 APP 的用户 + * + * @param toMails 收件邮箱 + * @param ccMails 抄送邮箱 + * @param bccMails 密送邮箱 + * @param userId 用户编码 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendMultipleMailToMember(List toMails, List ccMails, List bccMails, + Long userId, String templateCode, Map templateParams); + + /** + * 发送单条邮件给用户 + * + * @param toMails 收件邮箱 + * @param ccMails 抄送邮箱 + * @param bccMails 密送邮箱 + * @param userId 用户编码 + * @param userType 用户类型 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendMultipleMail(List toMails, List ccMails, List bccMails, + Long userId, Integer userType, String templateCode, Map templateParams); + /** * 执行真正的邮件发送 * 注意,该方法仅仅提供给 MQ Consumer 使用 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java index 306b05c048..580777ca70 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.service.mail; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; @@ -17,10 +18,12 @@ import org.dromara.hutool.extra.mail.*; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singletonList; /** * 邮箱发送 Service 实现类 @@ -99,6 +102,70 @@ public class MailSendServiceImpl implements MailSendService { return sendLogId; } + @Override + public Long sendMultipleMailToAdmin(List toMails, List ccMails, List bccMails, Long userId, String templateCode, Map templateParams) { + // 如果 mail 为空,则加载用户编号对应的邮箱 + if (CollUtil.isEmpty(toMails)) { + AdminUserDO user = adminUserService.getUser(userId); + if (user != null) { + toMails = singletonList(user.getEmail()); + } + } + // 执行发送 + return sendMultipleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendMultipleMailToMember(List toMails, List ccMails, List bccMails, Long userId, String templateCode, Map templateParams) { + // 如果 mail 为空,则加载用户编号对应的邮箱 + if (CollUtil.isEmpty(toMails)) { + toMails = singletonList(memberService.getMemberUserEmail(userId)); + } + // 执行发送 + return sendMultipleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendMultipleMail(List toMails, List ccMails, List bccMails, Long userId, Integer userType, String templateCode, Map templateParams) { + // 校验邮箱模版是否合法 + MailTemplateDO template = validateMailTemplate(templateCode); + // 校验邮箱账号是否合法 + MailAccountDO account = validateMailAccount(template.getAccountId()); + // 校验邮件参数是否缺失 + validateTemplateParams(template, templateParams); + + if (CollUtil.isEmpty(toMails)) { + throw exception(MAIL_SEND_MAIL_NOT_EXISTS); + } + // 校验邮箱是否存在 + for (String mail : toMails) { + validateMail(mail); + } + if (CollUtil.isNotEmpty(ccMails)) { + for (String mail : ccMails) { + validateMail(mail); + } + } + if (CollUtil.isNotEmpty(bccMails)) { + for (String mail : bccMails) { + validateMail(mail); + } + } + + // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); + String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); + String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); + Long sendLogId = mailLogService.createMailLog(userId, userType, toMails, ccMails, bccMails, + account, template, content, templateParams, isSend); + // 发送 MQ 消息,异步执行发送短信 + if (isSend) { + mailProducer.sendMailSendMessage(sendLogId, toMails, ccMails, bccMails, account.getId(), + template.getNickname(), title, content); + } + return sendLogId; + } + @Override public void doSendMail(MailSendMessage message) { // 1. 创建发送账号 @@ -106,7 +173,7 @@ public class MailSendServiceImpl implements MailSendService { MailAccount mailAccount = buildMailAccount(account, message.getNickname()); // 2. 发送邮件 try { - String messageId = MailUtil.send(mailAccount, message.getMail(), + String messageId = MailUtil.send(mailAccount, message.getToMails(), message.getCcMails(), message.getBccMails(), message.getTitle(), message.getContent(), true); // 3. 更新结果(成功) mailLogService.updateMailSendResult(message.getLogId(), messageId, null); @@ -155,7 +222,7 @@ public class MailSendServiceImpl implements MailSendService { } /** - * 校验邮件参数是否确实 + * 校验邮件参数是否缺失 * * @param template 邮箱模板 * @param templateParams 参数列表 From d2bf9945f7690760f7f881e2b406c0e41b6d6e85 Mon Sep 17 00:00:00 2001 From: LesanOuO <1960681385@qq.com> Date: Sat, 2 Aug 2025 14:14:42 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AE=A1=E6=89=B9=E4=BA=BA=E6=92=A4=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/model/BpmModelMetaInfoVO.java | 3 + .../admin/task/BpmTaskController.java | 8 ++ .../BpmProcessDefinitionInfoDO.java | 5 + .../module/bpm/enums/ErrorCodeConstants.java | 4 + .../module/bpm/enums/task/BpmReasonEnum.java | 1 + .../flowable/core/util/BpmnModelUtils.java | 42 ++++++++ .../bpm/service/task/BpmTaskService.java | 8 ++ .../bpm/service/task/BpmTaskServiceImpl.java | 96 +++++++++++++++---- 8 files changed, 151 insertions(+), 16 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index d2316f58eb..dca75851e9 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java @@ -97,6 +97,9 @@ public class BpmModelMetaInfoVO { @Schema(description = "任务后置通知设置", example = "{}") private HttpRequestSetting taskAfterTriggerSetting; + @Schema(description = "允许允许审批人撤回任务", example = "false") + private Boolean allowWithdrawTask; + @Schema(description = "流程 ID 规则") @Data @Valid diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java index b796c5c17e..b327b8e777 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java @@ -219,6 +219,14 @@ public class BpmTaskController { return success(true); } + @PutMapping("/withdraw") + @Operation(summary = "撤回任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult withdrawTask(@RequestParam("taskId") String taskId) { + taskService.withdrawTask(getLoginUserId(), taskId); + return success(true); + } + @GetMapping("/list-by-parent-task-id") @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表 @Parameter(name = "parentTaskId", description = "父级任务编号", required = true) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index c2799ef67f..7f10bda388 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java @@ -219,4 +219,9 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting; + /** + * 是否允许审批人撤回任务 + */ + private Boolean allowWithdrawTask; + } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index 4f7d7bf7ce..dabedb5463 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -61,6 +61,10 @@ public interface ErrorCodeConstants { ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!"); ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); + ErrorCode TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING = new ErrorCode(1_009_005_017, "撤回失败,流程实例未运行!"); + ErrorCode TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS = new ErrorCode(1_009_005_018, "撤回失败,未查询到用户已办任务!"); + ErrorCode TASK_WITHDRAW_FAIL_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,此流程不允许撤回操作!"); + ErrorCode TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW = new ErrorCode(1_009_005_019, "撤回失败,下一节点不满足撤回条件!"); // ========== 动态表单模块 1-009-010-000 ========== ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java index 46d1482a5e..6ce6f65b82 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java @@ -35,6 +35,7 @@ public enum BpmReasonEnum { APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"), APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"), CANCEL_BY_PROCESS_CLEAN("进程清理自动取消"), + CANCEL_BY_WITHDRAW("前一任务撤回,系统自动取消"), ; private final String reason; diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 1bafa578db..978693c0c8 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -908,6 +908,48 @@ public class BpmnModelUtils { return nextFlowNodes; } + /** + * 查找起始节点下一个用户任务列表列表 + * @param source 起始节点 + * @return 结果 + */ + public static List getNextUserTasks(FlowElement source) { + return getNextUserTasks(source, null, null); + } + + /** + * 查找起始节点下一个用户任务列表列表 + * @param source 起始节点 + * @param hasSequenceFlow 已经经过的连线的 ID,用于判断线路是否重复 + * @param userTaskList 用户任务列表 + * @return 结果 + */ + public static List getNextUserTasks(FlowElement source, Set hasSequenceFlow, List userTaskList) { + hasSequenceFlow = Optional.ofNullable(hasSequenceFlow).orElse(new HashSet<>()); + userTaskList = Optional.ofNullable(userTaskList).orElse(new ArrayList<>()); + // 获取出口连线 + List sequenceFlows = getElementOutgoingFlows(source); + if (!sequenceFlows.isEmpty()) { + for (SequenceFlow sequenceFlow : sequenceFlows) { + // 如果发现连线重复,说明循环了,跳过这个循环 + if (hasSequenceFlow.contains(sequenceFlow.getId())) { + continue; + } + // 添加已经走过的连线 + hasSequenceFlow.add(sequenceFlow.getId()); + FlowElement targetFlowElement = sequenceFlow.getTargetFlowElement(); + if (targetFlowElement instanceof UserTask) { + // 若节点为用户任务,加入到结果列表中 + userTaskList.add((UserTask) targetFlowElement); + } else { + // 若节点非用户任务,继续递归查找下一个节点 + getNextUserTasks(targetFlowElement, hasSequenceFlow, userTaskList); + } + } + } + return userTaskList; + } + /** * 处理排它网关 * diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index 0a5c866fda..34db2876fa 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -250,6 +250,14 @@ public interface BpmTaskService { */ void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO); + /** + * 撤回任务 + * + * @param userId 用户编号 + * @param taskId 任务编号 + */ + void withdrawTask(Long userId, String taskId); + // ========== Event 事件相关方法 ========== /** diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 8ea607a773..d75a91c12b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -40,10 +40,9 @@ import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.*; -import org.flowable.engine.HistoryService; -import org.flowable.engine.ManagementService; -import org.flowable.engine.RuntimeService; -import org.flowable.engine.TaskService; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.api.FlowableObjectNotFoundException; +import org.flowable.engine.*; import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.runtime.ActivityInstance; import org.flowable.engine.runtime.Execution; @@ -62,6 +61,7 @@ import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -196,7 +196,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { /** * 获得用户指定 processInstanceId 流程编号下的首个“待办”(未审批、且可审核)的任务 * - * @param userId 用户编号 + * @param userId 用户编号 * @param processInstanceId 流程编号 * @return 任务 */ @@ -599,15 +599,15 @@ public class BpmTaskServiceImpl implements BpmTaskService { /** * 校验选择的下一个节点的审批人,是否合法 - * + *

* 1. 是否有漏选:没有选择审批人 * 2. 是否有多选:非下一个节点 * * @param taskDefinitionKey 当前任务节点标识 - * @param variables 流程变量 - * @param bpmnModel 流程模型 - * @param nextAssignees 下一个节点审批人集合(参数) - * @param processInstance 流程实例 + * @param variables 流程变量 + * @param bpmnModel 流程模型 + * @param nextAssignees 下一个节点审批人集合(参数) + * @param processInstance 流程实例 */ @SuppressWarnings("unchecked") private Map validateAndSetNextAssignees(String taskDefinitionKey, Map variables, BpmnModel bpmnModel, @@ -659,7 +659,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { approveUserSelectAssignees = new HashMap<>(); } approveUserSelectAssignees.put(nextFlowNode.getId(), assignees); - Map> existingApproveUserSelectAssignees = (Map>) variables.get( + Map> existingApproveUserSelectAssignees = (Map>) variables.get( BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); if (CollUtil.isNotEmpty(existingApproveUserSelectAssignees)) { approveUserSelectAssignees.putAll(existingApproveUserSelectAssignees); @@ -1177,6 +1177,70 @@ public class BpmTaskServiceImpl implements BpmTaskService { processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId()); } + @Override + @Transactional(rollbackFor = Exception.class) + public void withdrawTask(Long userId, String taskId) { + // 1.查询本人已办任务 + HistoricTaskInstance taskInstance = historyService.createHistoricTaskInstanceQuery() + .taskId(taskId).taskAssignee(userId.toString()).finished().singleResult(); + if (ObjectUtil.isNull(taskInstance)) { + throw exception(TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS); + } + // 2.校验流程是否结束 + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() + .processInstanceId(taskInstance.getProcessInstanceId()) + .active() + .singleResult(); + if (ObjectUtil.isNull(processInstance)) { + throw exception(TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING); + } + // 3.判断此流程是否允许撤回 + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService + .getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); + if (ObjectUtil.isNull(processDefinitionInfo) || !Boolean.TRUE.equals(processDefinitionInfo.getAllowWithdrawTask())) { + throw exception(TASK_WITHDRAW_FAIL_NOT_ALLOW); + } + // 4.判断此任务下一节点是否满足撤回 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskInstance.getProcessDefinitionId()); + UserTask userTask = (UserTask) BpmnModelUtils.getFlowElementById(bpmnModel, taskInstance.getTaskDefinitionKey()); + List nextUserTaskList = BpmnModelUtils.getNextUserTasks(userTask); + List nextUserTaskKeys = nextUserTaskList.stream().map(UserTask::getId).toList(); + if (CollUtil.isEmpty(nextUserTaskKeys)) { + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + long nextUserTaskFinishedCount = historyService.createHistoricTaskInstanceQuery() + .processInstanceId(processInstance.getProcessInstanceId()) + .taskDefinitionKeys(nextUserTaskKeys) + .taskCreatedAfter(taskInstance.getEndTime()) // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以 + .finished() + .count(); + if (nextUserTaskFinishedCount > 0) { + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + // 5.获取需要撤回的运行任务 + List runningTaskList = taskService.createTaskQuery() + .processInstanceId(processInstance.getProcessInstanceId()) + .taskDefinitionKeys(nextUserTaskKeys) + .active().list(); + if (CollUtil.isEmpty(runningTaskList)) { + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + List withdrawExecutionIds = new ArrayList<>(); + for (Task task : runningTaskList) { + // 标记撤回任务为取消 + // TODO @芋艿:是否需要添加被撤回状态? + taskService.addComment(task.getId(), taskInstance.getProcessInstanceId(), BpmCommentTypeEnum.CANCEL.getType(), + BpmCommentTypeEnum.CANCEL.formatComment("前一节点撤回")); + updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_WITHDRAW.getReason()); + withdrawExecutionIds.add(task.getExecutionId()); + } + // 6.执行撤回操作 + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(processInstance.getProcessInstanceId()) + .moveExecutionsToSingleActivityId(withdrawExecutionIds, taskInstance.getTaskDefinitionKey()) + .changeState(); + } + /** * 校验任务是否能被减签 * @@ -1223,7 +1287,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 2. 任务前置通知 - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())){ + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskBeforeTriggerSetting())) { BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskBeforeTriggerSetting(); BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); @@ -1350,7 +1414,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) .finished(); if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType()) - && sameAssigneeQuery.count() > 0) { + && sameAssigneeQuery.count() > 0) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName())); return; @@ -1362,7 +1426,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { return; } List sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 - BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), + BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), SequenceFlow::getSourceRef); if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) @@ -1387,7 +1451,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); if (userTaskElement.getId().equals(START_USER_NODE_ID) && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 - || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 + || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); @@ -1456,7 +1520,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 任务后置通知 - if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())){ + if (ObjUtil.isNotNull(processDefinitionInfo.getTaskAfterTriggerSetting())) { BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getTaskAfterTriggerSetting(); BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); From 51e5294a3e5cc2a1af47abab60b8f11035a84c6c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 Aug 2025 15:44:19 +0800 Subject: [PATCH 03/11] =?UTF-8?q?review=EF=BC=9A=E3=80=90bpm=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E6=92=A4=E5=9B=9E=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=88=E6=B2=A1=E9=97=AE=E9=A2=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/model/BpmModelMetaInfoVO.java | 6 +- .../BpmProcessDefinitionInfoDO.java | 10 ++-- .../flowable/core/util/BpmnModelUtils.java | 3 +- .../bpm/service/task/BpmTaskServiceImpl.java | 59 ++++++++----------- 4 files changed, 36 insertions(+), 42 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index dca75851e9..943a82d546 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/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 { @Schema(description = "允许撤销审批中的申请", example = "true") private Boolean allowCancelRunningProcess; + @Schema(description = "允许允许审批人撤回任务", example = "false") + private Boolean allowWithdrawTask; + @Schema(description = "流程 ID 规则", example = "{}") private ProcessIdRule processIdRule; @@ -97,9 +100,6 @@ public class BpmModelMetaInfoVO { @Schema(description = "任务后置通知设置", example = "{}") private HttpRequestSetting taskAfterTriggerSetting; - @Schema(description = "允许允许审批人撤回任务", example = "false") - private Boolean allowWithdrawTask; - @Schema(description = "流程 ID 规则") @Data @Valid diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index 7f10bda388..37e2c4462d 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java @@ -172,6 +172,11 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { */ private Boolean allowCancelRunningProcess; + /** + * 是否允许审批人撤回任务 + */ + private Boolean allowWithdrawTask; + /** * 流程 ID 规则 */ @@ -219,9 +224,4 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.HttpRequestSetting taskAfterTriggerSetting; - /** - * 是否允许审批人撤回任务 - */ - private Boolean allowWithdrawTask; - } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 978693c0c8..a3414cedb4 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -852,7 +852,7 @@ public class BpmnModelUtils { } else if (flowNode instanceof ScriptTask) { skipExpression = ((ScriptTask) flowNode).getSkipExpression(); } - + if (StrUtil.isEmpty(skipExpression)) { return false; } @@ -910,6 +910,7 @@ public class BpmnModelUtils { /** * 查找起始节点下一个用户任务列表列表 + * * @param source 起始节点 * @return 结果 */ diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index d75a91c12b..133f05c76b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -40,9 +40,10 @@ import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.*; -import org.flowable.common.engine.api.FlowableException; -import org.flowable.common.engine.api.FlowableObjectNotFoundException; -import org.flowable.engine.*; +import org.flowable.engine.HistoryService; +import org.flowable.engine.ManagementService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.runtime.ActivityInstance; import org.flowable.engine.runtime.Execution; @@ -61,7 +62,6 @@ import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -1180,61 +1180,54 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Override @Transactional(rollbackFor = Exception.class) public void withdrawTask(Long userId, String taskId) { - // 1.查询本人已办任务 + // 1.1 查询本人已办任务 HistoricTaskInstance taskInstance = historyService.createHistoricTaskInstanceQuery() .taskId(taskId).taskAssignee(userId.toString()).finished().singleResult(); - if (ObjectUtil.isNull(taskInstance)) { + if (ObjUtil.isNull(taskInstance)) { throw exception(TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS); } - // 2.校验流程是否结束 - ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() - .processInstanceId(taskInstance.getProcessInstanceId()) - .active() - .singleResult(); - if (ObjectUtil.isNull(processInstance)) { + // 1.2 校验流程是否结束 + ProcessInstance processInstance = processInstanceService.getProcessInstance(taskInstance.getProcessInstanceId()); + if (ObjUtil.isNull(processInstance)) { throw exception(TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING); } - // 3.判断此流程是否允许撤回 - BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService - .getProcessDefinitionInfo(processInstance.getProcessDefinitionId()); - if (ObjectUtil.isNull(processDefinitionInfo) || !Boolean.TRUE.equals(processDefinitionInfo.getAllowWithdrawTask())) { + // 1.3 判断此流程是否允许撤回 + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo( + processInstance.getProcessDefinitionId()); + if (ObjUtil.isNull(processDefinitionInfo) || !Boolean.TRUE.equals(processDefinitionInfo.getAllowWithdrawTask())) { throw exception(TASK_WITHDRAW_FAIL_NOT_ALLOW); } - // 4.判断此任务下一节点是否满足撤回 + // 1.4 判断下一个节点是否被审批过,如果是则无法撤回 BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskInstance.getProcessDefinitionId()); UserTask userTask = (UserTask) BpmnModelUtils.getFlowElementById(bpmnModel, taskInstance.getTaskDefinitionKey()); - List nextUserTaskList = BpmnModelUtils.getNextUserTasks(userTask); - List nextUserTaskKeys = nextUserTaskList.stream().map(UserTask::getId).toList(); + List nextUserTaskKeys = convertList(BpmnModelUtils.getNextUserTasks(userTask), UserTask::getId); if (CollUtil.isEmpty(nextUserTaskKeys)) { throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); } + // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以;包括 todo 和 done 那边的查询哇??? long nextUserTaskFinishedCount = historyService.createHistoricTaskInstanceQuery() - .processInstanceId(processInstance.getProcessInstanceId()) - .taskDefinitionKeys(nextUserTaskKeys) - .taskCreatedAfter(taskInstance.getEndTime()) // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以 - .finished() - .count(); + .processInstanceId(processInstance.getProcessInstanceId()).taskDefinitionKeys(nextUserTaskKeys) + .taskCreatedAfter(taskInstance.getEndTime()).finished().count(); if (nextUserTaskFinishedCount > 0) { throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); } - // 5.获取需要撤回的运行任务 - List runningTaskList = taskService.createTaskQuery() - .processInstanceId(processInstance.getProcessInstanceId()) - .taskDefinitionKeys(nextUserTaskKeys) - .active().list(); - if (CollUtil.isEmpty(runningTaskList)) { + // 1.5 获取需要撤回的运行任务 + List runningTasks = taskService.createTaskQuery().processInstanceId(processInstance.getProcessInstanceId()) + .taskDefinitionKeys(nextUserTaskKeys).active().list(); + if (CollUtil.isEmpty(runningTasks)) { throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); } + + // 2.1 取消当前任务 List withdrawExecutionIds = new ArrayList<>(); - for (Task task : runningTaskList) { + for (Task task : runningTasks) { // 标记撤回任务为取消 - // TODO @芋艿:是否需要添加被撤回状态? taskService.addComment(task.getId(), taskInstance.getProcessInstanceId(), BpmCommentTypeEnum.CANCEL.getType(), BpmCommentTypeEnum.CANCEL.formatComment("前一节点撤回")); updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_WITHDRAW.getReason()); withdrawExecutionIds.add(task.getExecutionId()); } - // 6.执行撤回操作 + // 2.2 执行撤回操作 runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstance.getProcessInstanceId()) .moveExecutionsToSingleActivityId(withdrawExecutionIds, taskInstance.getTaskDefinitionKey()) From d4b1acb11e9191c508475d03690855cd2b0aeadc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 Aug 2025 16:40:09 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat=EF=BC=9A=E3=80=90system=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91dept=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/dept/DeptController.java | 9 +++++ .../system/service/dept/DeptService.java | 7 ++++ .../system/service/dept/DeptServiceImpl.java | 15 ++++++++ .../service/dept/DeptServiceImplTest.java | 34 +++++++++++++++++-- 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java index 7873d00f0a..7a243b778d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java @@ -56,6 +56,15 @@ public class DeptController { return success(true); } + @DeleteMapping("/delete-list") + @Operation(summary = "批量删除部门") + @Parameter(name = "ids", description = "编号列表", required = true) + @PreAuthorize("@ss.hasPermission('system:dept:delete')") + public CommonResult deleteDeptList(@RequestParam("ids") List ids) { + deptService.deleteDeptList(ids); + return success(true); + } + @GetMapping("/list") @Operation(summary = "获取部门列表") @PreAuthorize("@ss.hasPermission('system:dept:query')") diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java index a0b765e590..06a688e606 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java @@ -36,6 +36,13 @@ public interface DeptService { */ void deleteDept(Long id); + /** + * 批量删除部门 + * + * @param ids 部门编号数组 + */ + void deleteDeptList(List ids); + /** * 获得部门信息 * diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java index 946d92df3b..6086474c60 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java @@ -88,6 +88,21 @@ public class DeptServiceImpl implements DeptService { deptMapper.deleteById(id); } + @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 + public void deleteDeptList(List ids) { + // 校验是否有子部门 + for (Long id : ids) { + if (deptMapper.selectCountByParentId(id) > 0) { + throw exception(DEPT_EXITS_CHILDREN); + } + } + + // 批量删除部门 + deptMapper.deleteByIds(ids); + } + @VisibleForTesting void validateDeptExists(Long id) { if (id == null) { diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java index bcf55bda66..df16d4fc97 100644 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java @@ -105,12 +105,40 @@ public class DeptServiceImplTest extends BaseDbUnitTest { } @Test - public void testValidateDeptExists_notFound() { + public void testDeleteDeptList_success() { + // mock 数据 + DeptDO deptDO1 = randomPojo(DeptDO.class); + deptMapper.insert(deptDO1); + DeptDO deptDO2 = randomPojo(DeptDO.class); + deptMapper.insert(deptDO2); // 准备参数 - Long id = randomLongId(); + List ids = Arrays.asList(deptDO1.getId(), deptDO2.getId()); + + // 调用 + deptService.deleteDeptList(ids); + // 校验数据不存在了 + assertNull(deptMapper.selectById(deptDO1.getId())); + assertNull(deptMapper.selectById(deptDO2.getId())); + } + + @Test + public void testDeleteDeptList_exitsChildren() { + // mock 数据 + DeptDO parentDept = randomPojo(DeptDO.class); + deptMapper.insert(parentDept); + DeptDO childrenDeptDO = randomPojo(DeptDO.class, o -> { + o.setParentId(parentDept.getId()); + o.setStatus(randomCommonStatus()); + }); + deptMapper.insert(childrenDeptDO); + DeptDO anotherDept = randomPojo(DeptDO.class); + deptMapper.insert(anotherDept); + + // 准备参数(包含有子部门的 parentDept) + List ids = Arrays.asList(parentDept.getId(), anotherDept.getId()); // 调用, 并断言异常 - assertServiceException(() -> deptService.validateDeptExists(id), DEPT_NOT_FOUND); + assertServiceException(() -> deptService.deleteDeptList(ids), DEPT_EXITS_CHILDREN); } @Test From 93cb8bad066c077d9ddc8de7a9f698e0b48fe52c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 5 Aug 2025 21:14:08 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat=EF=BC=9A=E3=80=90system=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8A=9F=E8=83=BD=E3=80=91=E9=82=AE=E7=AE=B1=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=8A=84=E9=80=81=E3=80=81=E5=AF=86=E9=80=81=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/ruoyi-vue-pro.sql | 6 +- .../system/api/mail/MailSendApiImpl.java | 6 +- .../mail/dto/MailSendSingleToUserReqDTO.java | 19 ++- .../admin/mail/MailTemplateController.java | 3 +- .../admin/mail/vo/log/MailLogRespVO.java | 7 +- .../system/dal/dataobject/mail/MailLogDO.java | 15 +- .../system/dal/mysql/mail/MailLogMapper.java | 5 +- .../mq/message/mail/MailSendMessage.java | 7 +- .../system/mq/producer/mail/MailProducer.java | 19 +-- .../system/service/mail/MailLogService.java | 21 +-- .../service/mail/MailLogServiceImpl.java | 37 +---- .../system/service/mail/MailSendService.java | 81 ++++------ .../service/mail/MailSendServiceImpl.java | 145 ++++++------------ .../service/mail/MailLogServiceImplTest.java | 26 +++- .../service/mail/MailSendServiceImplTest.java | 123 +++++++-------- .../src/test/resources/sql/create_tables.sql | 4 +- 16 files changed, 215 insertions(+), 309 deletions(-) diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index c37fffca10..b9dad19d90 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -1259,9 +1259,9 @@ CREATE TABLE `system_mail_log` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', `user_id` bigint NULL DEFAULT NULL COMMENT '用户编号', `user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型', - `to_mail` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址', - `cc_mail` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '抄送邮箱地址', - `bcc_mail` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密送邮箱地址', + `to_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址', + `cc_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '抄送邮箱地址', + `bcc_mails` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密送邮箱地址', `account_id` bigint NOT NULL COMMENT '邮箱账号编号', `from_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送邮箱地址', `template_id` bigint NOT NULL COMMENT '模板编号', diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java index 0a8910e1c3..59bb505891 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java @@ -21,13 +21,15 @@ public class MailSendApiImpl implements MailSendApi { @Override public Long sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) { - return mailSendService.sendSingleMailToAdmin(reqDTO.getMail(), reqDTO.getUserId(), + return mailSendService.sendSingleMailToAdmin(reqDTO.getUserId(), + reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } @Override public Long sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) { - return mailSendService.sendSingleMailToMember(reqDTO.getMail(), reqDTO.getUserId(), + return mailSendService.sendSingleMailToMember(reqDTO.getUserId(), + reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java index 0481d59225..2d67a78087 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java @@ -4,6 +4,8 @@ import lombok.Data; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; + +import java.util.List; import java.util.Map; /** @@ -16,13 +18,24 @@ public class MailSendSingleToUserReqDTO { /** * 用户编号 + * + * 如果非空,则加载对应用户的邮箱,添加到 {@link #toMails} 中 */ private Long userId; + /** - * 邮箱 + * 收件邮箱 */ - @Email - private String mail; + private List<@Email String> toMails; + /** + * 抄送邮箱 + */ + private List<@Email String> ccMails; + /** + * 密送邮箱 + */ + private List<@Email String> bccMails; + /** * 邮件模板编号 diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java index f85f9e34e4..52ac150875 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java @@ -91,7 +91,8 @@ public class MailTemplateController { @Operation(summary = "发送短信") @PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')") public CommonResult sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) { - return success(mailSendService.sendMultipleMailToAdmin(sendReqVO.getToMails(), sendReqVO.getCcMails(), sendReqVO.getBccMails(), getLoginUserId(), + return success(mailSendService.sendSingleMailToAdmin(getLoginUserId(), + sendReqVO.getToMails(), sendReqVO.getCcMails(), sendReqVO.getBccMails(), sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java index b53488510c..8e67d1df73 100755 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; @Schema(description = "管理后台 - 邮件日志 Response VO") @@ -20,13 +21,13 @@ public class MailLogRespVO { private Byte userType; @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user1@example.com, user2@example.com") - private String toMail; + private List toMails; @Schema(description = "抄送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user3@example.com, user4@example.com") - private String ccMail; + private List ccMails; @Schema(description = "密送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user5@example.com, user6@example.com") - private String bccMail; + private List bccMails; @Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107") private Long accountId; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java index 9fac8582c1..756dba2ad7 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java +++ b/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; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.system.enums.mail.MailSendStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; @@ -12,6 +13,7 @@ import lombok.*; import java.io.Serializable; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; /** @@ -47,18 +49,23 @@ public class MailLogDO extends BaseDO implements Serializable { * 枚举 {@link UserTypeEnum} */ private Integer userType; + /** * 接收邮箱地址 */ - private String toMail; + @TableField(typeHandler = StringListTypeHandler.class) + private List toMails; /** * 接收邮箱地址 */ - private String ccMail; + @TableField(typeHandler = StringListTypeHandler.class) + private List ccMails; /** - * 接收邮箱地址 + * 密送邮箱地址 */ - private String bccMail; + @TableField(typeHandler = StringListTypeHandler.class) + private List bccMails; + /** * 邮箱账号编号 * diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java index 6b147cff62..44fab07a0d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.system.dal.mysql.mail; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; import org.apache.ibatis.annotations.Mapper; @@ -14,11 +16,12 @@ public interface MailLogMapper extends BaseMapperX { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(MailLogDO::getUserId, reqVO.getUserId()) .eqIfPresent(MailLogDO::getUserType, reqVO.getUserType()) - .likeIfPresent(MailLogDO::getToMail, reqVO.getToMail()) .eqIfPresent(MailLogDO::getAccountId, reqVO.getAccountId()) .eqIfPresent(MailLogDO::getTemplateId, reqVO.getTemplateId()) .eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus()) .betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime()) + .apply(StrUtil.isNotBlank(reqVO.getToMail()), + MyBatisUtils.findInSet("to_mails", reqVO.getToMail())) .orderByDesc(MailLogDO::getId)); } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java index 06c4a5102b..03a4b7f198 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java @@ -5,6 +5,7 @@ import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.util.Collection; import java.util.List; /** @@ -24,15 +25,15 @@ public class MailSendMessage { * 接收邮件地址 */ @NotEmpty(message = "接收邮件地址不能为空") - private List toMails; + private Collection toMails; /** * 抄送邮件地址 */ - private List ccMails; + private Collection ccMails; /** * 密送邮件地址 */ - private List bccMails; + private Collection bccMails; /** * 邮件账号编号 */ diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java index 8a485c7fe0..07aabb00a8 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component; import jakarta.annotation.Resource; +import java.util.Collection; import java.util.List; import static java.util.Collections.singletonList; @@ -24,21 +25,6 @@ public class MailProducer { @Resource private ApplicationContext applicationContext; - /** - * 发送 {@link MailSendMessage} 消息 - * - * @param sendLogId 发送日志编码 - * @param mail 接收邮件地址 - * @param accountId 邮件账号编号 - * @param nickname 邮件发件人 - * @param title 邮件标题 - * @param content 邮件内容 - */ - public void sendMailSendMessage(Long sendLogId, String mail, Long accountId, - String nickname, String title, String content) { - sendMailSendMessage(sendLogId, singletonList(mail), null, null, accountId, nickname, title, content); - } - /** * 发送 {@link MailSendMessage} 消息 * @@ -51,7 +37,8 @@ public class MailProducer { * @param title 邮件标题 * @param content 邮件内容 */ - public void sendMailSendMessage(Long sendLogId, List toMails, List ccMails, List bccMails, + public void sendMailSendMessage(Long sendLogId, + Collection toMails, Collection ccMails, Collection bccMails, Long accountId, String nickname, String title, String content) { MailSendMessage message = new MailSendMessage() .setLogId(sendLogId) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java index 168179aec7..1c66e55ef4 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailLogDO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -33,23 +34,6 @@ public interface MailLogService { */ MailLogDO getMailLog(Long id); - /** - * 创建邮件日志 - * - * @param userId 用户编码 - * @param userType 用户类型 - * @param toMail 收件人邮件 - * @param account 邮件账号信息 - * @param template 模版信息 - * @param templateContent 模版内容 - * @param templateParams 模版参数 - * @param isSend 是否发送成功 - * @return 日志编号 - */ - Long createMailLog(Long userId, Integer userType, String toMail, - MailAccountDO account, MailTemplateDO template , - String templateContent, Map templateParams, Boolean isSend); - /** * 创建邮件日志 * @@ -65,7 +49,8 @@ public interface MailLogService { * @param isSend 是否发送成功 * @return 日志编号 */ - Long createMailLog(Long userId, Integer userType, List toMails, List ccMails, List bccMails, + Long createMailLog(Long userId, Integer userType, + Collection toMails, Collection ccMails, Collection bccMails, MailAccountDO account, MailTemplateDO template, String templateContent, Map templateParams, Boolean isSend); diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java index 28d233da58..c17abaf016 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.system.service.mail; -import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; @@ -13,9 +13,7 @@ import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; @@ -43,7 +41,8 @@ public class MailLogServiceImpl implements MailLogService { } @Override - public Long createMailLog(Long userId, Integer userType, String toMail, + public Long createMailLog(Long userId, Integer userType, + Collection toMails, Collection ccMails, Collection bccMails, MailAccountDO account, MailTemplateDO template, String templateContent, Map templateParams, Boolean isSend) { MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); @@ -51,32 +50,8 @@ public class MailLogServiceImpl implements MailLogService { logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() : MailSendStatusEnum.IGNORE.getStatus()) // 用户信息 - .userId(userId).userType(userType).toMail(toMail) - .accountId(account.getId()).fromMail(account.getMail()) - // 模板相关字段 - .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) - .templateTitle(template.getTitle()).templateContent(templateContent).templateParams(templateParams); - - // 插入数据库 - MailLogDO logDO = logDOBuilder.build(); - mailLogMapper.insert(logDO); - return logDO.getId(); - } - - @Override - public Long createMailLog(Long userId, Integer userType, List toMails, List ccMails, List bccMails, - MailAccountDO account, MailTemplateDO template, - String templateContent, Map templateParams, Boolean isSend) { - String toMail = CollUtil.isEmpty(toMails) ? "" : String.join(",", toMails); - String ccMail = CollUtil.isEmpty(ccMails) ? "" : String.join(",", ccMails); - String bccMail = CollUtil.isEmpty(bccMails) ? "" : String.join(",", bccMails); - - MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); - // 根据是否要发送,设置状态 - logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() - : MailSendStatusEnum.IGNORE.getStatus()) - // 用户信息 - .userId(userId).userType(userType).toMail(toMail).ccMail(ccMail).bccMail(bccMail) + .userId(userId).userType(userType) + .toMails(ListUtil.toList(toMails)).ccMails(ListUtil.toList(ccMails)).bccMails(ListUtil.toList(bccMails)) .accountId(account.getId()).fromMail(account.getMail()) // 模板相关字段 .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java index 5300157b15..1b600bc90c 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java @@ -1,8 +1,9 @@ package cn.iocoder.yudao.module.system.service.mail; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage; -import java.util.List; +import java.util.Collection; import java.util.Map; /** @@ -16,83 +17,55 @@ public interface MailSendService { /** * 发送单条邮件给管理后台的用户 * - * @param mail 邮箱 * @param userId 用户编码 + * @param toMails 收件邮箱 + * @param ccMails 抄送邮箱 + * @param bccMails 密送邮箱 * @param templateCode 邮件模版编码 * @param templateParams 邮件模版参数 * @return 发送日志编号 */ - Long sendSingleMailToAdmin(String mail, Long userId, - String templateCode, Map templateParams); + default Long sendSingleMailToAdmin(Long userId, + Collection toMails, Collection ccMails, Collection bccMails, + String templateCode, Map templateParams) { + return sendSingleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.ADMIN.getValue(), + templateCode, templateParams); + } /** * 发送单条邮件给用户 APP 的用户 * - * @param mail 邮箱 * @param userId 用户编码 + * @param toMails 收件邮箱 + * @param ccMails 抄送邮箱 + * @param bccMails 密送邮箱 * @param templateCode 邮件模版编码 * @param templateParams 邮件模版参数 * @return 发送日志编号 */ - Long sendSingleMailToMember(String mail, Long userId, - String templateCode, Map templateParams); + default Long sendSingleMailToMember(Long userId, + Collection toMails, Collection ccMails, Collection bccMails, + String templateCode, Map templateParams) { + return sendSingleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.MEMBER.getValue(), + templateCode, templateParams); + } /** - * 发送单条邮件给用户 + * 发送单条邮件 * - * @param mail 邮箱 - * @param userId 用户编码 + * @param toMails 收件邮箱 + * @param ccMails 抄送邮箱 + * @param bccMails 密送邮箱 + * @param userId 用户编号 * @param userType 用户类型 * @param templateCode 邮件模版编码 * @param templateParams 邮件模版参数 * @return 发送日志编号 */ - Long sendSingleMail(String mail, Long userId, Integer userType, + Long sendSingleMail(Collection toMails, Collection ccMails, Collection bccMails, + Long userId, Integer userType, String templateCode, Map templateParams); - /** - * 发送多条邮件给管理后台的用户 - * - * @param toMails 收件邮箱 - * @param ccMails 抄送邮箱 - * @param bccMails 密送邮箱 - * @param userId 用户编码 - * @param templateCode 邮件模版编码 - * @param templateParams 邮件模版参数 - * @return 发送日志编号 - */ - Long sendMultipleMailToAdmin(List toMails, List ccMails, List bccMails, - Long userId, String templateCode, Map templateParams); - - /** - * 发送多条邮件给用户 APP 的用户 - * - * @param toMails 收件邮箱 - * @param ccMails 抄送邮箱 - * @param bccMails 密送邮箱 - * @param userId 用户编码 - * @param templateCode 邮件模版编码 - * @param templateParams 邮件模版参数 - * @return 发送日志编号 - */ - Long sendMultipleMailToMember(List toMails, List ccMails, List bccMails, - Long userId, String templateCode, Map templateParams); - - /** - * 发送单条邮件给用户 - * - * @param toMails 收件邮箱 - * @param ccMails 抄送邮箱 - * @param bccMails 密送邮箱 - * @param userId 用户编码 - * @param userType 用户类型 - * @param templateCode 邮件模版编码 - * @param templateParams 邮件模版参数 - * @return 发送日志编号 - */ - Long sendMultipleMail(List toMails, List ccMails, List bccMails, - Long userId, Integer userType, String templateCode, Map templateParams); - /** * 执行真正的邮件发送 * 注意,该方法仅仅提供给 MQ Consumer 使用 diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java index 580777ca70..682696f932 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.system.service.mail; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Validator; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; @@ -14,16 +15,17 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService; import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.dromara.hutool.extra.mail.*; +import org.dromara.hutool.extra.mail.MailAccount; +import org.dromara.hutool.extra.mail.MailUtil; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import java.util.List; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; -import static java.util.Collections.singletonList; /** * 邮箱发送 Service 实现类 @@ -52,120 +54,67 @@ public class MailSendServiceImpl implements MailSendService { private MailProducer mailProducer; @Override - public Long sendSingleMailToAdmin(String mail, Long userId, - String templateCode, Map templateParams) { - // 如果 mail 为空,则加载用户编号对应的邮箱 - if (StrUtil.isEmpty(mail)) { - AdminUserDO user = adminUserService.getUser(userId); - if (user != null) { - mail = user.getEmail(); - } - } - // 执行发送 - return sendSingleMail(mail, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); - } - - @Override - public Long sendSingleMailToMember(String mail, Long userId, - String templateCode, Map templateParams) { - // 如果 mail 为空,则加载用户编号对应的邮箱 - if (StrUtil.isEmpty(mail)) { - mail = memberService.getMemberUserEmail(userId); - } - // 执行发送 - return sendSingleMail(mail, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); - } - - @Override - public Long sendSingleMail(String mail, Long userId, Integer userType, + public Long sendSingleMail(Collection toMails, Collection ccMails, Collection bccMails, + Long userId, Integer userType, String templateCode, Map templateParams) { - // 校验邮箱模版是否合法 + // 1.1 校验邮箱模版是否合法 MailTemplateDO template = validateMailTemplate(templateCode); - // 校验邮箱账号是否合法 + // 1.2 校验邮箱账号是否合法 MailAccountDO account = validateMailAccount(template.getAccountId()); - - // 校验邮箱是否存在 - mail = validateMail(mail); + // 1.3 校验邮件参数是否缺失 validateTemplateParams(template, templateParams); - // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 - Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); - String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); - String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); - Long sendLogId = mailLogService.createMailLog(userId, userType, mail, - account, template, content, templateParams, isSend); - // 发送 MQ 消息,异步执行发送短信 - if (isSend) { - mailProducer.sendMailSendMessage(sendLogId, mail, account.getId(), - template.getNickname(), title, content); + // 2. 组装邮箱 + String userMail = getUserMail(userId, userType); + Collection toMailSet = new LinkedHashSet<>(); + Collection ccMailSet = new LinkedHashSet<>(); + Collection bccMailSet = new LinkedHashSet<>(); + if (Validator.isEmail(userMail)) { + toMailSet.add(userMail); } - return sendLogId; - } - - @Override - public Long sendMultipleMailToAdmin(List toMails, List ccMails, List bccMails, Long userId, String templateCode, Map templateParams) { - // 如果 mail 为空,则加载用户编号对应的邮箱 - if (CollUtil.isEmpty(toMails)) { - AdminUserDO user = adminUserService.getUser(userId); - if (user != null) { - toMails = singletonList(user.getEmail()); - } - } - // 执行发送 - return sendMultipleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); - } - - @Override - public Long sendMultipleMailToMember(List toMails, List ccMails, List bccMails, Long userId, String templateCode, Map templateParams) { - // 如果 mail 为空,则加载用户编号对应的邮箱 - if (CollUtil.isEmpty(toMails)) { - toMails = singletonList(memberService.getMemberUserEmail(userId)); - } - // 执行发送 - return sendMultipleMail(toMails, ccMails, bccMails, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); - } - - @Override - public Long sendMultipleMail(List toMails, List ccMails, List bccMails, Long userId, Integer userType, String templateCode, Map templateParams) { - // 校验邮箱模版是否合法 - MailTemplateDO template = validateMailTemplate(templateCode); - // 校验邮箱账号是否合法 - MailAccountDO account = validateMailAccount(template.getAccountId()); - // 校验邮件参数是否缺失 - validateTemplateParams(template, templateParams); - - if (CollUtil.isEmpty(toMails)) { - throw exception(MAIL_SEND_MAIL_NOT_EXISTS); - } - // 校验邮箱是否存在 - for (String mail : toMails) { - validateMail(mail); + if (CollUtil.isNotEmpty(toMails)) { + toMails.stream().filter(Validator::isEmail).forEach(toMailSet::add); } if (CollUtil.isNotEmpty(ccMails)) { - for (String mail : ccMails) { - validateMail(mail); - } + ccMails.stream().filter(Validator::isEmail).forEach(ccMailSet::add); } if (CollUtil.isNotEmpty(bccMails)) { - for (String mail : bccMails) { - validateMail(mail); - } + bccMails.stream().filter(Validator::isEmail).forEach(bccMailSet::add); + } + if (CollUtil.isEmpty(toMailSet)) { + throw exception(MAIL_SEND_MAIL_NOT_EXISTS); } // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); - Long sendLogId = mailLogService.createMailLog(userId, userType, toMails, ccMails, bccMails, + Long sendLogId = mailLogService.createMailLog(userId, userType, toMailSet, ccMailSet, bccMailSet, account, template, content, templateParams, isSend); // 发送 MQ 消息,异步执行发送短信 if (isSend) { - mailProducer.sendMailSendMessage(sendLogId, toMails, ccMails, bccMails, account.getId(), - template.getNickname(), title, content); + mailProducer.sendMailSendMessage(sendLogId, toMailSet, ccMailSet, bccMailSet, + account.getId(), template.getNickname(), title, content); } return sendLogId; } + private String getUserMail(Long userId, Integer userType) { + if (userId == null || userType == null) { + return null; + } + if (UserTypeEnum.ADMIN.getValue().equals(userType)) { + AdminUserDO user = adminUserService.getUser(userId); + if (user != null) { + return user.getEmail(); + } + } + if (UserTypeEnum.MEMBER.getValue().equals(userType)) { + return memberService.getMemberUserEmail(userId); + } + return null; + } + @Override public void doSendMail(MailSendMessage message) { // 1. 创建发送账号 @@ -213,14 +162,6 @@ public class MailSendServiceImpl implements MailSendService { return account; } - @VisibleForTesting - String validateMail(String mail) { - if (StrUtil.isEmpty(mail)) { - throw exception(MAIL_SEND_MAIL_NOT_EXISTS); - } - return mail; - } - /** * 校验邮件参数是否缺失 * diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java index 482aa5aeee..ec8aadacd9 100755 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java +++ b/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; import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailTemplateDO; import cn.iocoder.yudao.module.system.dal.mysql.mail.MailLogMapper; import cn.iocoder.yudao.module.system.enums.mail.MailSendStatusEnum; +import org.assertj.core.util.Lists; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import jakarta.annotation.Resource; +import java.util.Collection; import java.util.Map; import static cn.hutool.core.util.RandomUtil.randomEle; @@ -43,7 +45,9 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { // 准备参数 Long userId = randomLongId(); Integer userType = randomEle(UserTypeEnum.values()).getValue(); - String toMail = randomEmail(); + Collection toMails = Lists.newArrayList(randomEmail(), randomEmail()); + Collection ccMails = Lists.newArrayList(randomEmail()); + Collection bccMails = Lists.newArrayList(randomEmail()); MailAccountDO account = randomPojo(MailAccountDO.class); MailTemplateDO template = randomPojo(MailTemplateDO.class); String templateContent = randomString(); @@ -52,14 +56,20 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { // mock 方法 // 调用 - Long logId = mailLogService.createMailLog(userId, userType, toMail, account, template, templateContent, templateParams, isSend); + Long logId = mailLogService.createMailLog(userId, userType, toMails, ccMails, bccMails, + account, template, templateContent, templateParams, isSend); // 断言 MailLogDO log = mailLogMapper.selectById(logId); assertNotNull(log); assertEquals(MailSendStatusEnum.INIT.getStatus(), log.getSendStatus()); assertEquals(userId, log.getUserId()); assertEquals(userType, log.getUserType()); - assertEquals(toMail, log.getToMail()); + assertEquals(toMails.size(), log.getToMails().size()); + assertTrue(log.getToMails().containsAll(toMails)); + assertEquals(ccMails.size(), log.getCcMails().size()); + assertTrue(log.getCcMails().containsAll(ccMails)); + assertEquals(bccMails.size(), log.getBccMails().size()); + assertTrue(log.getBccMails().containsAll(bccMails)); assertEquals(account.getId(), log.getAccountId()); assertEquals(account.getMail(), log.getFromMail()); assertEquals(template.getId(), log.getTemplateId()); @@ -136,7 +146,9 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> { // 等会查询到 o.setUserId(1L); o.setUserType(UserTypeEnum.ADMIN.getValue()); - o.setToMail("768@qq.com"); + o.setToMails(Lists.newArrayList("768@qq.com")); + o.setCcMails(Lists.newArrayList()); + o.setBccMails(Lists.newArrayList()); o.setAccountId(10L); o.setTemplateId(100L); o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); @@ -148,8 +160,8 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L))); // 测试 userType 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); - // 测试 toMail 不匹配 - mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMail("788@.qq.com"))); + // 测试 toMails 不匹配(特殊:find_in_set 无法单测) +// mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMails(Lists.newArrayList("788@qq.com")))); // 测试 accountId 不匹配 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L))); // 测试 templateId 不匹配 @@ -162,7 +174,7 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { MailLogPageReqVO reqVO = new MailLogPageReqVO(); reqVO.setUserId(1L); reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); - reqVO.setToMail("768"); +// reqVO.setToMail("768@qq.com"); reqVO.setAccountId(10L); reqVO.setTemplateId(100L); reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus()); diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java index 1ef1e12322..459568360d 100644 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java @@ -20,6 +20,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -65,14 +66,18 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { } @Test - public void testSendSingleMailToAdmin() { + public void testSendSingleMail_success() { // 准备参数 Long userId = randomLongId(); String templateCode = RandomUtils.randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); + Collection toMails = Lists.newArrayList("admin@test.com"); + Collection ccMails = Lists.newArrayList("cc@test.com"); + Collection bccMails = Lists.newArrayList("bcc@test.com"); + // mock adminUserService 的方法 - AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300")); + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setEmail("admin@example.com")); when(adminUserService.getUser(eq(userId))).thenReturn(user); // mock MailTemplateService 的方法 @@ -93,61 +98,27 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // mock MailLogService 的方法 Long mailLogId = randomLongId(); - when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(user.getEmail()), + when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), + argThat(toMailSet -> toMailSet.contains(user.getEmail()) && toMailSet.contains("admin@test.com")), + argThat(ccMailSet -> ccMailSet.contains("cc@test.com")), + argThat(bccMailSet -> bccMailSet.contains("bcc@test.com")), eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); // 调用 - Long resultMailLogId = mailSendService.sendSingleMailToAdmin(null, userId, templateCode, templateParams); + Long resultMailLogId = mailSendService.sendSingleMail(toMails, ccMails, bccMails, userId, + UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); // 断言 assertEquals(mailLogId, resultMailLogId); // 断言调用 - verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(user.getEmail()), - eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); - } - - @Test - public void testSendSingleMailToMember() { - // 准备参数 - Long userId = randomLongId(); - String templateCode = RandomUtils.randomString(); - Map templateParams = MapUtil.builder().put("code", "1234") - .put("op", "login").build(); - // mock memberService 的方法 - String mail = randomEmail(); - when(memberService.getMemberUserEmail(eq(userId))).thenReturn(mail); - - // mock MailTemplateService 的方法 - MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { - o.setStatus(CommonStatusEnum.ENABLE.getStatus()); - o.setContent("验证码为{code}, 操作为{op}"); - o.setParams(Lists.newArrayList("code", "op")); - }); - when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); - String title = RandomUtils.randomString(); - when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) - .thenReturn(title); - String content = RandomUtils.randomString(); - when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) - .thenReturn(content); - // mock MailAccountService 的方法 - MailAccountDO account = randomPojo(MailAccountDO.class); - when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); - // mock MailLogService 的方法 - Long mailLogId = randomLongId(); - when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(mail), - eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); - - // 调用 - Long resultMailLogId = mailSendService.sendSingleMailToMember(null, userId, templateCode, templateParams); - // 断言 - assertEquals(mailLogId, resultMailLogId); - // 断言调用 - verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), + verify(mailProducer).sendMailSendMessage(eq(mailLogId), + argThat(toMailSet -> toMailSet.contains(user.getEmail()) && toMailSet.contains("admin@test.com")), + argThat(ccMailSet -> ccMailSet.contains("cc@test.com")), + argThat(bccMailSet -> bccMailSet.contains("bcc@test.com")), eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); } /** - * 发送成功,当短信模板开启时 + * 发送成功,当邮件模板开启时 */ @Test public void testSendSingleMail_successWhenMailTemplateEnable() { @@ -158,6 +129,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { String templateCode = RandomUtils.randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); + Collection toMails = Lists.newArrayList(mail); + // mock MailTemplateService 的方法 MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.ENABLE.getStatus()); @@ -176,23 +149,29 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // mock MailLogService 的方法 Long mailLogId = randomLongId(); - when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), + when(mailLogService.createMailLog(eq(userId), eq(userType), + argThat(toMailSet -> toMailSet.contains(mail)), + argThat(Collection::isEmpty), + argThat(Collection::isEmpty), eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); // 调用 - Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); + Long resultMailLogId = mailSendService.sendSingleMail(toMails, null, null, userId, userType, templateCode, templateParams); // 断言 assertEquals(mailLogId, resultMailLogId); // 断言调用 - verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), + verify(mailProducer).sendMailSendMessage(eq(mailLogId), + argThat(toMailSet -> toMailSet.contains(mail)), + argThat(Collection::isEmpty), + argThat(Collection::isEmpty), eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); } /** - * 发送成功,当短信模板关闭时 + * 发送成功,当邮件模板关闭时 */ @Test - public void testSendSingleMail_successWhenSmsTemplateDisable() { + public void testSendSingleMail_successWhenMailTemplateDisable() { // 准备参数 String mail = randomEmail(); Long userId = randomLongId(); @@ -200,6 +179,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { String templateCode = RandomUtils.randomString(); Map templateParams = MapUtil.builder().put("code", "1234") .put("op", "login").build(); + Collection toMails = Lists.newArrayList(mail); + // mock MailTemplateService 的方法 MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { o.setStatus(CommonStatusEnum.DISABLE.getStatus()); @@ -218,15 +199,18 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // mock MailLogService 的方法 Long mailLogId = randomLongId(); - when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), + when(mailLogService.createMailLog(eq(userId), eq(userType), + argThat(toMailSet -> toMailSet.contains(mail)), + argThat(Collection::isEmpty), + argThat(Collection::isEmpty), eq(account), eq(template), eq(content), eq(templateParams), eq(false))).thenReturn(mailLogId); // 调用 - Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); + Long resultMailLogId = mailSendService.sendSingleMail(toMails, null, null, userId, userType, templateCode, templateParams); // 断言 assertEquals(mailLogId, resultMailLogId); // 断言调用 - verify(mailProducer, times(0)).sendMailSendMessage(anyLong(), anyString(), + verify(mailProducer, times(0)).sendMailSendMessage(anyLong(), any(), any(), any(), anyLong(), anyString(), anyString(), anyString()); } @@ -255,12 +239,29 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { } @Test - public void testValidateMail_notExists() { + public void testSendSingleMail_noValidEmail() { // 准备参数 - // mock 方法 + Long userId = randomLongId(); + String templateCode = RandomUtils.randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + Collection toMails = Lists.newArrayList("invalid-email"); // 非法邮箱 + + // mock MailTemplateService 的方法 + MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + + // mock MailAccountService 的方法 + MailAccountDO account = randomPojo(MailAccountDO.class); + when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); // 调用,并断言异常 - assertServiceException(() -> mailSendService.validateMail(null), + assertServiceException(() -> mailSendService.sendSingleMail(toMails, null, null, userId, + UserTypeEnum.ADMIN.getValue(), templateCode, templateParams), MAIL_SEND_MAIL_NOT_EXISTS); } @@ -286,7 +287,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { assertEquals(account.getPort(), mailAccount.getPort()); assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); return true; - }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))) + }), eq(message.getToMails()), eq(message.getCcMails()), eq(message.getBccMails()), + eq(message.getTitle()), eq(message.getContent()), eq(true))) .thenReturn(messageId); // 调用 @@ -317,7 +319,8 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { assertEquals(account.getPort(), mailAccount.getPort()); assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); return true; - }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))).thenThrow(e); + }), eq(message.getToMails()), eq(message.getCcMails()), eq(message.getBccMails()), + eq(message.getTitle()), eq(message.getContent()), eq(true))).thenThrow(e); // 调用 mailSendService.doSendMail(message); diff --git a/yudao-module-system/src/test/resources/sql/create_tables.sql b/yudao-module-system/src/test/resources/sql/create_tables.sql index 4df039b8df..d8b68369aa 100644 --- a/yudao-module-system/src/test/resources/sql/create_tables.sql +++ b/yudao-module-system/src/test/resources/sql/create_tables.sql @@ -553,7 +553,9 @@ CREATE TABLE IF NOT EXISTS "system_mail_log" ( "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "user_id" bigint, "user_type" varchar, - "to_mail" varchar NOT NULL, + "to_mails" varchar NOT NULL, + "cc_mails" varchar, + "bcc_mails" varchar, "account_id" bigint NOT NULL, "from_mail" varchar NOT NULL, "template_id" bigint NOT NULL, From d4357a747174f3c78603280ab5b7dd4ef9d52cae Mon Sep 17 00:00:00 2001 From: LesanOuO <1960681385@qq.com> Date: Wed, 6 Aug 2025 19:29:14 +0800 Subject: [PATCH 06/11] =?UTF-8?q?chore:=20=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/bpm/service/task/BpmTaskServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 133f05c76b..f53d86d515 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -1204,7 +1204,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (CollUtil.isEmpty(nextUserTaskKeys)) { throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); } - // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以;包括 todo 和 done 那边的查询哇??? + // TODO @芋艿:是否选择升级flowable版本解决taskCreatedAfter、taskCreatedBefore问题,升级7.1.0可以;包括 todo 和 done 那边的查询哇??? 是的! long nextUserTaskFinishedCount = historyService.createHistoricTaskInstanceQuery() .processInstanceId(processInstance.getProcessInstanceId()).taskDefinitionKeys(nextUserTaskKeys) .taskCreatedAfter(taskInstance.getEndTime()).finished().count(); From 0004f1f6fbdd9e0b4810e99c1c73021d49e46c85 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 9 Aug 2025 10:04:07 +0800 Subject: [PATCH 07/11] =?UTF-8?q?fix=EF=BC=9A=E3=80=90crm=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E3=80=91=E4=BF=AE=E5=A4=8D=E7=BA=BF?= =?UTF-8?q?=E7=B4=A2=E6=B1=A0=E6=93=8D=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=A9=BA=E7=9A=84=E9=97=AE=E9=A2=98=20!1397?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java | 2 +- .../yudao/module/crm/service/clue/CrmClueServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java index aeeed316dd..30d51cf472 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java @@ -14,7 +14,7 @@ public interface LogRecordConstants { String CRM_CLUE_CREATE_SUB_TYPE = "创建线索"; String CRM_CLUE_CREATE_SUCCESS = "创建了线索{{#clue.name}}"; String CRM_CLUE_UPDATE_SUB_TYPE = "更新线索"; - String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReq}}"; + String CRM_CLUE_UPDATE_SUCCESS = "更新了线索【{{#clueName}}】: {_DIFF{#updateReqVO}}"; String CRM_CLUE_DELETE_SUB_TYPE = "删除线索"; String CRM_CLUE_DELETE_SUCCESS = "删除了线索【{{#clueName}}】"; String CRM_CLUE_TRANSFER_SUB_TYPE = "转移线索"; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java index 0250fe14f5..fe84db1bdc 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java @@ -106,7 +106,7 @@ public class CrmClueServiceImpl implements CrmClueService { // 3. 记录操作日志上下文 updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class)); + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmClueSaveReqVO.class)); LogRecordContext.putVariable("clueName", oldClue.getName()); } From 3ca2981af34a4eb0884e8728beba9c967c2c4961 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 9 Aug 2025 10:14:49 +0800 Subject: [PATCH 08/11] =?UTF-8?q?fix=EF=BC=9A=E3=80=90system=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8A=9F=E8=83=BD=E3=80=91=E4=BF=AE=E5=A4=8D=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=BB=84=E4=BB=B6=E5=90=8D=E9=87=8D=E5=A4=8D=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E4=B8=8D=E4=B8=A5=E8=B0=A8=E9=97=AE=E9=A2=98=20!201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/system/service/permission/MenuServiceImpl.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index 0d7536a1a7..80832e969f 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -255,9 +255,6 @@ public class MenuServiceImpl implements MenuService { return; } // 如果 id 为空,说明不用比较是否为相同 id 的菜单 - if (id == null) { - throw exception(MENU_NAME_DUPLICATE); - } if (!menu.getId().equals(id)) { throw exception(MENU_NAME_DUPLICATE); } From dfc92c94509436416a3912709e34604f4c5ece89 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 9 Aug 2025 10:17:31 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix=EF=BC=9A=E3=80=90system=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8A=9F=E8=83=BD=E3=80=91=E4=BF=AE=E5=A4=8D=E9=98=BF?= =?UTF-8?q?=E9=87=8C=E4=BA=91sms=20api=E7=BC=BA=E5=B0=91=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=20!159?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sms/core/client/impl/AliyunSmsClient.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java index 3dd12491a9..d4ed95d9b2 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java +++ b/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 { .map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))) .collect(Collectors.joining("&")); - // 2.1 请求 Header + // 2. 请求 Body + String requestBody = ""; // 短信 API 为 RPC 接口,query parameters 在 uri 中拼接,因此 request body 如果没有特殊要求,设置为空 + String hashedRequestPayload = DigestUtil.sha256Hex(requestBody); + + // 3.1 请求 Header TreeMap headers = new TreeMap<>(); headers.put("host", HOST); headers.put("x-acs-version", VERSION); headers.put("x-acs-action", apiName); headers.put("x-acs-date", FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT")).format(new Date())); headers.put("x-acs-signature-nonce", IdUtil.randomUUID()); + headers.put("x-acs-content-sha256", hashedRequestPayload); - // 2.2 构建签名 Header + // 3.2 构建签名 Header StringBuilder canonicalHeaders = new StringBuilder(); // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起 StringBuilder signedHeadersBuilder = new StringBuilder(); // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔 headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") @@ -157,13 +162,13 @@ public class AliyunSmsClient extends AbstractSmsClient { }); String signedHeaders = signedHeadersBuilder.substring(0, signedHeadersBuilder.length() - 1); - // 3. 请求 Body - String requestBody = ""; // 短信 API 为 RPC 接口,query parameters 在 uri 中拼接,因此 request body 如果没有特殊要求,设置为空。 - String hashedRequestBody = DigestUtil.sha256Hex(requestBody); - // 4. 构建 Authorization 签名 - String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" - + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; + String canonicalRequest = "POST" + "\n" + + "/" + "\n" + + queryString + "\n" + + canonicalHeaders + "\n" + + signedHeaders + "\n" + + hashedRequestPayload; String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest); String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest; String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名 @@ -184,7 +189,7 @@ public class AliyunSmsClient extends AbstractSmsClient { @SneakyThrows private static String percentCode(String str) { Assert.notNull(str, "str 不能为空"); - return HttpUtils.encodeUtf8(str) + return URLEncoder.encode(str, StandardCharsets.UTF_8.name()) .replace("+", "%20") // 加号 "+" 被替换为 "%20" .replace("*", "%2A") // 星号 "*" 被替换为 "%2A" .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" From 784a9964bac2b7a73aac2905019e4d515b4f80d9 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 9 Aug 2025 10:22:05 +0800 Subject: [PATCH 10/11] =?UTF-8?q?feat:=E3=80=90mp=20=E5=85=AC=E4=BC=97?= =?UTF-8?q?=E5=8F=B7=E3=80=91=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=20ID=20?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E6=9D=A1=E4=BB=B6=E4=BB=A5=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/message/vo/message/MpMessagePageReqVO.java | 3 +++ .../yudao/module/mp/dal/mysql/message/MpMessageMapper.java | 1 + .../pay/core/client/impl/alipay/AbstractAlipayPayClient.java | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java index d9f7cc8761..9e01a5c338 100644 --- a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java +++ b/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 { @Schema(description = "公众号粉丝标识", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") private String openid; + @Schema(description = "公众号粉丝 UserId", example = "1") + private String userId; + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) @Schema(description = "创建时间") private LocalDateTime[] createTime; diff --git a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java index 72ba566277..0f11bdfe55 100644 --- a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java +++ b/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 { .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) .eqIfPresent(MpMessageDO::getType, reqVO.getType()) .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenid()) + .eqIfPresent(MpMessageDO::getUserId, reqVO.getUserId()) .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime()) .orderByDesc(MpMessageDO::getId)); } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index fee7cddba8..caf210fa66 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/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 Date: Sat, 16 Aug 2025 15:57:47 +0800 Subject: [PATCH 11/11] =?UTF-8?q?feat=EF=BC=9A=E3=80=90framework=20?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E3=80=91=E5=A2=9E=E5=8A=A0=20api=20=E5=8A=A0?= =?UTF-8?q?=E8=A7=A3=E5=AF=86=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/enums/WebFilterOrderEnum.java | 2 + .../encrypt/config/ApiEncryptProperties.java | 70 ++++++++ .../YudaoApiEncryptAutoConfiguration.java | 34 ++++ .../encrypt/core/annotation/ApiEncrypt.java | 23 +++ .../core/filter/ApiDecryptRequestWrapper.java | 86 ++++++++++ .../encrypt/core/filter/ApiEncryptFilter.java | 152 ++++++++++++++++++ .../filter/ApiEncryptResponseWrapper.java | 109 +++++++++++++ .../yudao/framework/encrypt/package-info.java | 4 + .../core/filter/CacheRequestBodyWrapper.java | 17 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../framework/encrypt/ApiEncryptTest.java | 86 ++++++++++ .../controller/admin/auth/AuthController.http | 18 +++ .../src/main/resources/application.yaml | 7 + 13 files changed, 606 insertions(+), 5 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java create mode 100644 yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java index 11a5ee0782..497a213a2f 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java @@ -15,6 +15,8 @@ public interface WebFilterOrderEnum { int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; + int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1; + // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java new file mode 100644 index 0000000000..135eb85bb0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/ApiEncryptProperties.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.framework.encrypt.config; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * HTTP API 加解密配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.api-encrypt") +@Validated +@Data +public class ApiEncryptProperties { + + /** + * 是否开启 + */ + @NotNull(message = "是否开启不能为空") + private Boolean enable; + + /** + * 请求头(响应头)名称 + * + * 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密 + * 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密 + */ + @NotEmpty(message = "请求头(响应头)名称不能为空") + private String header = "X-Api-Encrypt"; + + /** + * 对称加密算法,用于请求/响应的加解密 + * + * 目前支持 + * 【对称加密】: + * 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES} + * 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低) + * 【非对称加密】 + * 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA} + * 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低) + * + * @see 什么是公钥和私钥? + */ + @NotEmpty(message = "对称加密算法不能为空") + private String algorithm; + + /** + * 请求的解密密钥 + * + * 注意: + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 + * 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!) + */ + @NotEmpty(message = "请求的解密密钥不能为空") + private String requestKey; + + /** + * 响应的加密密钥 + * + * 注意: + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 + * 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!) + */ + @NotEmpty(message = "响应的加密密钥不能为空") + private String responseKey; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java new file mode 100644 index 0000000000..03d0f1ac12 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/config/YudaoApiEncryptAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.encrypt.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.encrypt.core.filter.ApiEncryptFilter; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import static cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration.createFilterBean; + +@AutoConfiguration +@Slf4j +@EnableConfigurationProperties(ApiEncryptProperties.class) +@ConditionalOnProperty(prefix = "yudao.api-encrypt", name = "enable", havingValue = "true") +public class YudaoApiEncryptAutoConfiguration { + + @Bean + public FilterRegistrationBean apiEncryptFilter(WebProperties webProperties, + ApiEncryptProperties apiEncryptProperties, + RequestMappingHandlerMapping requestMappingHandlerMapping, + GlobalExceptionHandler globalExceptionHandler) { + ApiEncryptFilter filter = new ApiEncryptFilter(webProperties, apiEncryptProperties, + requestMappingHandlerMapping, globalExceptionHandler); + return createFilterBean(filter, WebFilterOrderEnum.API_ENCRYPT_FILTER); + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java new file mode 100644 index 0000000000..7405111038 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/annotation/ApiEncrypt.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.framework.encrypt.core.annotation; + +import java.lang.annotation.*; + +/** + * HTTP API 加解密注解 + */ +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiEncrypt { + + /** + * 是否对请求参数进行解密,默认 true + */ + boolean request() default true; + + /** + * 是否对响应结果进行加密,默认 true + */ + boolean response() default true; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java new file mode 100644 index 0000000000..b9f015a7ec --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiDecryptRequestWrapper.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.framework.encrypt.core.filter; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.symmetric.SymmetricDecryptor; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 解密请求 {@link HttpServletRequestWrapper} 实现类 + * + * @author 芋道源码 + */ +public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public ApiDecryptRequestWrapper(HttpServletRequest request, + SymmetricDecryptor symmetricDecryptor, + AsymmetricDecryptor asymmetricDecryptor) throws IOException { + super(request); + // 读取 body,允许 HEX、BASE64 传输 + String requestBody = StrUtil.utf8Str( + IoUtil.readBytes(request.getInputStream(), false)); + + // 解密 body + body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody) + : asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream stream = new ByteArrayInputStream(body); + return new ServletInputStream() { + + @Override + public int read() { + return stream.read(); + } + + @Override + public int available() { + return body.length; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + }; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java new file mode 100644 index 0000000000..e6d03ba32b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptFilter.java @@ -0,0 +1,152 @@ +package cn.iocoder.yudao.framework.encrypt.core.filter; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; +import cn.hutool.crypto.symmetric.SymmetricDecryptor; +import cn.hutool.crypto.symmetric.SymmetricEncryptor; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties; +import cn.iocoder.yudao.framework.encrypt.core.annotation.ApiEncrypt; +import cn.iocoder.yudao.framework.web.config.WebProperties; +import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; +import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.io.IOException; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +/** + * API 加密过滤器,处理 {@link ApiEncrypt} 注解。 + * + * 1. 解密请求参数 + * 2. 加密响应结果 + * + * 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢? + * 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!! + * + * @author 芋道源码 + */ +@Slf4j +public class ApiEncryptFilter extends ApiRequestFilter { + + private final ApiEncryptProperties apiEncryptProperties; + + private final RequestMappingHandlerMapping requestMappingHandlerMapping; + + private final GlobalExceptionHandler globalExceptionHandler; + + private final SymmetricDecryptor requestSymmetricDecryptor; + private final AsymmetricDecryptor requestAsymmetricDecryptor; + + private final SymmetricEncryptor responseSymmetricEncryptor; + private final AsymmetricEncryptor responseAsymmetricEncryptor; + + public ApiEncryptFilter(WebProperties webProperties, + ApiEncryptProperties apiEncryptProperties, + RequestMappingHandlerMapping requestMappingHandlerMapping, + GlobalExceptionHandler globalExceptionHandler) { + super(webProperties); + this.apiEncryptProperties = apiEncryptProperties; + this.requestMappingHandlerMapping = requestMappingHandlerMapping; + this.globalExceptionHandler = globalExceptionHandler; + if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) { + this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey())); + this.requestAsymmetricDecryptor = null; + this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey())); + this.responseAsymmetricEncryptor = null; + } else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) { + this.requestSymmetricDecryptor = null; + this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null); + this.responseSymmetricEncryptor = null; + this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey()); + } else { + // 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。 + throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm()); + } + } + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 获取 @ApiEncrypt 注解 + ApiEncrypt apiEncrypt = getApiEncrypt(request); + boolean requestEnable = apiEncrypt != null && apiEncrypt.request(); + boolean responseEnable = apiEncrypt != null && apiEncrypt.response(); + String encryptHeader = request.getHeader(apiEncryptProperties.getHeader()); + if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) { + chain.doFilter(request, response); + return; + } + + // 1. 解密请求 + if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()), + HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) { + try { + if (StrUtil.isNotBlank(encryptHeader)) { + request = new ApiDecryptRequestWrapper(request, + requestSymmetricDecryptor, requestAsymmetricDecryptor); + } else if (requestEnable) { + throw invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头"); + } + } catch (Exception ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } + + // 2. 执行过滤器链 + if (responseEnable) { + // 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!! + response = new ApiEncryptResponseWrapper(response); + } + chain.doFilter(request, response); + + // 3. 加密响应(真正执行) + if (responseEnable) { + ((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties, + responseSymmetricEncryptor, responseAsymmetricEncryptor); + } + } + + /** + * 获取 @ApiEncrypt 注解 + * + * @param request 请求 + */ + private ApiEncrypt getApiEncrypt(HttpServletRequest request) { + try { + HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request); + if (mappingHandler == null) { + return null; + } + Object handler = mappingHandler.getHandler(); + if (handler instanceof HandlerMethod handlerMethod) { + ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class); + if (annotation == null) { + annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class); + } + return annotation; + } + } catch (Exception e) { + log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]", + request.getRequestURI(), request.getMethod(), e); + } + return null; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java new file mode 100644 index 0000000000..fed38917b9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/core/filter/ApiEncryptResponseWrapper.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.framework.encrypt.core.filter; + +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.symmetric.SymmetricEncryptor; +import cn.iocoder.yudao.framework.encrypt.config.ApiEncryptProperties; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +/** + * 加密响应 {@link HttpServletResponseWrapper} 实现类 + * + * @author 芋道源码 + */ +public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper { + + private final ByteArrayOutputStream byteArrayOutputStream; + private final ServletOutputStream servletOutputStream; + private final PrintWriter printWriter; + + public ApiEncryptResponseWrapper(HttpServletResponse response) { + super(response); + this.byteArrayOutputStream = new ByteArrayOutputStream(); + this.servletOutputStream = this.getOutputStream(); + this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream)); + } + + public void encrypt(ApiEncryptProperties properties, + SymmetricEncryptor symmetricEncryptor, + AsymmetricEncryptor asymmetricEncryptor) throws IOException { + // 1.1 清空 body + HttpServletResponse response = (HttpServletResponse) this.getResponse(); + response.resetBuffer(); + // 1.2 获取 body + this.flushBuffer(); + byte[] body = byteArrayOutputStream.toByteArray(); + + // 2. 加密 body + String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body) + : asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey); + response.getWriter().write(encryptedBody); + + // 3. 添加加密 header 标识 + this.addHeader(properties.getHeader(), "true"); + // 特殊:特殊:https://juejin.cn/post/6867327674675625992 + this.addHeader("Access-Control-Expose-Headers", properties.getHeader()); + } + + @Override + public PrintWriter getWriter() { + return printWriter; + } + + @Override + public void flushBuffer() throws IOException { + if (servletOutputStream != null) { + servletOutputStream.flush(); + } + if (printWriter != null) { + printWriter.flush(); + } + } + + @Override + public void reset() { + byteArrayOutputStream.reset(); + } + + @Override + public ServletOutputStream getOutputStream() { + return new ServletOutputStream() { + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + } + + @Override + public void write(int b) { + byteArrayOutputStream.write(b); + } + + @Override + @SuppressWarnings("NullableProblems") + public void write(byte[] b) throws IOException { + byteArrayOutputStream.write(b); + } + + @Override + @SuppressWarnings("NullableProblems") + public void write(byte[] b, int off, int len) { + byteArrayOutputStream.write(b, off, len); + } + + }; + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java new file mode 100644 index 0000000000..ca08197125 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/encrypt/package-info.java @@ -0,0 +1,4 @@ +/** + * HTTP API 加密组件:支持 Request 和 Response 的加密、解密 + */ +package cn.iocoder.yudao.framework.encrypt; \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java index 8e80fa591f..b5f38d96fd 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -1,14 +1,13 @@ package cn.iocoder.yudao.framework.web.core.filter; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; - import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; + import java.io.BufferedReader; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStreamReader; /** @@ -29,12 +28,22 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { } @Override - public BufferedReader getReader() throws IOException { + public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(this.getInputStream())); } @Override - public ServletInputStream getInputStream() throws IOException { + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public ServletInputStream getInputStream() { final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); // 返回 ServletInputStream return new ServletInputStream() { diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 9cdcd09c4e..07a7955c34 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/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 cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration cn.iocoder.yudao.framework.xss.config.YudaoXssAutoConfiguration -cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration \ No newline at end of file +cn.iocoder.yudao.framework.banner.config.YudaoBannerAutoConfiguration +cn.iocoder.yudao.framework.encrypt.config.YudaoApiEncryptAutoConfiguration \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java new file mode 100644 index 0000000000..12d406e5f5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/encrypt/ApiEncryptTest.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.framework.encrypt; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +/** + * 各种 API 加解密的测试类:不是单测,而是方便大家生成密钥、加密、解密等操作。 + * + * @author 芋道源码 + */ +@SuppressWarnings("ConstantValue") +public class ApiEncryptTest { + + @Test + public void testGenerateAsymmetric() { + String asymmetricAlgorithm = AsymmetricAlgorithm.RSA.getValue(); +// String asymmetricAlgorithm = "SM2"; +// String asymmetricAlgorithm = SM4.ALGORITHM_NAME; +// String asymmetricAlgorithm = SymmetricAlgorithm.AES.getValue(); + String requestClientKey = null; + String requestServerKey = null; + String responseClientKey = null; + String responseServerKey = null; + if (Objects.equals(asymmetricAlgorithm, AsymmetricAlgorithm.RSA.getValue())) { + // 请求的密钥 + RSA requestRsa = SecureUtil.rsa(); + requestClientKey = requestRsa.getPublicKeyBase64(); + requestServerKey = requestRsa.getPrivateKeyBase64(); + // 响应的密钥 + RSA responseRsa = new RSA(); + responseClientKey = responseRsa.getPrivateKeyBase64(); + responseServerKey = responseRsa.getPublicKeyBase64(); + } else if (Objects.equals(asymmetricAlgorithm, SymmetricAlgorithm.AES.getValue())) { + // AES 密钥可选 32、24、16 位 + // 请求的密钥(前后端密钥一致) + requestClientKey = RandomUtil.randomNumbers(32); + requestServerKey = requestClientKey; + // 响应的密钥(前后端密钥一致) + responseClientKey = RandomUtil.randomNumbers(32); + responseServerKey = responseClientKey; + } + + // 打印结果 + System.out.println("requestClientKey = " + requestClientKey); + System.out.println("requestServerKey = " + requestServerKey); + System.out.println("responseClientKey = " + responseClientKey); + System.out.println("responseServerKey = " + responseServerKey); + } + + @Test + public void testEncrypt_aes() { + String key = "52549111389893486934626385991395"; + String body = "{\n" + + " \"username\": \"admin\",\n" + + " \"password\": \"admin123\",\n" + + " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" + + " \"code\": \"1024\"\n" + + "}"; + String encrypt = SecureUtil.aes(StrUtil.utf8Bytes(key)) + .encryptBase64(body); + System.out.println("encrypt = " + encrypt); + } + + @Test + public void testEncrypt_rsa() { + String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB"; + String body = "{\n" + + " \"username\": \"admin\",\n" + + " \"password\": \"admin123\",\n" + + " \"uuid\": \"3acd87a09a4f48fb9118333780e94883\",\n" + + " \"code\": \"1024\"\n" + + "}"; + String encrypt = SecureUtil.rsa(null, key) + .encryptBase64(body, KeyType.PublicKey); + System.out.println("encrypt = " + encrypt); + } + +} diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http index f42dfcd030..52a724bf35 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http @@ -11,6 +11,24 @@ tag: Yunai.local "code": "1024" } +### 请求 /login 接口【加密 AES】 => 成功 +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenantId}} +tag: Yunai.local +X-API-ENCRYPT: true + +WvSX9MOrenyGfBhEM0g1/hHgq8ocktMZ9OwAJ6MOG5FUrzYF/rG5JF1eMptQM1wT73VgDS05l/37WeRtad+JrqChAul/sR/SdOsUKqjBhvvQx1JVhzxr6s8uUP67aKTSZ6Psv7O32ELxXrzSaQvG5CInzz3w6sLtbNNLd1kXe6Q= + +### 请求 /login 接口【加密 RSA】 => 成功 +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenantId}} +tag: Yunai.local +X-API-ENCRYPT: true + +e7QZTork9ZV5CmgZvSd+cHZk3xdUxKtowLM02kOha+gxHK2H/daU8nVBYS3+bwuDRy5abf+Pz1QJJGVAEd27wwrXBmupOOA/bhpuzzDwcRuJRD+z+YgiNoEXFDRHERxPYlPqAe9zAHtihD0ceub1AjybQsEsROew4C3Q602XYW0= + ### 请求 /login 接口 => 成功(无验证码) POST {{baseUrl}}/system/auth/login Content-Type: application/json diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index d82d3974a5..1decd79cda 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -245,6 +245,13 @@ yudao: security: permit-all_urls: - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 + api-encrypt: + enable: true # 是否开启 API 加密 + algorithm: AES # 加密算法,支持 AES、RSA 等 + request-key: 52549111389893486934626385991395 # 【AES 案例】请求加密的秘钥,,必须 16、24、32 位 + response-key: 96103715984234343991809655248883 # 【AES 案例】响应加密的秘钥,AES 案例,必须 16、24、32 位 +# 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 案例】请求解密的私钥 +# response-key: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDh/CHyBcS/zEfVyINVA7+c9Xxl0CPdxPMK1OIjxaLy/7BLfbkoEpI8onQtjuzfpuxCraDem9bu3BMF0pMH95HytI3Vi0kGjaV+WLIalwgc2w37oA2sbsmKzQOP7SDLO5s2QJNAD7kVwd+Q5rqaLu2MO0xVv+0IUJhn83hClC0L5wIDAQAB # 【RSA 案例】响应加密的公钥 websocket: enable: true # websocket的开关 path: /infra/ws # 路径