From abed96ecc930d3cb5cb3900c8c25a07798d346db Mon Sep 17 00:00:00 2001 From: C77 Date: Mon, 22 Dec 2025 10:40:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(mail):=20=E5=A2=9E=E5=BC=BA=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E6=A8=A1=E6=9D=BF=E5=8F=82=E6=95=B0=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=92=8CHTML=E5=86=85=E5=AE=B9=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改邮件模板创建和更新逻辑,同时解析标题和内容中的参数 - 增强邮件内容格式化方法,支持HTML特殊字符反转义 - 实现代码块格式化,为
标签添加样式
- 替换最外层pre标签为div标签以改善显示效果
- 添加完整的HTML邮件模板处理测试用例
- 扩展参数解析方法以合并标题和内容中的参数并去重
---
 .../service/mail/MailTemplateServiceImpl.java | 107 ++++++++++-
 .../mail/MailTemplateServiceImplTest.java     | 169 ++++++++++++++++++
 2 files changed, 271 insertions(+), 5 deletions(-)

diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java
index 3088ee79af..8da1a33422 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImpl.java
@@ -19,8 +19,10 @@ import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -53,7 +55,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
 
         // 插入
         MailTemplateDO template = BeanUtils.toBean(createReqVO, MailTemplateDO.class)
-                .setParams(parseTemplateContentParams(createReqVO.getContent()));
+                .setParams(parseTemplateTitleAndContentParams(createReqVO.getTitle(), createReqVO.getContent()));
         mailTemplateMapper.insert(template);
         return template.getId();
     }
@@ -69,7 +71,7 @@ public class MailTemplateServiceImpl implements MailTemplateService {
 
         // 更新
         MailTemplateDO updateObj = BeanUtils.toBean(updateReqVO, MailTemplateDO.class)
-                .setParams(parseTemplateContentParams(updateReqVO.getContent()));
+                .setParams(parseTemplateTitleAndContentParams(updateReqVO.getTitle(), updateReqVO.getContent()));
         mailTemplateMapper.updateById(updateObj);
     }
 
