feat: upgrade verification code email with bilingual HTML template
- Chinese domains (qq.com, 163.com, etc.) receive a Chinese email - All other domains receive an English email - Prominent code display: 40px monospace with wide letter-spacing - Clean OpenAI-inspired layout with dark header and card design Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,8 +130,7 @@ func (svc *UserService) SendEmailVerifyCode(ctx context.Context, emailAddr strin
|
|||||||
|
|
||||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||||
|
|
||||||
subject := "Your verification code"
|
subject, body := email.BuildVerifyCodeEmail(emailAddr, code)
|
||||||
body := fmt.Sprintf("Your verification code is: %s\nIt will expire in 10 minutes.", code)
|
|
||||||
if err := email.Send(ctx, emailAddr, subject, body); err != nil {
|
if err := email.Send(ctx, emailAddr, subject, body); err != nil {
|
||||||
log.Error(ctx, "func", "SendEmailVerifyCode", "msg", "send email error", "error", err)
|
log.Error(ctx, "func", "SendEmailVerifyCode", "msg", "send email error", "error", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
166
pkg/email/template.go
Normal file
166
pkg/email/template.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package email
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// BuildVerifyCodeEmail returns a locale-appropriate subject and HTML body for
|
||||||
|
// the verification code email. Chinese domains get a Chinese email; all others
|
||||||
|
// get an English one.
|
||||||
|
func BuildVerifyCodeEmail(toEmail, code string) (subject, body string) {
|
||||||
|
domain := toEmail[lastIndex(toEmail, '@')+1:]
|
||||||
|
if chineseDomainRe.MatchString(domain) {
|
||||||
|
return buildVerifyCodeZH(code)
|
||||||
|
}
|
||||||
|
return buildVerifyCodeEN(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVerifyCodeZH(code string) (subject, body string) {
|
||||||
|
subject = "您的验证码"
|
||||||
|
body = fmt.Sprintf(`<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>验证码</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
||||||
|
<table width="100%%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;padding:40px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="480" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td style="background:#0a0a0a;padding:32px 40px;">
|
||||||
|
<span style="color:#ffffff;font-size:20px;font-weight:700;letter-spacing:-0.3px;">TexPixel</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:40px 40px 32px;">
|
||||||
|
<p style="margin:0 0 8px;font-size:24px;font-weight:700;color:#0a0a0a;line-height:1.3;">验证您的邮箱</p>
|
||||||
|
<p style="margin:0 0 32px;font-size:15px;color:#6b7280;line-height:1.6;">
|
||||||
|
请使用以下验证码完成注册。验证码仅对您本人有效,请勿分享给他人。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Code block -->
|
||||||
|
<table width="100%%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:28px 0;">
|
||||||
|
<span style="font-size:40px;font-weight:700;letter-spacing:12px;color:#0a0a0a;font-family:'Courier New',Courier,monospace;">%s</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin:24px 0 0;font-size:13px;color:#9ca3af;text-align:center;">
|
||||||
|
验证码 <strong>10 分钟</strong>内有效,请尽快使用
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 40px;">
|
||||||
|
<div style="height:1px;background:#f3f4f6;"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 40px 32px;">
|
||||||
|
<p style="margin:0;font-size:12px;color:#9ca3af;line-height:1.7;">
|
||||||
|
如果您没有请求此验证码,可以忽略本邮件,您的账户仍然安全。<br/>
|
||||||
|
© 2025 TexPixel. 保留所有权利。
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`, code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVerifyCodeEN(code string) (subject, body string) {
|
||||||
|
subject = "Your verification code"
|
||||||
|
body = fmt.Sprintf(`<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Verification Code</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
||||||
|
<table width="100%%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;padding:40px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="480" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.08);">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td style="background:#0a0a0a;padding:32px 40px;">
|
||||||
|
<span style="color:#ffffff;font-size:20px;font-weight:700;letter-spacing:-0.3px;">TexPixel</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:40px 40px 32px;">
|
||||||
|
<p style="margin:0 0 8px;font-size:24px;font-weight:700;color:#0a0a0a;line-height:1.3;">Verify your email address</p>
|
||||||
|
<p style="margin:0 0 32px;font-size:15px;color:#6b7280;line-height:1.6;">
|
||||||
|
Use the verification code below to complete your registration. Never share this code with anyone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Code block -->
|
||||||
|
<table width="100%%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:28px 0;">
|
||||||
|
<span style="font-size:40px;font-weight:700;letter-spacing:12px;color:#0a0a0a;font-family:'Courier New',Courier,monospace;">%s</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin:24px 0 0;font-size:13px;color:#9ca3af;text-align:center;">
|
||||||
|
This code expires in <strong>10 minutes</strong>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:0 40px;">
|
||||||
|
<div style="height:1px;background:#f3f4f6;"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 40px 32px;">
|
||||||
|
<p style="margin:0;font-size:12px;color:#9ca3af;line-height:1.7;">
|
||||||
|
If you didn’t request this code, you can safely ignore this email. Your account is still secure.<br/>
|
||||||
|
© 2025 TexPixel. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`, code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastIndex returns the last index of sep in s, or -1.
|
||||||
|
func lastIndex(s string, sep byte) int {
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
if s[i] == sep {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user