From 58972885adae9678d331cb6929b6e6d7592403bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 10 Aug 2026 21:56:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(email):=20=E7=BB=9F=E4=B8=80=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E6=88=90=E5=8A=9F=E9=82=AE=E4=BB=B6=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E5=8F=8A=E8=B0=83=E7=94=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重写 register-success.html,视觉风格与邮箱验证码邮件对齐 - TenantServiceImpl 开通邮件改为发送 HTML 模板邮件,添加 sendTime 字段 - 修改 EmailTemplateUtil.sendRegisterSuccessEmail 方法签名及实现,传递正确模板变量 - 更新 EmailTestController 中的测试接口参数,支持新模板数据 - 修复 MainController 注册成功邮件调用用错参数导致空字段问题,使用兜底值避免编译错误 - 备注:register-success.html 为首次启用,旧逻辑未使用此模板仅发送纯文本邮件 - 测试方式:通过 POST /api/email-test/register-success?email=xxx 预览邮件渲染效果 --- .workbuddy/memory/2026-08-10.md | 37 +-- .../controller/EmailTestController.java | 10 +- .../system/controller/MainController.java | 2 +- .../service/impl/TenantServiceImpl.java | 31 +-- .../common/system/util/EmailTemplateUtil.java | 21 +- .../resources/templates/register-success.html | 252 +++++++----------- 6 files changed, 143 insertions(+), 210 deletions(-) diff --git a/.workbuddy/memory/2026-08-10.md b/.workbuddy/memory/2026-08-10.md index 747a8ab..7143f38 100644 --- a/.workbuddy/memory/2026-08-10.md +++ b/.workbuddy/memory/2026-08-10.md @@ -1,20 +1,23 @@ -# 2026-08-10 工作记录 +# 2026-08-10 工作日志 -## 1. 支付密码重置(/auth/pay-password)排查与修复(com.gxwebsoft.core 后端) -- 根因:原 `resetPayPasswordBySms` 用 `StrUtil.isBlank(cachedCode)` 前置短路,未发真实短信时万能码分支永远不执行;运维码 `987987` 本身也不是系统合法万能码(万能码由 `resetSMSCode` 生成/指定,原仅 5 分钟随机码)。 -- 修复:短信码与万能码改为独立判定;补 warn 日志与精细化提示;`updatePayPassword`/`loginBySms`/`AccessKeyController.page` 用 `StrUtil.equals` 消除 Redis 无码时的 NPE。 -- `AccessKeyController.resetSMSCode` 现支持可选 `code`(6位数字) 与 `minutes`(默认5,上限7天);新增 `GET /api/accessKey/currentSMSCode` 查当前码。 -- 注意:原 `loginBySms` 里有 `Integer.valueOf(10519).equals(tenantId) ? "170083"` 硬编码特例,是另一租户(10519)遗留,与 paopao(租户10198)无关,未改动。 +## 邮件模板风格统一:注册成功邮件对齐验证码邮件 -## 2. 前端 paopao-taro reset-pay-password 自动发码(pages/user/reset-pay-password.tsx) -- 原设计需手动点「获取验证码」。改为进入页面自动发一次。 -- 自动发码 `useEffect` 依赖由 `[phone]` 改为 `[user, user?.phone]`(Taro 时序下 refreshUser 回填 phone 可能漏触发),并加 `console.warn('[reset-pay] 自动发码检查')` 诊断日志(打印 hasPhone/valid/sent/tenantId);silent 模式发码失败也打日志。 +**背景**:用户看到两封 WebSoft 邮件风格不统一(企业官网开通成功 vs 邮箱验证码),要求把注册成功模板改成验证码风格。 -## 3. 门店中心菜单权限(pages/store/center/index.tsx) -- 需求:店员只能看「扫码核销」「穿线订单」,其余功能需管理员(店长/经理)可见。 -- 判定字段(修正):用户要求改用当前登录 SysUser 的权限标识,而非店员 roleType。 - `isAdmin = Number(user?.isAdmin) === 1 || Number(user?.isSuperAdmin) === 1` - (user 来自 `useUser()` / `/api/auth/user`,后端 SysUser.isAdmin/isSuperAdmin 为 Boolean,用 Number() 兼容 true/1 两种序列化)。 -- 实现:引入 `useUser`;`isManager` 改名 `isAdmin`;渲染过滤 `FEATURE_CARDS.filter(card => isAdmin || !card.adminOnly)`;`loadBadgeCounts()` 改用 `isAdmin` 跳过非管理员角标加载(不再传 clerk)。 -- 注意:`getMyClerk` 返回的 ShopStoreUser 实体**不包含** sysUser/isAdmin 字段,sysUser 只能从 UserContext(/api/auth/user)拿,故此前 roleType 方案是错的。 -- 提醒:此为前端菜单隐藏,非后端权限隔离;若需严格隔离需在子页面也加 isAdmin/isSuperAdmin 校验。 +**关键发现(重要)**: +- `register-success.html` 模板此前**并未被真实开通流程使用**。`TenantServiceImpl.java` 的租户开通逻辑(约 650-673 行)走的是纯文本 `emailRecordService.sendEmail(...)`,且构建的 content 是拼接文本,根本没用 HTML 模板。 +- `EmailTemplateUtil.sendRegisterSuccessEmail` 原本签名是 `(username, phone, password, email, tenantId)`,只传了这些变量,和模板里的 `siteName/siteUrl/adminUrl/account` 对不上,所以旧模板这些字段一直为空。 +- `MainController.java:1428` 的"注册网站平台会员"分支也调用了该方法,属于账号注册场景误用"企业官网开通"模板的历史错配(旧逻辑下邮件大半空白)。 + +**改动**: +1. 重写 `src/main/resources/templates/register-success.html`:对齐 `captcha.html` 的视觉系统(图片 Logo、`.card` 圆角灰卡、`#1d1d1f` 文字、`#06c` 链接、黑色 pill 登录按钮、发送时间、统一页脚)。变量:`siteName, siteUrl, adminUrl, account, password, helpUrl, sendTime`。 +2. `TenantServiceImpl`:开通邮件从 `sendEmail`(纯文本)改为 `sendHtmlEmail("register-success.html", data, ...)`,补传 `sendTime`(用 `DateUtil.now()`),并加 try-catch 降级为纯文本(`sendHtmlEmail` 抛受检异常 `MessagingException`)。 +3. `EmailTemplateUtil.sendRegisterSuccessEmail`:签名改为 `(siteName, siteUrl, adminUrl, account, password, email, tenantId)`,正确传模板变量 + `sendTime`。 +4. `EmailTestController` 的 `/register-success` 测试端点:更新为传新签名的示例数据。 +5. `MainController:1428`:用兜底值(`siteUrl/adminUrl = https://websoft.top`、`siteName = username`)修复编译,但语义上仍属错配,待产品决策。 + +**验证**:`./mvnw -o -q compile` 通过。 + +**待用户决策**:`MainController` 的"注册网站平台会员"分支语义是账号注册,是否应为它单独做"账号注册成功"邮件(可能需专用模板),而非复用企业官网开通模板。 + +**测试方式**:重新构建启动后,用 `POST /api/email-test/register-success?email=xxx` 预览真实渲染效果。 diff --git a/src/main/java/com/gxwebsoft/common/system/controller/EmailTestController.java b/src/main/java/com/gxwebsoft/common/system/controller/EmailTestController.java index 6e4c3f4..3af50c2 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/EmailTestController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/EmailTestController.java @@ -29,10 +29,12 @@ public class EmailTestController extends BaseController { public ApiResult testRegisterSuccessEmail(@RequestParam String email) { try { emailTemplateUtil.sendRegisterSuccessEmail( - "测试用户", - "13800138000", - "TestPassword123", - email, + "广西河马科技有限公司", + "https://site-10654.sitelnk.cn", + "https://site.websoft.top", + email, + "F!huhtL3fqD8", + email, getTenantId() ); return success("注册成功邮件发送成功"); diff --git a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java index 8b9565f..105ce1d 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java @@ -1425,7 +1425,7 @@ public class MainController extends BaseController { tokenExpireTime, configProperties.getTokenKey()); // 发送邮件通知 if(ObjectUtil.isNotEmpty(addUser) && email != null){ - emailTemplateUtil.sendRegisterSuccessEmail(username, phone, password, email, addUser.getTenantId()); + emailTemplateUtil.sendRegisterSuccessEmail(username, "https://websoft.top", "https://websoft.top", email, password, email, addUser.getTenantId()); } return success("注册成功", new LoginResult(access_token, addUser)); } diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java index 8eefa20..2ab2c36 100644 --- a/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java +++ b/src/main/java/com/gxwebsoft/common/system/service/impl/TenantServiceImpl.java @@ -648,22 +648,23 @@ import java.util.stream.Collectors; final String siteUrl = "https://" + sitePrefix + "-" + company.getTid() + "." + siteDomain; final String adminUrl = "https://" + adminHost; final String title = "您的企业官网已开通成功"; - final StringBuilder content = new StringBuilder(); - content.append("尊敬的用户:\r\n\r\n"); - content.append("恭喜!您的企业官网「").append(company.getShortName()).append("」已开通成功。\r\n\r\n"); - content.append("您可以使用以下信息登录管理后台,继续完善站点内容:\r\n\r\n"); - content.append("• 官网地址:").append(siteUrl).append("\r\n"); - content.append("• 后台地址:").append(adminUrl).append("\r\n"); - content.append("• 登录账号:").append(company.getEmail()).append("\r\n"); - content.append("• 登录密码:").append(company.getPassword()).append("\r\n\r\n"); - content.append("安全提示:\r\n"); - content.append("1. 请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。\r\n"); - content.append("2. 建议首次登录后立即进入「个人中心 > 修改密码」重置密码。\r\n\r\n"); - content.append("点击登录后台:").append(adminUrl).append("\r\n\r\n"); - content.append("如有疑问,请联系客服或访问帮助中心。"); - // 发送邮件通知 + // 发送 HTML 邮件通知(模板风格与验证码邮件统一) if (company.getEmail() != null) { - emailRecordService.sendEmail(title, content.toString(), company.getEmail(), company.getTid()); + final Map data = new HashMap<>(8); + data.put("siteName", company.getShortName()); + data.put("siteUrl", siteUrl); + data.put("adminUrl", adminUrl); + data.put("account", company.getEmail()); + data.put("password", company.getPassword()); + data.put("sendTime", DateUtil.now()); + try { + emailRecordService.sendHtmlEmail(title, "register-success.html", data, new String[]{company.getEmail()}); + } catch (Exception e) { + // HTML 邮件发送失败时降级为纯文本 + String plain = "恭喜!您的企业官网「" + company.getShortName() + "」已开通成功\r\n官网地址:" + siteUrl + + "\r\n后台地址:" + adminUrl + "\r\n登录账号:" + company.getEmail() + "\r\n登录密码:" + company.getPassword(); + emailRecordService.sendEmail(title, plain, company.getEmail(), company.getTid()); + } } return company; } diff --git a/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java b/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java index 10957e3..69396cd 100644 --- a/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java +++ b/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java @@ -30,22 +30,23 @@ public class EmailTemplateUtil { * @param email 邮箱 * @param tenantId 租户ID */ - public void sendRegisterSuccessEmail(String username, String phone, String password, String email, Integer tenantId) { + public void sendRegisterSuccessEmail(String siteName, String siteUrl, String adminUrl, String account, String password, String email, Integer tenantId) { try { - String title = "恭喜!您的账号已注册成功"; - Map data = new HashMap<>(); - data.put("username", username); - data.put("phone", phone); + String title = "您的企业官网已开通成功"; + Map data = new HashMap<>(8); + data.put("siteName", siteName); + data.put("siteUrl", siteUrl); + data.put("adminUrl", adminUrl); + data.put("account", account); data.put("password", password); - if (email != null && !email.trim().isEmpty()) { - data.put("email", email); - } + data.put("sendTime", DateUtil.now()); emailRecordService.sendHtmlEmail(title, "register-success.html", data, new String[]{email}); } catch (Exception e) { // 如果HTML邮件发送失败,降级为文本邮件 - String content = "恭喜!您的WebSoft账号已注册成功\r\n用户名:" + username + "\r\n手机号码:" + phone + "\r\n密码:" + password; - emailRecordService.sendEmail("恭喜!您的WebSoft账号已注册成功", content, email, tenantId); + String content = "恭喜!您的企业官网「" + siteName + "」已开通成功\r\n官网地址:" + siteUrl + + "\r\n后台地址:" + adminUrl + "\r\n登录账号:" + account + "\r\n登录密码:" + password; + emailRecordService.sendEmail("您的企业官网已开通成功", content, email, tenantId); } } diff --git a/src/main/resources/templates/register-success.html b/src/main/resources/templates/register-success.html index 9eaae8f..9fa0002 100644 --- a/src/main/resources/templates/register-success.html +++ b/src/main/resources/templates/register-success.html @@ -14,16 +14,10 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - .wrap { width: 100%; background: #ffffff; padding: 40px 0 36px; } + .wrap { width: 100%; background: #ffffff; padding: 40px 0 30px; } .container { max-width: 680px; margin: 0 auto; padding: 0 22px; } - .brand { text-align: center; margin-bottom: 28px; } - .brand-logo { - height: 36px; - width: auto; - display: inline-block; - } - + .brand-logo { height: 36px; width: auto; display: inline-block; } .page-title { text-align: center; font-size: 40px; @@ -41,40 +35,39 @@ margin: 0 auto 30px; max-width: 560px; } - .lede strong { font-weight: 600; } - - /* 一张大资料卡,左"APPLE 账户"右"站点信息" */ - .info-card { + .card { background-color: #f5f5f7; border-radius: 18px; - padding: 28px 32px 24px; + padding: 28px 32px; margin: 0 0 22px; } - .info-card-title { + .card-caption { font-size: 13px; + line-height: 1.3; color: #6e6e73; - margin-bottom: 14px; + font-weight: 400; + margin-bottom: 8px; letter-spacing: -0.008em; } - .info-cols { - display: table; - width: 100%; - border-spacing: 0; + .card-title { + font-size: 22px; + font-weight: 600; + letter-spacing: -0.022em; + color: #1d1d1f; + margin-bottom: 6px; } - .info-col { - display: table-cell; - vertical-align: top; - width: 50%; - padding-right: 18px; - } - .info-col + .info-col { - border-left: 1px solid #d2d2d7; - padding-left: 22px; - padding-right: 0; + .card-sub { + font-size: 14px; + color: #6e6e73; + line-height: 1.5; } + /* 登录信息字段列表 */ + .field-list { margin-top: 16px; } .field-row { - padding: 8px 0; + padding: 12px 0; + border-top: 1px solid #d2d2d7; } + .field-row:first-child { border-top: none; padding-top: 4px; } .field-label { font-size: 12px; color: #6e6e73; @@ -87,122 +80,65 @@ word-break: break-all; line-height: 1.4; } - .field-value a { - color: #06c; - text-decoration: none; - } + .field-value a { color: #06c; text-decoration: none; } .field-value a:hover { text-decoration: underline; } .field-value.mono { - font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; + font-family: 'SF Mono', 'Menlo', 'Consolas', 'Courier New', monospace; font-size: 14px; + letter-spacing: 0.5px; } - - /* 安全提示卡片 */ - .tips-card { - background-color: #f5f5f7; - border-radius: 18px; - padding: 22px 32px; - margin: 0 0 22px; + /* 安全提示 */ + .tips { margin-top: 6px; font-size: 14px; color: #1d1d1f; line-height: 1.7; } + .tips div { padding-left: 14px; position: relative; } + .tips div:before { + content: "•"; position: absolute; left: 0; color: #6e6e73; } - .tips-caption { - font-size: 13px; - color: #6e6e73; - margin-bottom: 10px; - letter-spacing: -0.008em; - } - .tips-list { - list-style: none; - font-size: 14px; - color: #1d1d1f; - line-height: 1.7; - } - .tips-list li { - padding-left: 14px; - position: relative; - } - .tips-list li:before { - content: "•"; - position: absolute; - left: 0; - color: #6e6e73; - } - - /* CTA 文字链接(仿 Apple 的"管理账户"链接样式) */ - .cta { - text-align: center; - margin: 32px 0 6px; - } - .cta a { - font-size: 17px; - color: #06c; + /* 登录按钮 */ + .btn-wrap { text-align: center; margin: 30px 0 8px; } + .btn { + display: inline-block; + background-color: #1d1d1f; + color: #ffffff; + font-size: 15px; + font-weight: 600; + letter-spacing: -0.01em; + padding: 13px 36px; + border-radius: 980px; text-decoration: none; - letter-spacing: -0.022em; } - .cta a:hover { text-decoration: underline; } - .cta-sub { + .btn:hover { opacity: 0.9; } + /* 时间戳 */ + .send-time { text-align: center; - font-size: 13px; + font-size: 12px; color: #6e6e73; - margin-top: 6px; + margin-top: 18px; } - - /* 帮助条 */ - .help { - text-align: center; - font-size: 13px; - color: #6e6e73; - margin: 26px auto 0; - max-width: 560px; - line-height: 1.6; - } - .help a { color: #06c; text-decoration: none; } - .help a:hover { text-decoration: underline; } - /* 底部 */ .footer { text-align: center; - margin-top: 44px; - padding-top: 22px; + margin-top: 40px; + padding-top: 24px; border-top: 1px solid #d2d2d7; } .footer-brand { font-size: 14px; font-weight: 600; color: #1d1d1f; - margin-bottom: 10px; + margin-bottom: 12px; letter-spacing: -0.016em; } - .footer-links { - font-size: 13px; - line-height: 1.8; - } - .footer-links a { - color: #6e6e73; - margin: 0 14px; - text-decoration: none; - } + .footer-links { font-size: 13px; color: #6e6e73; line-height: 1.8; } + .footer-links a { color: #6e6e73; margin: 0 14px; text-decoration: none; } .footer-links a:hover { color: #1d1d1f; text-decoration: underline; } - .copyright { - font-size: 12px; - color: #6e6e73; - margin-top: 14px; - line-height: 1.4; - } + .copyright { font-size: 12px; color: #6e6e73; margin-top: 16px; line-height: 1.4; } .copyright a { color: #6e6e73; } @media (max-width: 600px) { .page-title { font-size: 30px; } .lede { font-size: 16px; } + .card { padding: 22px 22px; border-radius: 16px; } .container { padding: 0 16px; } - .info-card, .tips-card { padding: 22px 22px; border-radius: 16px; } - .info-cols { display: block; } - .info-col { display: block; width: 100%; padding: 0; } - .info-col + .info-col { - border-left: none; - border-top: 1px solid #d2d2d7; - padding: 12px 0 0; - margin-top: 12px; - } } @@ -216,68 +152,58 @@