@@ -129,13 +131,108 @@ public class MailTemplateServiceImpl implements MailTemplateService {
 
     @Override
     public String formatMailTemplateContent(String content, Map params) {
-        return StrUtil.format(content, params);
+        // 先替换模板变量
+        String formattedContent = StrUtil.format(content, params);
+
+        // 反转义HTML特殊字符
+        formattedContent = unescapeHtml(formattedContent);
+
+        // 处理代码块(确保
标签格式正确)
+        formattedContent = formatHtmlCodeBlocks(formattedContent);
+
+        // 将最外层的pre标签替换为div标签
+        formattedContent = replaceOuterPreWithDiv(formattedContent);
+
+        return formattedContent;
+    }
+
+    private String replaceOuterPreWithDiv(String content) {
+        if (content == null) {
+            return null;
+        }
+
+        // 使用正则表达式匹配所有的
标签,包括嵌套的标签
+        String regex = "(?s)]*>(.*?)
"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(content); + + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + // 提取
标签内的内容
+            String innerContent = matcher.group(1);
+            // 返回div标签包裹的内容
+            matcher.appendReplacement(sb, "
" + innerContent + "
"); + } + matcher.appendTail(sb); + + return sb.toString(); + } + + /** + * 反转义HTML特殊字符 + * + * @param input 输入字符串 + * @return 反转义后的字符串 + */ + private String unescapeHtml(String input) { + if (input == null) { + return null; + } + return input + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'") + .replace(" ", " "); + } + + /** + * 格式化HTML中的代码块 + * + * @param content 邮件内容 + * @return 格式化后的邮件内容 + */ + private String formatHtmlCodeBlocks(String content) { + // 匹配
标签的代码块
+        Pattern codeBlockPattern = Pattern.compile("(.*?)
", Pattern.DOTALL); + Matcher matcher = codeBlockPattern.matcher(content); + + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + // 获取代码块内容 + String codeBlock = matcher.group(1); + + // 为代码块添加样式 + String replacement = "
" + codeBlock + "
"; + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + + return sb.toString(); } @Override public long getMailTemplateCountByAccountId(Long accountId) { return mailTemplateMapper.selectCountByAccountId(accountId); } + /** + * 解析标题和内容中的参数 + */ + @VisibleForTesting + public List parseTemplateTitleAndContentParams(String title, String content) { + List titleParams = ReUtil.findAllGroup1(PATTERN_PARAMS, title); + List contentParams = ReUtil.findAllGroup1(PATTERN_PARAMS, content); + + // 合并参数并去重 + List allParams = new ArrayList<>(titleParams); + for (String param : contentParams) { + if (!allParams.contains(param)) { + allParams.add(param); + } + } + return allParams; + } /** * 获得邮件模板中的参数,形如 {key} @@ -143,8 +240,8 @@ public class MailTemplateServiceImpl implements MailTemplateService { * @param content 内容 * @return 参数列表 */ - private List parseTemplateContentParams(String content) { + List parseTemplateContentParams(String content) { return ReUtil.findAllGroup1(PATTERN_PARAMS, content); } -} +} \ No newline at end of file diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java index 70059b233c..9a43e2ddae 100755 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailTemplateServiceImplTest.java @@ -196,6 +196,119 @@ public class MailTemplateServiceImplTest extends BaseDbUnitTest { mailTemplateService.formatMailTemplateContent("{name},你好,{what}吃了吗?", params)); } + @Test + public void testFormatMailTemplateContent_htmlUnescape() { + // 准备参数 + Map params = new HashMap<>(); + params.put("title", "测试标题"); + + // 测试HTML反转义 + String content = "

{title}

<p>这是一个测试</p>&nbsp;空格"; + String expected = "

测试标题

这是一个测试

空格"; + // 调用,并断言 + assertEquals(expected, + mailTemplateService.formatMailTemplateContent(content, params)); + } + + @Test + public void testFormatMailTemplateContent_codeBlockFormatting() { + // 准备参数 + Map params = new HashMap<>(); + params.put("name", "测试"); + + // 测试代码块格式化 + String content = "
public class Test {\n    public static void main(String[] args) {\n        System.out.println(\"Hello {name}\"));\n    }\n}
"; + + // 调用,并断言结果 + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言pre标签被替换为div标签 + assertTrue(result.contains("
public class Test {")); + assertTrue(result.contains("System.out.println(\"Hello 测试\"")); + assertTrue(result.contains("
")); + } + + @Test + public void testFormatMailTemplateContent_preToDiv() { + // 准备参数 + Map params = new HashMap<>(); + params.put("content", "测试内容"); + + // 测试pre标签替换为div标签 + String content = "
{content}
"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言结果中包含div标签,而不包含pre标签 + assertTrue(result.contains("
测试内容
")); + } + + @Test + public void testFormatMailTemplateContent_completeHtml() { + // 准备参数 + Map params = new HashMap<>(); + params.put("username", "testuser"); + params.put("company", "测试公司"); + + // 测试完整的HTML邮件模板 + String content = "\n \n \n \n \n Title\n \n \n
\n \n
\n
\n
\n
\n \n
\n
\n \n \n \n \n \n \n \n \n
\n
\n \n \n \n
\n
此邮件由系统发出,请勿直接回复或转发他人
\n

\n
\n \n \n \n \n \n \n \n \n
\n

\n 尊敬的 {username},\n

\n
\n
\n

\n 内容
\n 内容
\n 内容123

\n\n 如果您在使用过程中遇到任何问题或者有任何建议,都可以随时联系我们的客户团队,我们将竭诚为您服务。\n\n\n\n\n
\n
\n {company}
\n 地址:xxxxx
\n 邮箱:lambc77@163.com\n

\n
\n
\n
\n
\n
\n 声明:本邮件含有保密信息,仅限于收件人所用。禁止任何人未经发件人许可,以任何形式(包括但不限于部分的泄露、复制或散发)不当的使用本邮件中的信息。如果您错收了本邮件,请您立即电话或邮件通知发件人并删除本邮件,谢谢!\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n \n "; + + // 调用,并断言成功处理 + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言结果中包含替换后的变量 + assertTrue(result.contains("尊敬的 testuser")); + assertTrue(result.contains("测试公司")); + // 断言结果是有效的HTML + assertTrue(result.startsWith("")); + } + + @Test + public void testFormatMailTemplateContent_emptyContent() { + // 准备参数 + Map params = new HashMap<>(); + + // 测试空内容 + String result = mailTemplateService.formatMailTemplateContent("", params); + assertEquals("", result); + } + + @Test + public void testFormatMailTemplateContent_noParams() { + // 准备参数 + Map params = new HashMap<>(); + + // 测试没有参数需要替换的情况 + String content = "
System.out.println(\"Hello World\");
"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + assertTrue(result.contains("
System.out.println(\"Hello World\");
")); + } + + @Test + public void testFormatMailTemplateContent_multiplePreTags() { + // 准备参数 + Map params = new HashMap<>(); + params.put("param1", "value1"); + params.put("param2", "value2"); + + // 测试多个pre标签的情况 + String content = "
First code block: {param1}
\n" + + "

Some text between code blocks

\n" + + "
Second code block: {param2}
"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言两个pre标签都被替换为div标签 + assertTrue(result.contains("
First code block: value1
")); + assertTrue(result.contains("
Second code block: value2
")); + } + + @Test + public void testFormatMailTemplateContent_specialCharacters() { + // 准备参数 + Map params = new HashMap<>(); + + // 简化测试,只测试基本的HTML特殊字符 + String content = "<div>测试 & 特殊字符</div>"; + String result = mailTemplateService.formatMailTemplateContent(content, params); + // 断言特殊字符被正确反转义 + assertTrue(result.contains("
测试 & 特殊字符
")); + } + @Test public void testCountByAccountId() { // mock 数据 @@ -212,4 +325,60 @@ public class MailTemplateServiceImplTest extends BaseDbUnitTest { assertEquals(1, count); } + @Test + public void testDifferenceWithHtmlContent() { + // 准备包含HTML格式的模板内容 + String content = "
" + + "

Welcome, {username}!

" + + "

Your account has been created successfully.

" + + "
" + + "Account Details:
" + + "Username: {username}
" + + "Email: {email}
" + + "Role: {role}
" + + "
" + + "

Please click here to activate your account.

" + + "
public class WelcomeMessage {\n    public static void main(String[] args) {\n        System.out.println(\"Hello {username}!\");\n    }\n}
" + + "
"; + + Map params = new HashMap<>(); + params.put("username", "testuser"); + params.put("email", "test@163.com"); + params.put("role", "admin"); + params.put("activationLink", "https://example.com/activate?code=12345"); + + // 1. 使用parseTemplateContentParams:只提取参数名称,忽略了HTML格式 + List parsedParams = mailTemplateService.parseTemplateContentParams(content); + System.out.println("parseTemplateContentParams结果:" + parsedParams); + + // 断言:只提取了纯参数名称,没有HTML格式 + assertEquals(6, parsedParams.size()); + // 检查所有参数类型 + assertTrue(parsedParams.stream().filter("username"::equals).count() == 3); + assertTrue(parsedParams.stream().filter("email"::equals).count() == 1); + assertTrue(parsedParams.stream().filter("role"::equals).count() == 1); + assertTrue(parsedParams.stream().filter("activationLink"::equals).count() == 1); + // 断言:没有包含任何HTML标签 + for (String param : parsedParams) { + assertFalse(param.contains("<")); + assertFalse(param.contains(">")); + } + + // 2. 使用formatMailTemplateContent:处理HTML格式,生成最终内容 + String formattedContent = mailTemplateService.formatMailTemplateContent(content, params); + System.out.println("formatMailTemplateContent结果:" + formattedContent); + + // 断言:HTML格式被保留并处理 + assertTrue(formattedContent.contains("
")); + assertTrue(formattedContent.contains("

Welcome, testuser!

")); + assertTrue(formattedContent.contains("here")); + assertTrue(formattedContent.contains("
public class WelcomeMessage {")); + assertTrue(formattedContent.contains("
")); + // 断言:所有参数都被正确替换 + assertFalse(formattedContent.contains("{username}")); + assertFalse(formattedContent.contains("{email}")); + assertFalse(formattedContent.contains("{role}")); + assertFalse(formattedContent.contains("{activationLink}")); + } + }