fix(email): 统一注册成功邮件模板风格及调用逻辑

- 重写 register-success.html,视觉风格与邮箱验证码邮件对齐
- TenantServiceImpl 开通邮件改为发送 HTML 模板邮件,添加 sendTime 字段
- 修改 EmailTemplateUtil.sendRegisterSuccessEmail 方法签名及实现,传递正确模板变量
- 更新 EmailTestController 中的测试接口参数,支持新模板数据
- 修复 MainController 注册成功邮件调用用错参数导致空字段问题,使用兜底值避免编译错误
- 备注:register-success.html 为首次启用,旧逻辑未使用此模板仅发送纯文本邮件
- 测试方式:通过 POST /api/email-test/register-success?email=xxx 预览邮件渲染效果
This commit is contained in:
2026-08-10 21:56:46 +08:00
parent c585647dec
commit 58972885ad
6 changed files with 143 additions and 210 deletions
+20 -17
View File
@@ -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` 预览真实渲染效果。
@@ -29,9 +29,11 @@ public class EmailTestController extends BaseController {
public ApiResult<?> testRegisterSuccessEmail(@RequestParam String email) {
try {
emailTemplateUtil.sendRegisterSuccessEmail(
"测试用户",
"13800138000",
"TestPassword123",
"广西河马科技有限公司",
"https://site-10654.sitelnk.cn",
"https://site.websoft.top",
email,
"F!huhtL3fqD8",
email,
getTenantId()
);
@@ -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));
}
@@ -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<String, Object> 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;
}
@@ -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<String, Object> data = new HashMap<>();
data.put("username", username);
data.put("phone", phone);
String title = "您的企业官网已开通成功";
Map<String, Object> 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);
}
}
@@ -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;
}
}
</style>
</head>
@@ -216,68 +152,58 @@
<!-- Title + Lede -->
<h1 class="page-title">开通成功</h1>
<p class="lede">
您好!您的企业官网「<strong>${siteName!}</strong>」已开通成功。
</p>
<p class="lede">恭喜!您的企业官网已开通成功,可使用下方信息登录管理后台。</p>
<!-- 资料大卡:左站点 / 右账号 -->
<div class="info-card">
<div class="info-card-title">账户信息</div>
<div class="info-cols">
<div class="info-col">
<div class="field-row">
<div class="field-label">官网地址</div>
<div class="field-value">
<a href="${siteUrl!}">${siteUrl!}</a>
</div>
</div>
<div class="field-row">
<div class="field-label">后台地址</div>
<div class="field-value">
<a href="${adminUrl!}">${adminUrl!}</a>
</div>
</div>
<!-- 站点信息 + 登录信息主卡 -->
<div class="card">
<div class="card-caption">站点信息</div>
<div class="card-title">企业官网「${siteName!}」</div>
<div class="card-sub">请妥善保管登录信息,首次登录后建议立即修改密码。</div>
<div class="field-list">
<div class="field-row">
<div class="field-label">官网地址</div>
<div class="field-value"><a href="${siteUrl!}">${siteUrl!}</a></div>
</div>
<div class="info-col">
<div class="field-row">
<div class="field-label">登录账号</div>
<div class="field-value">${account!}</div>
</div>
<div class="field-row">
<div class="field-label">登录密码</div>
<div class="field-value mono">${password!}</div>
</div>
<div class="field-row">
<div class="field-label">后台地址</div>
<div class="field-value"><a href="${adminUrl!}">${adminUrl!}</a></div>
</div>
<div class="field-row">
<div class="field-label">登录账号</div>
<div class="field-value">${account!}</div>
</div>
<div class="field-row">
<div class="field-label">登录密码</div>
<div class="field-value mono">${password!}</div>
</div>
</div>
</div>
<!-- 安全提示 -->
<div class="tips-card">
<div class="tips-caption">安全提示</div>
<ul class="tips-list">
<li>请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。</li>
<li>建议首次登录后立即进入「个人中心 > 修改密码」重置密码。</li>
<li>如发现可疑登录或账号被盗,请立即联系客服并修改密码。</li>
</ul>
<div class="card">
<div class="card-caption">安全提示</div>
<div class="tips">
<div>请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。</div>
<div>建议首次登录后立即进入「个人中心 &gt; 修改密码」重置密码。</div>
<div>如发现可疑登录或账号被盗,请立即联系客服并修改密码。</div>
</div>
</div>
<!-- CTA -->
<div class="cta">
<a href="${adminUrl!}">点击登录后台 →</a>
<!-- 登录按钮 -->
<div class="btn-wrap">
<a href="${adminUrl!}" class="btn">登录管理后台</a>
</div>
<div class="cta-sub">如按钮未生效,请将上面的后台地址复制到浏览器打开。</div>
<!-- 帮助 -->
<p class="help">
如有疑问,请联系客服或访问 <a href="${helpUrl!'https://websoft.top/help'}">帮助中心</a>
</p>
<!-- 时间戳 -->
<div class="send-time">发送时间:${sendTime!}</div>
<!-- Footer -->
<div class="footer">
<div class="footer-brand">WebSoft</div>
<div class="footer-links">
<a href="https://websoft.top">官方网站</a>
<a href="https://websoft.top/help">帮助中心</a>
<a href="${helpUrl!'https://websoft.top/help'}">帮助中心</a>
<a href="https://websoft.top/contact">联系我们</a>
<a href="https://websoft.top/settings">账户设置</a>
</div>