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}")); + } + }