feat():learning后台管理项目初始化

This commit is contained in:
yuj
2025-12-04 16:23:46 +08:00
parent 39886d50d2
commit 88e048f4d1
154 changed files with 28966 additions and 6 deletions

View File

@@ -0,0 +1,443 @@
package services
import (
"bytes"
"crypto/tls"
"fmt"
"html/template"
"math"
"net/smtp"
"time"
"github.com/jordan-wright/email"
"goalfymax-admin/internal/config"
)
type EmailService struct {
host string
port int
username string
password string
sender string
inviteURLPrefix string
}
// NewEmailService 创建邮件服务实例
func NewEmailService() *EmailService {
cfg := config.GetConfig()
return &EmailService{
host: cfg.Email.Host,
port: cfg.Email.Port,
username: cfg.Email.Username,
password: cfg.Email.Password,
sender: cfg.Email.Sender,
inviteURLPrefix: cfg.Email.InviteURLPrefix,
}
}
// SendInviteCodeApprovalEmail 发送邀请码审批通过邮件
func (s *EmailService) SendInviteCodeApprovalEmail(toEmail, inviteCode, language string, expiresAt *time.Time) error {
var subject string
if language == "en" {
subject = "Your GoalfyAI Beta Access Invitation"
} else {
subject = "GoalfyAI 内测邀请函"
}
// 构造邮件内容
htmlContent := s.generateApprovalEmailHTML(inviteCode, language, expiresAt)
return s.sendEmail(toEmail, subject, htmlContent)
}
// SendInviteCodeRejectionEmail 发送邀请码申请拒绝邮件
func (s *EmailService) SendInviteCodeRejectionEmail(toEmail, rejectReason string) error {
subject := "关于您的 GoalfyAI 申请"
// 构造邮件内容
htmlContent := s.generateRejectionEmailHTML(rejectReason)
return s.sendEmail(toEmail, subject, htmlContent)
}
// generateApprovalEmailHTML 生成审批通过的邮件HTML
func (s *EmailService) generateApprovalEmailHTML(inviteCode, language string, expiresAt *time.Time) string {
if language == "en" {
return s.GenerateApprovalEmailEN(inviteCode, expiresAt)
}
return s.GenerateApprovalEmailZH(inviteCode, expiresAt)
}
// formatExpiryTimeEN 格式化过期时间为英文显示(全部显示为小时,向上取整)
func formatExpiryTimeEN(expiresAt *time.Time) string {
if expiresAt == nil {
return "until used"
}
now := time.Now()
if expiresAt.Before(now) {
return "expired"
}
duration := expiresAt.Sub(now)
hours := int(math.Ceil(duration.Hours())) // 向上取整
if hours <= 0 {
hours = 1 // 不足一小时算一小时
}
if hours == 1 {
return "1 hour"
}
return fmt.Sprintf("%d hours", hours)
}
// formatExpiryTimeZH 格式化过期时间为中文显示(全部显示为小时,向上取整)
func formatExpiryTimeZH(expiresAt *time.Time) string {
if expiresAt == nil {
return "永久有效"
}
now := time.Now()
if expiresAt.Before(now) {
return "已过期"
}
duration := expiresAt.Sub(now)
hours := int(math.Ceil(duration.Hours())) // 向上取整
if hours <= 0 {
hours = 1 // 不足一小时算一小时
}
return fmt.Sprintf("%d小时", hours)
}
// GenerateApprovalEmailEN 生成英文版审批通过邮件(导出用于测试)
func (s *EmailService) GenerateApprovalEmailEN(inviteCode string, expiresAt *time.Time) string {
expiryHours := formatExpiryTimeEN(expiresAt)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<p>Thank you again for your interest in GoalfyAI!</p>
<p>We're excited to let you know that your request for beta access has been approved.<br>
You can now activate your GoalfyAI account using the link below:</p>
<p>👉 <a href="{{.InviteURL}}">Activate Your Account</a><br>
<span style="color: #666; font-size: 14px;">(This link is valid for {{.ExpiryHours}})</span></p>
<p>With this invite, you'll be among the first to explore our intelligent task execution system—designed for long-range, professional workflows. We'd love to hear your feedback as we continue to refine the experience.</p>
<p>Need help getting started? Visit our website for tips, use cases, and product updates:<br>
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
<p>Thanks again for joining us on this journey.<br>
Let's build the future of intelligent tasks—together.</p>
<p>Warm regards,<br>
The GoalfyAI Team</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
This email is sent automatically. Please do not reply.<br>
For any questions, please contact <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
</p>
</body>
</html>
`
tmpl, _ := template.New("approval_en").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"InviteCode": inviteCode,
"ExpiryHours": expiryHours,
"InviteURL": s.inviteURLPrefix + inviteCode,
})
return buf.String()
}
// GenerateApprovalEmailZH 生成中文版审批通过邮件(导出用于测试)
func (s *EmailService) GenerateApprovalEmailZH(inviteCode string, expiresAt *time.Time) string {
expiryHours := formatExpiryTimeZH(expiresAt)
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: 'Microsoft YaHei', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<p>感谢您对 GoalfyAI 的关注与支持!</p>
<p>我们很高兴通知您,您的内测申请已通过审核。<br>
请通过以下链接激活您的 GoalfyAI 账户:</p>
<p>👉 <a href="{{.InviteURL}}">点击激活账户</a><br>
<span style="color: #666; font-size: 14px;">(该链接在 {{.ExpiryHours}} 内有效)</span></p>
<p>通过本次邀请,您将率先体验我们为长周期专业任务打造的智能任务系统。我们也非常欢迎您在使用过程中给予反馈,帮助我们持续优化产品体验。</p>
<p>如需了解更多使用建议、典型场景或最新进展,欢迎访问官网:<br>
🌐 <a href="https://www.goalfyai.com">GoalfyAI.com</a></p>
<p>感谢您的加入,<br>
让我们一同开启智能任务的新篇章!</p>
<p>此致,<br>
GoalfyAI 团队</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 40px 0;">
<p style="text-align: center; color: #999; font-size: 12px; line-height: 1.5;">
本邮件为自动化发送,请勿回复。<br>
如有疑问请联系 <a href="mailto:hi@goalfyai.com" style="color: #999;">hi@goalfyai.com</a>
</p>
</body>
</html>
`
tmpl, _ := template.New("approval_zh").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"InviteCode": inviteCode,
"ExpiryHours": expiryHours,
"InviteURL": s.inviteURLPrefix + inviteCode,
})
return buf.String()
}
// generateRejectionEmailHTML 生成申请拒绝的邮件HTML
func (s *EmailService) generateRejectionEmailHTML(rejectReason string) string {
if rejectReason == "" {
rejectReason = "感谢您对 GoalfyAI 的关注。经过审核,您的账户申请暂未通过。"
}
tmplStr := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
color: #1a1a1a;
background-color: #f5f5f5;
padding: 40px 20px;
}
.email-wrapper {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.header {
background-color: #000000;
padding: 32px 40px;
text-align: center;
}
.header-logo {
font-size: 28px;
font-weight: 700;
color: #ffffff;
letter-spacing: -0.5px;
}
.content {
padding: 40px;
}
.greeting {
font-size: 16px;
color: #1a1a1a;
margin-bottom: 24px;
}
.message {
font-size: 15px;
color: #4a4a4a;
margin-bottom: 24px;
line-height: 1.7;
}
.reason-box {
background-color: #fafafa;
border-left: 3px solid #4a4a4a;
padding: 20px 24px;
margin: 24px 0;
border-radius: 0 4px 4px 0;
}
.reason-box p {
font-size: 15px;
color: #1a1a1a;
line-height: 1.7;
}
.support-box {
background-color: #fafafa;
border-radius: 6px;
padding: 20px 24px;
margin-top: 32px;
text-align: center;
}
.support-box p {
font-size: 14px;
color: #4a4a4a;
margin: 0;
}
.support-box a {
color: #000000;
text-decoration: none;
font-weight: 600;
}
.footer {
background-color: #fafafa;
padding: 32px 40px;
text-align: center;
border-top: 1px solid #e5e5e5;
}
.footer-brand {
font-size: 14px;
color: #1a1a1a;
font-weight: 600;
margin-bottom: 8px;
}
.footer-contact {
font-size: 13px;
color: #666666;
margin-top: 8px;
}
.footer-contact a {
color: #000000;
text-decoration: none;
}
@media only screen and (max-width: 600px) {
.content {
padding: 24px 20px;
}
.header {
padding: 24px 20px;
}
.reason-box {
padding: 16px 20px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
<div class="header">
<div class="header-logo">GoalfyAI</div>
</div>
<div class="content">
<div class="greeting">您好,</div>
<div class="message">
感谢您对 GoalfyAI 的关注和申请。
</div>
<div class="reason-box">
<p>{{.RejectReason}}</p>
</div>
<div class="message" style="margin-top: 24px;">
我们期待未来有机会为您提供服务。
</div>
<div class="support-box">
<p>如有任何疑问,欢迎联系我们</p>
<p style="margin-top: 8px;">
<a href="mailto:goalfymax@goalfyai.com">goalfymax@goalfyai.com</a>
</p>
</div>
</div>
<div class="footer">
<div class="footer-brand">GoalfyAI 团队</div>
<div class="footer-contact">
© 2025 GoalfyAI. All rights reserved.
</div>
</div>
</div>
</body>
</html>
`
tmpl, _ := template.New("rejection").Parse(tmplStr)
var buf bytes.Buffer
tmpl.Execute(&buf, map[string]string{
"RejectReason": rejectReason,
})
return buf.String()
}
// sendEmail 发送邮件的通用方法
func (s *EmailService) sendEmail(toEmail, subject, htmlContent string) error {
e := email.NewEmail()
e.From = s.sender
e.To = []string{toEmail}
e.Subject = subject
e.HTML = []byte(htmlContent)
// 创建SMTP认证
auth := smtp.PlainAuth("", s.username, s.password, s.host)
// 配置TLS
tlsConfig := &tls.Config{
ServerName: s.host,
}
// 发送邮件
addr := fmt.Sprintf("%s:%d", s.host, s.port)
// 如果是465端口使用SSL
if s.port == 465 {
return e.SendWithTLS(addr, auth, tlsConfig)
}
// 否则使用STARTTLS
return e.SendWithStartTLS(addr, auth, tlsConfig)
}
// SendBatchEmails 批量发送邮件(异步)
func (s *EmailService) SendBatchEmails(emails []string, subject, htmlContent string) []error {
errors := make([]error, len(emails))
for i, email := range emails {
errors[i] = s.sendEmail(email, subject, htmlContent)
// 避免发送过快
time.Sleep(100 * time.Millisecond)
}
return errors
}
// TestConnection 测试邮件服务器连接
func (s *EmailService) TestConnection() error {
addr := fmt.Sprintf("%s:%d", s.host, s.port)
// 尝试连接SMTP服务器
client, err := smtp.Dial(addr)
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer client.Close()
return nil
}