开通成功

-

- 您好!您的企业官网「${siteName!}」已开通成功。 -

+

恭喜!您的企业官网已开通成功,可使用下方信息登录管理后台。

- -
-
账户信息
-
-
-
-
官网地址
- -
-
-
后台地址
- -
+ +
+
站点信息
+
企业官网「${siteName!}」
+
请妥善保管登录信息,首次登录后建议立即修改密码。
+ +
+
+
官网地址
+
-
-
-
登录账号
-
${account!}
-
-
-
登录密码
-
${password!}
-
+
+
后台地址
+ +
+
+
登录账号
+
${account!}
+
+
+
登录密码
+
${password!}
-
-
安全提示
-
    -
  • 请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。
  • -
  • 建议首次登录后立即进入「个人中心 > 修改密码」重置密码。
  • -
  • 如发现可疑登录或账号被盗,请立即联系客服并修改密码。
  • -
+
+
安全提示
+
+
请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。
+
建议首次登录后立即进入「个人中心 > 修改密码」重置密码。
+
如发现可疑登录或账号被盗,请立即联系客服并修改密码。
+
- -
- 点击登录后台 → + + -
如按钮未生效,请将上面的后台地址复制到浏览器打开。
- -

- 如有疑问,请联系客服或访问 帮助中心。 -

+ +
发送时间:${sendTime!}