From 19ca4e7e89dd553da14bcaf2a1e89d1718e40339 Mon Sep 17 00:00:00 2001 From: zhouhongzhi Date: Wed, 30 Jul 2025 14:20:13 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix=20=E4=BF=AE=E5=A4=8D=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=90=8D=E9=87=8D=E5=A4=8D=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E4=B8=8D=E4=B8=A5=E8=B0=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/system/service/permission/MenuServiceImpl.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index 0d7536a1a..8cb752229 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -278,10 +278,6 @@ public class MenuServiceImpl implements MenuService { if (menu == null) { return; } - // 如果 id 为空,说明不用比较是否为相同 id 的菜单 - if (id == null) { - return; - } if (!menu.getId().equals(id)) { throw exception(MENU_COMPONENT_NAME_DUPLICATE); } From 2382c3d844cc4e5bca1c61086fb75bcdef44a373 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 4 Aug 2025 13:01:02 +0800 Subject: [PATCH 2/8] =?UTF-8?q?reactor=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=BC=98=E5=8C=96=E2=80=9C?= =?UTF-8?q?=E6=96=87=E5=AD=97=E9=AA=8C=E8=AF=81=E7=A0=81=E2=80=9D=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/PictureWordCaptchaServiceImpl.java | 212 ++++++++++++++++++ .../com.anji.captcha.service.CaptchaService | 1 + .../src/main/resources/application.yaml | 2 +- .../src/main/resources/application.yaml | 2 +- 4 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java create mode 100644 yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java new file mode 100644 index 000000000..a9e3d919b --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java @@ -0,0 +1,212 @@ +package cn.iocoder.yudao.module.system.framework.captcha.core; + +import cn.hutool.core.util.RandomUtil; +import com.anji.captcha.model.common.RepCodeEnum; +import com.anji.captcha.model.common.ResponseModel; +import com.anji.captcha.model.vo.CaptchaVO; +import com.anji.captcha.service.impl.AbstractCaptchaService; +import com.anji.captcha.service.impl.CaptchaServiceFactory; +import com.anji.captcha.util.AESUtil; +import com.anji.captcha.util.ImageUtils; +import com.anji.captcha.util.RandomUtils; +import org.apache.commons.lang3.StringUtils; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.util.Properties; + +/** + * 图片文字验证码 + * + * @author Tsui + * @since 2025/7/23 20:44 + */ +public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService { + + /** + * 验证码的基础字符 + */ + private static final String CHARACTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + /** + * 验证码长度 + */ + private static final Integer LENGTH = 4; + + private static final int WIDTH = 120; + private static final int HEIGHT = 40; + private static final int LINES = 10; + + @Override + public void init(Properties config) { + super.init(config); + } + + @Override + public void destroy(Properties config) { + logger.info("start-clear-history-data-{}", captchaType()); + } + + @Override + public String captchaType() { + return "pictureWord"; + } + + @Override + public ResponseModel get(CaptchaVO captchaVO) { + String text = generateRandomText(LENGTH); + CaptchaVO imageData = getImageData(text); + // pointJson 不传到前端,只做后端校验,测试时放开 +// imageData.setPointJson(text); + return ResponseModel.successData(imageData); + } + + @Override + public ResponseModel check(CaptchaVO captchaVO) { + ResponseModel r = super.check(captchaVO); + if (!validatedReq(r)) { + return r; + } + + // 取出验证码 + String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken()); + if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) { + return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID); + } + // 正确的验证码 + String codeValue = CaptchaServiceFactory.getCache(cacheType).get(codeKey); + String code = getCodeByCodeValue(codeValue); + String secretKey = getSecretKeyByCodeValue(codeValue); + // 验证码只用一次,即刻失效 + CaptchaServiceFactory.getCache(cacheType).delete(codeKey); + + // 用户输入的验证码(CaptchaVO 中 没有预留字段,暂时用 pointJson 无需加解密) + String userCode = captchaVO.getPointJson(); + if (!StringUtils.equalsIgnoreCase(code, userCode)) { + afterValidateFail(captchaVO); + return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR); + } + + // 校验成功,将信息存入缓存 + String value; + try { + value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(userCode), secretKey); + } catch (Exception e) { + logger.error("AES 加密失败", e); + afterValidateFail(captchaVO); + return ResponseModel.errorMsg(e.getMessage()); + } + String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value); + CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE); + captchaVO.setResult(true); + captchaVO.resetClientFlag(); + return ResponseModel.successData(captchaVO); + } + + @Override + public ResponseModel verification(CaptchaVO captchaVO) { + ResponseModel r = super.verification(captchaVO); + if (!validatedReq(r)) { + return r; + } + try { + String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification()); + if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) { + return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID); + } + // 二次校验取值后,即刻失效 + CaptchaServiceFactory.getCache(cacheType).delete(codeKey); + } catch (Exception e) { + logger.error("验证码解析失败", e); + return ResponseModel.errorMsg(e.getMessage()); + } + return ResponseModel.success(); + } + + + private CaptchaVO getImageData(String text) { + CaptchaVO dataVO = new CaptchaVO(); + BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + + // 设置背景色 + g.setColor(getRandomColor(200, 250)); + g.fillRect(0, 0, WIDTH, HEIGHT); + // 绘制干扰线 + for (int i = 0; i < LINES; i++) { + g.setColor(getRandomColor(100, 200)); + int x1 = RandomUtil.randomInt(WIDTH); + int y1 = RandomUtil.randomInt(HEIGHT); + int x2 = RandomUtil.randomInt(WIDTH); + int y2 = RandomUtil.randomInt(HEIGHT); + g.drawLine(x1, y1, x2, y2); + } + // 设置字体 + g.setFont(new Font("Arial", Font.BOLD, 24)); + // 绘制验证码文本 + for (int i = 0; i < text.length(); i++) { + g.setColor(getRandomColor(20, 130)); + // 文字旋转 + AffineTransform affineTransform = new AffineTransform(); + int x = 20 + i * 20; + int y = 24 + RandomUtil.randomInt(8); + // 旋转范围 -45 ~ 45 + affineTransform.setToRotation(Math.toRadians(RandomUtil.randomInt(-45, 45)), x, y); + g.setTransform(affineTransform); + g.drawString(text.charAt(i) + "", x, y); + } + // 添加噪点 + for (int i = 0; i < 100; i++) { + int x = RandomUtil.randomInt(WIDTH); + int y = RandomUtil.randomInt(HEIGHT); + image.setRGB(x, y, getRandomColor(0, 255).getRGB()); + } + g.dispose(); + + String secretKey = null; + if (captchaAesStatus) { + secretKey = AESUtil.getKey(); + } + dataVO.setSecretKey(secretKey); + + dataVO.setOriginalImageBase64(ImageUtils.getImageToBase64Str(image).replaceAll("\r|\n", "")); + dataVO.setToken(RandomUtils.getUUID()); +// dataVO.setSecretKey(secretKey); + // 将坐标信息存入 redis 中 + String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken()); + CaptchaServiceFactory.getCache(cacheType).set(codeKey, getCodeValue(text, secretKey), EXPIRESIN_SECONDS); + return dataVO; + } + + private String getCodeValue(String text, String secretKey) { + return text + "," + secretKey; + } + + private String getCodeByCodeValue(String codeValue) { + return codeValue.split(",")[0]; + } + + private String getSecretKeyByCodeValue(String codeValue) { + return codeValue.split(",")[1]; + } + + private Color getRandomColor(int min, int max) { + int minVal = Math.min(min, max); + int maxVal = Math.max(min, max); + int r = RandomUtil.randomInt(minVal, maxVal); + int g = RandomUtil.randomInt(minVal, maxVal); + int b = RandomUtil.randomInt(minVal, maxVal); + return new Color(r, g, b); + } + + /** + * 生成指定长度的随机字符串 + * + * @param length 长度 + * @return {@link String} + */ + public static String generateRandomText(int length) { + return RandomUtil.randomString(CHARACTERS, length); + } + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService b/yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService new file mode 100644 index 000000000..80adf6ddb --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService @@ -0,0 +1 @@ +cn.iocoder.yudao.module.system.framework.captcha.core.PictureWordCaptchaServiceImpl \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml b/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml index 396447ff9..2d90ccfac 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml @@ -135,7 +135,7 @@ aj: cache-type: redis # 缓存 local/redis... cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 - type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 + type: blockPuzzle # 验证码类型 default 三种都实例化。blockPuzzle 滑块拼图、clickWord 文字点选、pictureWord 文本输入 water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode interference-options: 0 # 滑动干扰项(0/1/2) req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 4eeb96f9d..e7e87bd06 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -131,7 +131,7 @@ aj: cache-type: redis # 缓存 local/redis... cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 - type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 + type: blockPuzzle # 验证码类型 default 三种都实例化。blockPuzzle 滑块拼图、clickWord 文字点选、pictureWord 文本输入 water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode interference-options: 0 # 滑动干扰项(0/1/2) req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false From 9f4c7f1feacbdb82345a650ce8a26dd56fef3cbf Mon Sep 17 00:00:00 2001 From: wuKong Date: Sat, 9 Aug 2025 00:32:41 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix(pay):=20=E4=BF=AE=E5=A4=8D=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AE=9D=E8=AF=81=E4=B9=A6=E6=A8=A1=E5=BC=8F=E7=9A=84?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 AbstractAlipayPayClient 类中的签名验证逻辑 - 在证书模式下,使用正确的公钥编码方式进行验证- 优化代码,确保支付宝签名验证的正确性和可靠性 --- .../pay/core/client/impl/alipay/AbstractAlipayPayClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index fee7cddba..caf210fa6 100644 --- a/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/yudao-module-pay/yudao-module-pay-server/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, 9 Aug 2025 00:33:20 +0800 Subject: [PATCH 4/8] =?UTF-8?q?feat(mp):=20=E6=B7=BB=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7ID=E7=AD=9B=E9=80=89=E6=9D=A1=E4=BB=B6=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E6=B6=88=E6=81=AF=E6=9F=A5=E8=AF=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 MpMessagePageReqVO 类中添加 userId 字段,用于筛选特定用户的消息 - 在 MpMessageMapper 类中添加对应的查询条件,实现按用户ID筛选消息的功能 --- .../admin/message/vo/message/MpMessagePageReqVO.java | 3 +++ .../yudao/module/mp/dal/mysql/message/MpMessageMapper.java | 1 + 2 files changed, 4 insertions(+) diff --git a/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java b/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java index d9f7cc876..9e01a5c33 100644 --- a/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java +++ b/yudao-module-mp/yudao-module-mp-server/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/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java b/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java index 72ba56627..0f11bdfe5 100644 --- a/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java +++ b/yudao-module-mp/yudao-module-mp-server/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)); } From 110c38bf6e1ee36da5222c9f94da6a37cb01c528 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 Aug 2025 19:02:44 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/ruoyi-vue-pro.sql | 6 +- .../module/bpm/enums/ErrorCodeConstants.java | 4 + .../module/bpm/enums/task/BpmReasonEnum.java | 1 + .../vo/model/BpmModelMetaInfoVO.java | 3 + .../admin/task/BpmTaskController.java | 8 + .../BpmProcessDefinitionInfoDO.java | 5 + .../flowable/core/util/BpmnModelUtils.java | 53 ++++++- .../task/BpmProcessInstanceServiceImpl.java | 25 +-- .../bpm/service/task/BpmTaskService.java | 8 + .../bpm/service/task/BpmTaskServiceImpl.java | 81 ++++++++-- .../crm/service/clue/CrmClueServiceImpl.java | 4 +- .../contact/CrmContactServiceImpl.java | 6 +- .../controller/admin/file/FileController.java | 2 +- .../vue/views/components/list_sub_erp.vue.vm | 1 + .../resources/codegen/vue/views/index.vue.vm | 1 + .../vue3/views/components/list_sub_erp.vue.vm | 1 + .../resources/codegen/vue3/views/index.vue.vm | 1 + .../general/views/index.vue.vm | 1 + .../general/views/modules/list_sub_erp.vue.vm | 1 + .../vue3_vben5_antd/schema/views/index.vue.vm | 1 + .../schema/views/modules/list_sub_erp.vue.vm | 1 + .../vue3_vben5_ele/general/views/index.vue.vm | 1 + .../general/views/modules/list_sub_erp.vue.vm | 1 + .../vue3_vben5_ele/schema/views/index.vue.vm | 1 + .../schema/views/modules/list_sub_erp.vue.vm | 1 + .../vo/message/MpMessagePageReqVO.java | 3 + .../mp/dal/mysql/message/MpMessageMapper.java | 1 + .../service/order/PayOrderServiceImpl.java | 2 +- .../mail/dto/MailSendSingleToUserReqDTO.java | 41 +++-- .../system/api/mail/MailSendApiImpl.java | 6 +- .../controller/admin/auth/AuthController.http | 28 ++-- .../controller/admin/dept/DeptController.java | 9 ++ .../admin/mail/MailTemplateController.java | 3 +- .../admin/mail/vo/log/MailLogRespVO.java | 11 +- .../vo/template/MailTemplateSendReqVO.java | 11 +- .../system/dal/dataobject/mail/MailLogDO.java | 16 +- .../system/dal/mysql/mail/MailLogMapper.java | 5 +- .../mq/message/mail/MailSendMessage.java | 15 +- .../system/mq/producer/mail/MailProducer.java | 26 +++- .../system/service/dept/DeptService.java | 7 + .../system/service/dept/DeptServiceImpl.java | 15 ++ .../system/service/mail/MailLogService.java | 23 +-- .../service/mail/MailLogServiceImpl.java | 10 +- .../system/service/mail/MailSendService.java | 37 +++-- .../service/mail/MailSendServiceImpl.java | 98 ++++++------ .../service/permission/MenuServiceImpl.java | 3 - .../service/mail/MailLogServiceImplTest.java | 102 +++++++------ .../service/mail/MailSendServiceImplTest.java | 142 +++++++++--------- .../src/test/resources/sql/create_tables.sql | 4 +- 49 files changed, 574 insertions(+), 262 deletions(-) diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index cdbaa0d06..b9dad19d9 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -1259,14 +1259,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_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 '模板编号', `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-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index 0e3b1b920..aeac0876f 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -60,6 +60,10 @@ public interface ErrorCodeConstants { ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在"); 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/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java index 46d1482a5..6ce6f65b8 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/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/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index d2316f58e..943a82d54 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/yudao-module-bpm/yudao-module-bpm-server/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; diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java index b796c5c17..b327b8e77 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java +++ b/yudao-module-bpm/yudao-module-bpm-server/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/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index c2799ef67..37e2c4462 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/yudao-module-bpm/yudao-module-bpm-server/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 规则 */ diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index fd08a1d97..a3414cedb 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -797,9 +797,9 @@ public class BpmnModelUtils { // 情况:StartEvent/EndEvent/UserTask/ServiceTask if (currentElement instanceof StartEvent - || currentElement instanceof EndEvent - || currentElement instanceof UserTask - || currentElement instanceof ServiceTask) { + || currentElement instanceof EndEvent + || currentElement instanceof UserTask + || currentElement instanceof ServiceTask) { // 添加节点 FlowNode flowNode = (FlowNode) currentElement; resultElements.add(flowNode); @@ -908,6 +908,49 @@ 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; + } + /** * 处理排它网关 * @@ -938,8 +981,8 @@ public class BpmnModelUtils { */ private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map variables) { SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), - flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) - && (evalConditionExpress(variables, flow.getConditionExpression()))); + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && (evalConditionExpress(variables, flow.getConditionExpression()))); if (matchSequenceFlow == null) { matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index ad1c74542..7ff8a132c 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -67,7 +67,6 @@ import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; @@ -221,11 +220,6 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService List simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, processDefinitionInfo, processVariables, activities); - // 3.3 如果是发起动作,activityId 为开始节点,不校验审批人自选节点 - if (ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) { - simulateActivityNodes.removeIf(node -> - BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy())); - } // 4. 拼接最终数据 return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, @@ -415,7 +409,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService endActivities.forEach(activity -> { // StartEvent:只处理 BPMN 的场景。因为,SIMPLE 情况下,已经有 START_USER_NODE 节点 if (ELEMENT_EVENT_START.equals(activity.getActivityType()) - && BpmModelTypeEnum.BPMN.getType().equals(processDefinitionInfo.getModelType())) { + && BpmModelTypeEnum.BPMN.getType().equals(processDefinitionInfo.getModelType()) + && !CollUtil.contains(activities, // 特殊:如果已经存在用户手动创建的 START_USER_NODE_ID 节点,则忽略 StartEvent + historicActivity -> historicActivity.getActivityId().equals(START_USER_NODE_ID))) { ActivityNodeTask startTask = new ActivityNodeTask().setId(BpmnModelConstants.START_USER_NODE_ID) .setAssignee(startUserId).setStatus(BpmTaskStatusEnum.APPROVE.getStatus()); ActivityNode startNode = new ActivityNode().setId(startTask.getId()) @@ -555,7 +551,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 情况一:BPMN 设计器 if (Objects.equals(BpmModelTypeEnum.BPMN.getType(), processDefinitionInfo.getModelType())) { List flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables); - return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn(startUserId, bpmnModel, + return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn( + startUserId, bpmnModel, flowElements, processDefinitionInfo, processVariables, flowElement, runActivityIds)); } // 情况二:SIMPLE 设计器 @@ -563,7 +560,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService BpmSimpleModelNodeVO simpleModel = JsonUtils.parseObject(processDefinitionInfo.getSimpleModel(), BpmSimpleModelNodeVO.class); List simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables); - return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple(startUserId, bpmnModel, + return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple( + startUserId, bpmnModel, processDefinitionInfo, processVariables, simpleNode, runActivityIds)); } throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType()); @@ -618,8 +616,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService return null; } - private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, + private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List flowElements, + BpmProcessDefinitionInfoDO processDefinitionInfo, + Map processVariables, FlowElement node, Set runActivityIds) { if (runActivityIds.contains(node.getId())) { return null; @@ -634,6 +633,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 1. 开始节点 if (node instanceof StartEvent) { + if (CollUtil.contains(flowElements, // 特殊:如果已经存在用户手动创建的 START_USER_NODE_ID 节点,则忽略 StartEvent + flowElement -> flowElement.getId().equals(START_USER_NODE_ID))) { + return null; + } return activityNode.setName(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getName()) .setNodeType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()); } diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index 0a5c866fd..34db2876f 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/yudao-module-bpm-server/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/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 04e85f2f9..a05f132e7 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -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,63 @@ 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.1 查询本人已办任务 + HistoricTaskInstance taskInstance = historyService.createHistoricTaskInstanceQuery() + .taskId(taskId).taskAssignee(userId.toString()).finished().singleResult(); + if (ObjUtil.isNull(taskInstance)) { + throw exception(TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS); + } + // 1.2 校验流程是否结束 + ProcessInstance processInstance = processInstanceService.getProcessInstance(taskInstance.getProcessInstanceId()); + if (ObjUtil.isNull(processInstance)) { + throw exception(TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING); + } + // 1.3 判断此流程是否允许撤回 + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo( + processInstance.getProcessDefinitionId()); + if (ObjUtil.isNull(processDefinitionInfo) || !Boolean.TRUE.equals(processDefinitionInfo.getAllowWithdrawTask())) { + throw exception(TASK_WITHDRAW_FAIL_NOT_ALLOW); + } + // 1.4 判断下一个节点是否被审批过,如果是则无法撤回 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskInstance.getProcessDefinitionId()); + UserTask userTask = (UserTask) BpmnModelUtils.getFlowElementById(bpmnModel, taskInstance.getTaskDefinitionKey()); + 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()).finished().count(); + if (nextUserTaskFinishedCount > 0) { + throw exception(TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + // 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 : runningTasks) { + // 标记撤回任务为取消 + 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()); + } + // 2.2 执行撤回操作 + runtimeService.createChangeActivityStateBuilder() + .processInstanceId(processInstance.getProcessInstanceId()) + .moveExecutionsToSingleActivityId(withdrawExecutionIds, taskInstance.getTaskDefinitionKey()) + .changeState(); + } + /** * 校验任务是否能被减签 * @@ -1223,7 +1280,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 +1407,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 +1419,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 +1444,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 +1513,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()); diff --git a/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java index 71c729cd1..fe84db1bd 100644 --- a/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java +++ b/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java @@ -106,14 +106,14 @@ 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()); } private void validateRelationDataExists(CrmClueSaveReqVO reqVO) { // 校验负责人 if (Objects.nonNull(reqVO.getOwnerUserId()) && - Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()).getCheckedData())) { + Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()))) { throw exception(USER_NOT_EXISTS); } } diff --git a/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java index 958fc7520..c3855f70a 100644 --- a/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java +++ b/yudao-module-crm/yudao-module-crm-server/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java @@ -159,10 +159,10 @@ public class CrmContactServiceImpl implements CrmContactService { // 2. 删除联系人 contactMapper.deleteById(id); - // 4.1 删除数据权限 - permissionService.deletePermission(CrmBizTypeEnum.CRM_CONTACT.getType(), id); - // 4.2 删除商机关联 + // 4.1 删除商机关联 contactBusinessService.deleteContactBusinessByContactId(id); + // 4.2 删除数据权限 + permissionService.deletePermission(CrmBizTypeEnum.CRM_CONTACT.getType(), id); // 记录操作日志上下文 LogRecordContext.putVariable("contactName", contact.getName()); diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index d5611b7a0..afcf71623 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -43,7 +43,7 @@ public class FileController { @PostMapping("/upload") @Operation(summary = "上传文件", description = "模式一:后端上传文件") - public CommonResult uploadFile(FileUploadReqVO uploadReqVO) throws Exception { + public CommonResult uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception { MultipartFile file = uploadReqVO.getFile(); byte[] content = IoUtil.readBytes(file.getInputStream()); return success(fileService.createFile(content, file.getOriginalFilename(), diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm index e1305586c..3f290ccff 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm @@ -170,6 +170,7 @@ await this.#[[$modal]]#.confirm('是否确认删除?') try { await ${simpleClassName}Api.delete${subSimpleClassName}List(this.checkedIds); + this.checkedIds = []; await this.getList(); this.#[[$modal]]#.msgSuccess("删除成功"); } catch {} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/index.vue.vm index 30014a8ff..bbc913114 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue/views/index.vue.vm @@ -338,6 +338,7 @@ export default { await this.#[[$modal]]#.confirm('是否确认删除?') try { await ${simpleClassName}Api.delete${simpleClassName}List(this.checkedIds); + this.checkedIds = []; await this.getList(); this.#[[$modal]]#.msgSuccess("删除成功"); } catch {} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm index f9fbb9787..a94cab5a5 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm @@ -209,6 +209,7 @@ const handleDeleteBatch = async () => { // 删除的二次确认 await message.delConfirm() await ${simpleClassName}Api.delete${subSimpleClassName}List(checkedIds.value); + checkedIds.value = []; message.success(t('common.delSuccess')) await getList(); } catch {} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/index.vue.vm index 851bc2b5e..dfb97804c 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/views/index.vue.vm @@ -366,6 +366,7 @@ const handleDeleteBatch = async () => { // 删除的二次确认 await message.delConfirm() await ${simpleClassName}Api.delete${simpleClassName}List(checkedIds.value); + checkedIds.value = []; message.success(t('common.delSuccess')) await getList(); } catch {} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm index bb743305f..6553ed0c8 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm @@ -168,6 +168,7 @@ async function handleDeleteBatch() { }); try { await delete${simpleClassName}List(checkedIds.value); + checkedIds.value = []; message.success( $t('ui.actionMessage.deleteSuccess') ); await getList(); } finally { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm index cfd85589c..999257d91 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm @@ -92,6 +92,7 @@ async function handleDeleteBatch() { }); try { await delete${subSimpleClassName}List(checkedIds.value); + checkedIds.value = []; message.success( $t('ui.actionMessage.deleteSuccess') ); await getList(); } finally { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm index 635e12ac2..1e13de2e9 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm @@ -102,6 +102,7 @@ async function handleDeleteBatch() { }); try { await delete${simpleClassName}List(checkedIds.value); + checkedIds.value = []; message.success({ content: $t('ui.actionMessage.deleteSuccess'), key: 'action_key_msg', diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm index 4001ed399..e046226ef 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm @@ -82,6 +82,7 @@ async function handleDeleteBatch() { }); try { await delete${subSimpleClassName}List(checkedIds.value); + checkedIds.value = []; message.success({ content: $t('ui.actionMessage.deleteSuccess', [row.id]), key: 'action_key_msg', diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/index.vue.vm index 9897ba677..ae77cd4c7 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/index.vue.vm @@ -163,6 +163,7 @@ async function handleDeleteBatch() { }); try { await delete${simpleClassName}List(checkedIds.value); + checkedIds.value = []; ElMessage.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/modules/list_sub_erp.vue.vm index e27965e4c..ccad79a0d 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/general/views/modules/list_sub_erp.vue.vm @@ -87,6 +87,7 @@ async function handleDeleteBatch() { }); try { await delete${subSimpleClassName}List(checkedIds.value); + checkedIds.value = []; ElMessage.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm index f9232d6b5..c29beb9aa 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm @@ -99,6 +99,7 @@ async function handleDeleteBatch() { }); try { await delete${simpleClassName}List(checkedIds.value); + checkedIds.value = []; ElMessage.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm index 5afb9c7a0..13a2415ef 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm @@ -79,6 +79,7 @@ async function handleDeleteBatch() { }); try { await delete${subSimpleClassName}List(checkedIds.value); + checkedIds.value = []; ElMessage.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java b/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java index d9f7cc876..9e01a5c33 100644 --- a/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java +++ b/yudao-module-mp/yudao-module-mp-server/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/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java b/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java index 72ba56627..0f11bdfe5 100644 --- a/yudao-module-mp/yudao-module-mp-server/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/message/MpMessageMapper.java +++ b/yudao-module-mp/yudao-module-mp-server/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/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java index 31532e5c6..b7f18abd6 100755 --- a/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-server/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java @@ -585,7 +585,7 @@ public class PayOrderServiceImpl implements PayOrderService { log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId()); return false; } - log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId()); + log.info("[expireOrder][order({}) 更新为支付关闭成功]", order.getId()); return true; } catch (Throwable e) { log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e); diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java index 78b9233ce..2d67a7808 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java @@ -1,27 +1,50 @@ package cn.iocoder.yudao.module.system.api.mail.dto; -import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; + +import java.util.List; import java.util.Map; -@Schema(description = "RPC 服务 - 邮件发送给 Admin 或者 Member 用户 Request DTO") +/** + * 邮件发送 Request DTO + * + * @author wangjingqi + */ @Data public class MailSendSingleToUserReqDTO { - @Schema(description = "用户编号", example = "1024") + /** + * 用户编号 + * + * 如果非空,则加载对应用户的邮箱,添加到 {@link #toMails} 中 + */ private Long userId; - @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") - @Email - private String mail; - @Schema(description = "邮件模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "USER_SEND") + /** + * 收件邮箱 + */ + private List<@Email String> toMails; + /** + * 抄送邮箱 + */ + private List<@Email String> ccMails; + /** + * 密送邮箱 + */ + private List<@Email String> bccMails; + + + /** + * 邮件模板编号 + */ @NotNull(message = "邮件模板编号不能为空") private String templateCode; - - @Schema(description = "邮件模板参数") + /** + * 邮件模板参数 + */ private Map templateParams; } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java index c4e5f82c6..45633a326 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/mail/MailSendApiImpl.java @@ -19,13 +19,15 @@ public class MailSendApiImpl implements MailSendApi { @Override public CommonResult sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) { - return success(mailSendService.sendSingleMailToAdmin(reqDTO.getMail(), reqDTO.getUserId(), + return success(mailSendService.sendSingleMailToAdmin(reqDTO.getUserId(), + reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams())); } @Override public CommonResult sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) { - return success(mailSendService.sendSingleMailToMember(reqDTO.getMail(), reqDTO.getUserId(), + return success(mailSendService.sendSingleMailToMember(reqDTO.getUserId(), + reqDTO.getToMails(), reqDTO.getCcMails(), reqDTO.getBccMails(), reqDTO.getTemplateCode(), reqDTO.getTemplateParams())); } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http index f21eb5268..52a724bf3 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http +++ b/yudao-module-system/yudao-module-system-server/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 @@ -21,16 +39,6 @@ tenant-id: {{adminTenantId}} "password": "admin123" } -### 请求 /login 接口 => 失败(租户不存在) -POST {{baseUrl}}/system/auth/login -Content-Type: application/json -tenant-id: 2 - -{ - "username": "admin", - "password": "admin123" -} - ### 请求 /get-permission-info 接口 => 成功 GET {{baseUrl}}/system/auth/get-permission-info Authorization: Bearer {{token}} diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java index 7873d00f0..7a243b778 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java +++ b/yudao-module-system/yudao-module-system-server/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/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java index 9ebcda532..52ac15087 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java +++ b/yudao-module-system/yudao-module-system-server/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.sendSingleMailToAdmin(sendReqVO.getMail(), getLoginUserId(), + return success(mailSendService.sendSingleMailToAdmin(getLoginUserId(), + sendReqVO.getToMails(), sendReqVO.getCcMails(), sendReqVO.getBccMails(), sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java index fed1d9233..8e67d1df7 100755 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/log/MailLogRespVO.java +++ b/yudao-module-system/yudao-module-system-server/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") @@ -19,8 +20,14 @@ public class MailLogRespVO { @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") private Byte userType; - @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "76854@qq.com") - private String toMail; + @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "user1@example.com, user2@example.com") + 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 = "18107") private Long accountId; diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java index b76b7ffcc..f125d77e9 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java +++ b/yudao-module-system/yudao-module-system-server/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-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java index 2a0da5172..756dba2ad 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/mail/MailLogDO.java +++ b/yudao-module-system/yudao-module-system-server/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,10 +49,22 @@ public class MailLogDO extends BaseDO implements Serializable { * 枚举 {@link UserTypeEnum} */ private Integer userType; + /** * 接收邮箱地址 */ - private String toMail; + @TableField(typeHandler = StringListTypeHandler.class) + private List toMails; + /** + * 接收邮箱地址 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List ccMails; + /** + * 密送邮箱地址 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List bccMails; /** * 邮箱账号编号 diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java index 6b147cff6..44fab07a0 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java +++ b/yudao-module-system/yudao-module-system-server/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/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java index 8d5af7c4c..03a4b7f19 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/message/mail/MailSendMessage.java @@ -5,6 +5,9 @@ import lombok.Data; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.util.Collection; +import java.util.List; + /** * 邮箱发送消息 * @@ -21,8 +24,16 @@ public class MailSendMessage { /** * 接收邮件地址 */ - @NotNull(message = "接收邮件地址不能为空") - private String mail; + @NotEmpty(message = "接收邮件地址不能为空") + private Collection toMails; + /** + * 抄送邮件地址 + */ + private Collection ccMails; + /** + * 密送邮件地址 + */ + private Collection bccMails; /** * 邮件账号编号 */ diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java index 5a44218bb..07aabb00a 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/mq/producer/mail/MailProducer.java @@ -7,6 +7,11 @@ import org.springframework.stereotype.Component; import jakarta.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.singletonList; + /** * Mail 邮件相关消息的 Producer * @@ -24,17 +29,22 @@ public class MailProducer { * 发送 {@link MailSendMessage} 消息 * * @param sendLogId 发送日志编码 - * @param mail 接收邮件地址 + * @param toMails 接收邮件地址 + * @param ccMails 抄送邮件地址 + * @param bccMails 密送邮件地址 * @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) { + 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).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-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java index a0b765e59..06a688e60 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java +++ b/yudao-module-system/yudao-module-system-server/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/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java index 946d92df3..6086474c6 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/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/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java index 4a0b20438..1c66e55ef 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogService.java @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.system.dal.dataobject.mail.MailAccountDO; 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; /** @@ -35,18 +37,21 @@ public interface MailLogService { /** * 创建邮件日志 * - * @param userId 用户编码 - * @param userType 用户类型 - * @param toMail 收件人邮件 - * @param account 邮件账号信息 - * @param template 模版信息 + * @param userId 用户编码 + * @param userType 用户类型 + * @param toMails 收件人邮件 + * @param ccMails 收件人邮件 + * @param bccMails 收件人邮件 + * @param account 邮件账号信息 + * @param template 模版信息 * @param templateContent 模版内容 - * @param templateParams 模版参数 - * @param isSend 是否发送成功 + * @param templateParams 模版参数 + * @param isSend 是否发送成功 * @return 日志编号 */ - Long createMailLog(Long userId, Integer userType, String toMail, - MailAccountDO account, MailTemplateDO template , + 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/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java index 827d0c56e..c17abaf01 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/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.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; @@ -12,8 +13,7 @@ import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.time.LocalDateTime; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; @@ -41,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(); @@ -49,7 +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) + .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/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java index 898816868..1b600bc90 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendService.java @@ -1,7 +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.Collection; import java.util.Map; /** @@ -15,38 +17,53 @@ 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); /** diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java index 306b05c04..682696f93 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImpl.java @@ -1,5 +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; @@ -13,10 +15,13 @@ 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.Collection; +import java.util.LinkedHashSet; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -49,56 +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); + // 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); + } + if (CollUtil.isNotEmpty(toMails)) { + toMails.stream().filter(Validator::isEmail).forEach(toMailSet::add); + } + if (CollUtil.isNotEmpty(ccMails)) { + ccMails.stream().filter(Validator::isEmail).forEach(ccMailSet::add); + } + if (CollUtil.isNotEmpty(bccMails)) { + 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, mail, + Long sendLogId = mailLogService.createMailLog(userId, userType, toMailSet, ccMailSet, bccMailSet, account, template, content, templateParams, isSend); // 发送 MQ 消息,异步执行发送短信 if (isSend) { - mailProducer.sendMailSendMessage(sendLogId, mail, 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. 创建发送账号 @@ -106,7 +122,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); @@ -146,16 +162,8 @@ 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; - } - /** - * 校验邮件参数是否确实 + * 校验邮件参数是否缺失 * * @param template 邮箱模板 * @param templateParams 参数列表 diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index 0d7536a1a..80832e969 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/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); } diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java index d883b4698..09312d5c9 100755 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java @@ -10,15 +10,17 @@ 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; -import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; @@ -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()); @@ -132,48 +142,50 @@ public class MailLogServiceImplTest extends BaseDbUnitTest { @Test public void testGetMailLogPage() { - // mock 数据 - MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> { // 等会查询到 - o.setUserId(1L); - o.setUserType(UserTypeEnum.ADMIN.getValue()); - o.setToMail("768@qq.com"); - o.setAccountId(10L); - o.setTemplateId(100L); - o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); - o.setSendTime(buildTime(2023, 2, 10)); - o.setTemplateParams(randomTemplateParams()); - }); - mailLogMapper.insert(dbMailLog); - // 测试 userId 不匹配 - 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"))); - // 测试 accountId 不匹配 - mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L))); - // 测试 templateId 不匹配 - mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setTemplateId(101L))); - // 测试 sendStatus 不匹配 - mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendStatus(MailSendStatusEnum.SUCCESS.getStatus()))); - // 测试 sendTime 不匹配 - mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendTime(buildTime(2023, 3, 10)))); - // 准备参数 - MailLogPageReqVO reqVO = new MailLogPageReqVO(); - reqVO.setUserId(1L); - reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); - reqVO.setToMail("768"); - reqVO.setAccountId(10L); - reqVO.setTemplateId(100L); - reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus()); - reqVO.setSendTime((buildBetweenTime(2023, 2, 1, 2023, 2, 15))); + // mock 数据 + MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + 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()); + o.setSendTime(buildTime(2023, 2, 10)); + o.setTemplateParams(randomTemplateParams()); + }); + mailLogMapper.insert(dbMailLog); + // 测试 userId 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 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 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setTemplateId(101L))); + // 测试 sendStatus 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendStatus(MailSendStatusEnum.SUCCESS.getStatus()))); + // 测试 sendTime 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendTime(buildTime(2023, 3, 10)))); + // 准备参数 + MailLogPageReqVO reqVO = new MailLogPageReqVO(); + reqVO.setUserId(1L); + reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); +// reqVO.setToMail("768@qq.com"); + reqVO.setAccountId(10L); + reqVO.setTemplateId(100L); + reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus()); + reqVO.setSendTime((buildBetweenTime(2023, 2, 1, 2023, 2, 15))); - // 调用 - PageResult pageResult = mailLogService.getMailLogPage(reqVO); - // 断言 - assertEquals(1, pageResult.getTotal()); - assertEquals(1, pageResult.getList().size()); - assertPojoEquals(dbMailLog, pageResult.getList().get(0)); + // 调用 + PageResult pageResult = mailLogService.getMailLogPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbMailLog, pageResult.getList().get(0)); } private static Map randomTemplateParams() { diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java index c7020f7c7..e5d71079a 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailSendServiceImplTest.java @@ -13,14 +13,14 @@ import cn.iocoder.yudao.module.system.mq.producer.mail.MailProducer; import cn.iocoder.yudao.module.system.service.member.MemberService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; import org.assertj.core.util.Lists; -import org.dromara.hutool.extra.mail.MailAccount; -import org.dromara.hutool.extra.mail.MailUtil; +import org.dromara.hutool.extra.mail.*; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -66,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 的方法 @@ -94,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() { @@ -159,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()); @@ -177,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(); @@ -201,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()); @@ -219,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()); } @@ -256,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); } @@ -287,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); // 调用 @@ -310,15 +311,16 @@ public class MailSendServiceImplTest extends BaseMockitoUnitTest { // mock 方法(发送邮件) Exception e = new NullPointerException("啦啦啦"); mailUtilMock.when(() -> MailUtil.send(argThat(mailAccount -> { - assertEquals("芋艿 <7685@qq.com>", mailAccount.getFrom()); - assertTrue(mailAccount.isAuth()); - assertEquals(account.getUsername(), mailAccount.getUser()); - assertArrayEquals(account.getPassword().toCharArray(), mailAccount.getPass()); - assertEquals(account.getHost(), mailAccount.getHost()); - 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); + assertEquals("芋艿 <7685@qq.com>", mailAccount.getFrom()); + assertTrue(mailAccount.isAuth()); + assertEquals(account.getUsername(), mailAccount.getUser()); + assertArrayEquals(account.getPassword().toCharArray(), mailAccount.getPass()); + assertEquals(account.getHost(), mailAccount.getHost()); + assertEquals(account.getPort(), mailAccount.getPort()); + assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); + return true; + }), 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/yudao-module-system-server/src/test/resources/sql/create_tables.sql b/yudao-module-system/yudao-module-system-server/src/test/resources/sql/create_tables.sql index 4df039b8d..d8b68369a 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/resources/sql/create_tables.sql +++ b/yudao-module-system/yudao-module-system-server/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 b4df6f93cbcd8771cfadeeb2aaad9b4c6ac54b3f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 Aug 2025 19:13:10 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/enums/WebFilterOrderEnum.java | 2 + .../framework/common/pojo/CommonResult.java | 8 +- .../framework/common/pojo/PageResult.java | 6 +- .../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 +- .../module/crm/enums/LogRecordConstants.java | 2 +- .../service/permission/MenuServiceImpl.java | 4 + .../src/main/resources/application.yaml | 7 + .../auth/AdminAuthServiceImplTest.java | 1 - .../service/dept/DeptServiceImplTest.java | 34 +++- yudao-server/pom.xml | 70 ++++---- .../src/main/resources/application.yaml | 7 + 19 files changed, 587 insertions(+), 52 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 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 9d9f4257b..1cac50c7c 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 @@ -17,6 +17,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-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java index ac7410315..afb9cd306 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java @@ -25,16 +25,16 @@ public class CommonResult implements Serializable { * @see ErrorCode#getCode() */ private Integer code; - /** - * 返回数据 - */ - private T data; /** * 错误提示,用户可阅读 * * @see ErrorCode#getMsg() () */ private String msg; + /** + * 返回数据 + */ + private T data; /** * 将传入的 result 对象,转换成另外一个泛型结果的对象 diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java index ff9087a81..47c59d1d9 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageResult.java @@ -11,12 +11,12 @@ import java.util.List; @Data public final class PageResult implements Serializable { - @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) - private List list; - @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) private Long total; + @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) + private List list; + public PageResult() { } 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 000000000..135eb85bb --- /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 000000000..03d0f1ac1 --- /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 000000000..740511103 --- /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 000000000..b9f015a7e --- /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 000000000..e6d03ba32 --- /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 000000000..fed38917b --- /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 000000000..ca0819712 --- /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 8e80fa591..b5f38d96f 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 5e1f2f29d..ea1197d94 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 @@ -4,4 +4,5 @@ cn.iocoder.yudao.framework.swagger.config.YudaoSwaggerAutoConfiguration cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration cn.iocoder.yudao.framework.apilog.config.YudaoApiLogRpcAutoConfiguration 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-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java b/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java index aeeed316d..30d51cf47 100644 --- a/yudao-module-crm/yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/enums/LogRecordConstants.java +++ b/yudao-module-crm/yudao-module-crm-api/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-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index 4a6d11492..80832e969 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -275,6 +275,10 @@ public class MenuServiceImpl implements MenuService { if (menu == null) { return; } + // 如果 id 为空,说明不用比较是否为相同 id 的菜单 + if (id == null) { + return; + } if (!menu.getId().equals(id)) { throw exception(MENU_COMPONENT_NAME_DUPLICATE); } diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml b/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml index 2d90ccfac..3f48e4a33 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/application.yaml @@ -158,6 +158,13 @@ yudao: enable: false exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + 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 案例】响应加密的公钥 swagger: title: 管理后台 description: 提供管理员管理的所有功能 diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java index 1b7b55929..aa3795ff6 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java @@ -26,7 +26,6 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java index bcf55bda6..df16d4fc9 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/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 diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index 53e5e0fa2..66dcf4c46 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -33,11 +33,11 @@ - - - - - + + cn.iocoder.cloud + yudao-module-member-server + ${revision} + @@ -46,17 +46,17 @@ - - - - - + + cn.iocoder.cloud + yudao-module-bpm-server + ${revision} + - - - - - + + cn.iocoder.cloud + yudao-module-pay-server + ${revision} + @@ -66,26 +66,26 @@ - - - - - - - - - - - - - - - - - - - - + + cn.iocoder.cloud + yudao-module-product-server + ${revision} + + + cn.iocoder.cloud + yudao-module-promotion-server + ${revision} + + + cn.iocoder.cloud + yudao-module-trade-server + ${revision} + + + cn.iocoder.cloud + yudao-module-statistics-server + ${revision} + diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index e7e87bd06..21d210422 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -269,6 +269,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 # 路径 From c55fe616b6bc81c9208821d04d388e7456f719e0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 16 Aug 2025 21:23:06 +0800 Subject: [PATCH 7/8] =?UTF-8?q?reactor=EF=BC=9A=E3=80=90cloud=20=E5=BE=AE?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E3=80=91EnvLoadBalancerClient=20=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E8=B0=83=E8=AF=95=E6=97=B6=EF=BC=8C=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E4=B8=BA=E7=A9=BA=E6=98=AF=E7=9B=B8=E5=AF=B9=E6=AD=A3=E5=B8=B8?= =?UTF-8?q?=E6=83=85=E5=86=B5=EF=BC=8Chttps://t.zsxq.com/hcam5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/env/core/fegin/EnvLoadBalancerClient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClient.java b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClient.java index 79339b25c..e54559575 100644 --- a/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClient.java +++ b/yudao-framework/yudao-spring-boot-starter-env/src/main/java/cn/iocoder/yudao/framework/env/core/fegin/EnvLoadBalancerClient.java @@ -96,7 +96,6 @@ public class EnvLoadBalancerClient implements ReactorServiceInstanceLoadBalancer List chooseInstances = CollectionUtils.filterList(instances, instance -> StrUtil.isEmpty(EnvUtils.getTag(instance))); // 【重要】补充说明:如果希望在 chooseInstances 为空时,不允许打到有 tag 的实例,可以取消注释下面的代码 if (CollUtil.isEmpty(chooseInstances)) { - log.warn("[getInstanceResponseWithoutTag][serviceId({}) 没有不带 tag 的服务实例列表,直接使用所有服务实例列表]", serviceId); chooseInstances = instances; } From adadcf4f6264f746ede016f560fcb8dcae5dbe87 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 17 Aug 2025 11:30:17 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat=EF=BC=9A=E8=A1=A5=E5=85=85=20applicati?= =?UTF-8?q?on-dev=20=E7=BC=BA=E5=A4=B1=E7=9A=84=20xxl-job=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B9=EF=BC=8C=E7=BB=9F=E4=B8=80=E4=B8=80=E4=BA=9B?= =?UTF-8?q?~=EF=BC=88=E9=9D=9E=20bug=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yaml | 5 +++++ .../src/main/resources/application-dev.yaml | 5 +++++ .../src/main/resources/application-dev.yaml | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/yudao-module-crm/yudao-module-crm-server/src/main/resources/application-dev.yaml b/yudao-module-crm/yudao-module-crm-server/src/main/resources/application-dev.yaml index a6dbbfe95..08050aa13 100644 --- a/yudao-module-crm/yudao-module-crm-server/src/main/resources/application-dev.yaml +++ b/yudao-module-crm/yudao-module-crm-server/src/main/resources/application-dev.yaml @@ -77,6 +77,11 @@ spring: --- #################### MQ 消息队列相关配置 #################### --- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 #################### diff --git a/yudao-module-erp/yudao-module-erp-server/src/main/resources/application-dev.yaml b/yudao-module-erp/yudao-module-erp-server/src/main/resources/application-dev.yaml index a6dbbfe95..08050aa13 100644 --- a/yudao-module-erp/yudao-module-erp-server/src/main/resources/application-dev.yaml +++ b/yudao-module-erp/yudao-module-erp-server/src/main/resources/application-dev.yaml @@ -77,6 +77,11 @@ spring: --- #################### MQ 消息队列相关配置 #################### --- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 #################### diff --git a/yudao-module-pay/yudao-module-pay-server/src/main/resources/application-dev.yaml b/yudao-module-pay/yudao-module-pay-server/src/main/resources/application-dev.yaml index 28cec7694..2e389ca05 100644 --- a/yudao-module-pay/yudao-module-pay-server/src/main/resources/application-dev.yaml +++ b/yudao-module-pay/yudao-module-pay-server/src/main/resources/application-dev.yaml @@ -77,6 +77,11 @@ spring: --- #################### MQ 消息队列相关配置 #################### --- #################### 定时任务相关配置 #################### +xxl: + job: + enabled: false # 是否开启调度中心,默认为 true 开启 + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 ####################