713 lines
19 KiB
Go
713 lines
19 KiB
Go
|
|
package email
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"crypto/tls"
|
|||
|
|
"fmt"
|
|||
|
|
"html/template"
|
|||
|
|
"net"
|
|||
|
|
"net/smtp"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
|
|||
|
|
"tyapi-server/internal/config"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// QQEmailService QQ邮箱服务
|
|||
|
|
type QQEmailService struct {
|
|||
|
|
config config.EmailConfig
|
|||
|
|
logger *zap.Logger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// EmailData 邮件数据
|
|||
|
|
type EmailData struct {
|
|||
|
|
To string `json:"to"`
|
|||
|
|
Subject string `json:"subject"`
|
|||
|
|
Content string `json:"content"`
|
|||
|
|
Data map[string]interface{} `json:"data"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// InvoiceEmailData 发票邮件数据
|
|||
|
|
type InvoiceEmailData struct {
|
|||
|
|
CompanyName string `json:"company_name"`
|
|||
|
|
Amount string `json:"amount"`
|
|||
|
|
InvoiceType string `json:"invoice_type"`
|
|||
|
|
FileURL string `json:"file_url"`
|
|||
|
|
FileName string `json:"file_name"`
|
|||
|
|
ReceivingEmail string `json:"receiving_email"`
|
|||
|
|
ApprovedAt string `json:"approved_at"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewQQEmailService 创建QQ邮箱服务
|
|||
|
|
func NewQQEmailService(config config.EmailConfig, logger *zap.Logger) *QQEmailService {
|
|||
|
|
return &QQEmailService{
|
|||
|
|
config: config,
|
|||
|
|
logger: logger,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SendEmail 发送邮件
|
|||
|
|
func (s *QQEmailService) SendEmail(ctx context.Context, data *EmailData) error {
|
|||
|
|
s.logger.Info("开始发送邮件",
|
|||
|
|
zap.String("to", data.To),
|
|||
|
|
zap.String("subject", data.Subject),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 构建邮件内容
|
|||
|
|
message := s.buildEmailMessage(data)
|
|||
|
|
|
|||
|
|
// 发送邮件
|
|||
|
|
err := s.sendSMTP(data.To, data.Subject, message)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Error("发送邮件失败",
|
|||
|
|
zap.String("to", data.To),
|
|||
|
|
zap.String("subject", data.Subject),
|
|||
|
|
zap.Error(err),
|
|||
|
|
)
|
|||
|
|
return fmt.Errorf("发送邮件失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.logger.Info("邮件发送成功",
|
|||
|
|
zap.String("to", data.To),
|
|||
|
|
zap.String("subject", data.Subject),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SendInvoiceEmail 发送发票邮件
|
|||
|
|
func (s *QQEmailService) SendInvoiceEmail(ctx context.Context, data *InvoiceEmailData) error {
|
|||
|
|
s.logger.Info("开始发送发票邮件",
|
|||
|
|
zap.String("to", data.ReceivingEmail),
|
|||
|
|
zap.String("company_name", data.CompanyName),
|
|||
|
|
zap.String("amount", data.Amount),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 构建邮件内容
|
|||
|
|
subject := "您的发票已开具成功"
|
|||
|
|
content := s.buildInvoiceEmailContent(data)
|
|||
|
|
|
|||
|
|
emailData := &EmailData{
|
|||
|
|
To: data.ReceivingEmail,
|
|||
|
|
Subject: subject,
|
|||
|
|
Content: content,
|
|||
|
|
Data: map[string]interface{}{
|
|||
|
|
"company_name": data.CompanyName,
|
|||
|
|
"amount": data.Amount,
|
|||
|
|
"invoice_type": data.InvoiceType,
|
|||
|
|
"file_url": data.FileURL,
|
|||
|
|
"file_name": data.FileName,
|
|||
|
|
"approved_at": data.ApprovedAt,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return s.SendEmail(ctx, emailData)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// buildEmailMessage 构建邮件消息
|
|||
|
|
func (s *QQEmailService) buildEmailMessage(data *EmailData) string {
|
|||
|
|
headers := make(map[string]string)
|
|||
|
|
headers["From"] = s.config.FromEmail
|
|||
|
|
headers["To"] = data.To
|
|||
|
|
headers["Subject"] = data.Subject
|
|||
|
|
headers["MIME-Version"] = "1.0"
|
|||
|
|
headers["Content-Type"] = "text/html; charset=UTF-8"
|
|||
|
|
|
|||
|
|
var message strings.Builder
|
|||
|
|
for key, value := range headers {
|
|||
|
|
message.WriteString(fmt.Sprintf("%s: %s\r\n", key, value))
|
|||
|
|
}
|
|||
|
|
message.WriteString("\r\n")
|
|||
|
|
message.WriteString(data.Content)
|
|||
|
|
|
|||
|
|
return message.String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// buildInvoiceEmailContent 构建发票邮件内容
|
|||
|
|
func (s *QQEmailService) buildInvoiceEmailContent(data *InvoiceEmailData) string {
|
|||
|
|
htmlTemplate := `
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html>
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<title>发票开具成功通知</title>
|
|||
|
|
<style>
|
|||
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|||
|
|
|
|||
|
|
* {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
font-family: 'Inter', 'Microsoft YaHei', Arial, sans-serif;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
color: #2d3748;
|
|||
|
|
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
|
|||
|
|
min-height: 100vh;
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.container {
|
|||
|
|
max-width: 650px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
background: #ffffff;
|
|||
|
|
border-radius: 24px;
|
|||
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.08);
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid #e2e8f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header {
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
color: white;
|
|||
|
|
padding: 50px 40px 40px;
|
|||
|
|
text-align: center;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: -50%;
|
|||
|
|
left: -50%;
|
|||
|
|
width: 200%;
|
|||
|
|
height: 200%;
|
|||
|
|
background: radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%);
|
|||
|
|
animation: float 6s ease-in-out infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes float {
|
|||
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
|||
|
|
50% { transform: translateY(-20px) rotate(180deg); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.success-icon {
|
|||
|
|
font-size: 48px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
opacity: 0.9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header h1 {
|
|||
|
|
font-size: 24px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
margin: 0;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
letter-spacing: 0.5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content {
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.greeting {
|
|||
|
|
padding: 40px 40px 20px;
|
|||
|
|
text-align: center;
|
|||
|
|
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.greeting p {
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: #4a5568;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
font-weight: 400;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.access-section {
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
padding: 40px;
|
|||
|
|
text-align: center;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
margin: 0 20px 30px;
|
|||
|
|
border-radius: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.access-section::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: -50%;
|
|||
|
|
left: -50%;
|
|||
|
|
width: 200%;
|
|||
|
|
height: 200%;
|
|||
|
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
|||
|
|
animation: shimmer 8s ease-in-out infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes shimmer {
|
|||
|
|
0%, 100% { transform: translateX(-100%) translateY(-100%) rotate(0deg); }
|
|||
|
|
50% { transform: translateX(100%) translateY(100%) rotate(180deg); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.access-section h3 {
|
|||
|
|
color: white;
|
|||
|
|
font-size: 22px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.access-section p {
|
|||
|
|
color: rgba(255, 255, 255, 0.9);
|
|||
|
|
margin-bottom: 25px;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
font-size: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.access-btn {
|
|||
|
|
display: inline-block;
|
|||
|
|
background: rgba(255, 255, 255, 0.15);
|
|||
|
|
color: white;
|
|||
|
|
padding: 16px 32px;
|
|||
|
|
text-decoration: none;
|
|||
|
|
border-radius: 50px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
font-size: 15px;
|
|||
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
letter-spacing: 0.3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.access-btn:hover {
|
|||
|
|
background: rgba(255, 255, 255, 0.25);
|
|||
|
|
transform: translateY(-3px);
|
|||
|
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
|
|||
|
|
border-color: rgba(255, 255, 255, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-section {
|
|||
|
|
padding: 0 40px 40px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|||
|
|
gap: 20px;
|
|||
|
|
margin: 30px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-item {
|
|||
|
|
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
|
|||
|
|
padding: 24px;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
border: 1px solid #e2e8f0;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-item:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 12px 25px -5px rgba(102, 126, 234, 0.1);
|
|||
|
|
border-color: #cbd5e0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-item::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 4px;
|
|||
|
|
height: 100%;
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
border-radius: 0 2px 2px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-label {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #718096;
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 0.8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-value {
|
|||
|
|
color: #2d3748;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-section {
|
|||
|
|
background: linear-gradient(135deg, #f0fff4 0%, #ffffff 100%);
|
|||
|
|
padding: 30px;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
margin: 30px 0;
|
|||
|
|
border: 1px solid #c6f6d5;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-section::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 4px;
|
|||
|
|
height: 100%;
|
|||
|
|
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
|||
|
|
border-radius: 0 2px 2px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-section h4 {
|
|||
|
|
color: #2f855a;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-section h4::before {
|
|||
|
|
content: '📋';
|
|||
|
|
margin-right: 8px;
|
|||
|
|
font-size: 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-section ul {
|
|||
|
|
list-style: none;
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-section li {
|
|||
|
|
color: #4a5568;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
padding-left: 24px;
|
|||
|
|
position: relative;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notes-section li::before {
|
|||
|
|
content: '✓';
|
|||
|
|
color: #48bb78;
|
|||
|
|
font-weight: bold;
|
|||
|
|
position: absolute;
|
|||
|
|
left: 0;
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer {
|
|||
|
|
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
|||
|
|
color: rgba(255, 255, 255, 0.8);
|
|||
|
|
padding: 35px 40px;
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer p {
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer-divider {
|
|||
|
|
width: 60px;
|
|||
|
|
height: 2px;
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
margin: 20px auto;
|
|||
|
|
border-radius: 1px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 600px) {
|
|||
|
|
.container {
|
|||
|
|
margin: 10px;
|
|||
|
|
border-radius: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header {
|
|||
|
|
padding: 40px 30px 30px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.greeting {
|
|||
|
|
padding: 30px 30px 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.access-section {
|
|||
|
|
margin: 0 15px 25px;
|
|||
|
|
padding: 30px 25px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-section {
|
|||
|
|
padding: 0 30px 30px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-grid {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.footer {
|
|||
|
|
padding: 30px 30px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="header">
|
|||
|
|
<div class="success-icon">✓</div>
|
|||
|
|
<h1>发票已开具完成</h1>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="content">
|
|||
|
|
<div class="greeting">
|
|||
|
|
<p>尊敬的用户,您好!</p>
|
|||
|
|
<p>您的发票申请已审核通过,发票已成功开具。</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="access-section">
|
|||
|
|
<h3>📄 发票访问链接</h3>
|
|||
|
|
<p>您的发票已准备就绪,请点击下方按钮访问查看页面</p>
|
|||
|
|
<a href="{{.FileURL}}" class="access-btn" target="_blank">
|
|||
|
|
🔗 访问发票页面
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="info-section">
|
|||
|
|
<div class="info-grid">
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span class="info-label">公司名称</span>
|
|||
|
|
<span class="info-value">{{.CompanyName}}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span class="info-label">发票金额</span>
|
|||
|
|
<span class="info-value">¥{{.Amount}}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span class="info-label">发票类型</span>
|
|||
|
|
<span class="info-value">{{.InvoiceType}}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="info-item">
|
|||
|
|
<span class="info-label">开具时间</span>
|
|||
|
|
<span class="info-value">{{.ApprovedAt}}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="notes-section">
|
|||
|
|
<h4>注意事项</h4>
|
|||
|
|
<ul>
|
|||
|
|
<li>访问页面后可在页面内下载发票文件</li>
|
|||
|
|
<li>请妥善保管发票文件,建议打印存档</li>
|
|||
|
|
<li>如有疑问,请回到我们平台进行下载</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="footer">
|
|||
|
|
<p>此邮件由系统自动发送,请勿回复</p>
|
|||
|
|
<div class="footer-divider"></div>
|
|||
|
|
<p>天远数据 API 服务平台</p>
|
|||
|
|
<p>发送时间:{{.CurrentTime}}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</body>
|
|||
|
|
</html>`
|
|||
|
|
|
|||
|
|
// 解析模板
|
|||
|
|
tmpl, err := template.New("invoice_email").Parse(htmlTemplate)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Error("解析邮件模板失败", zap.Error(err))
|
|||
|
|
return s.buildSimpleInvoiceEmail(data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 准备模板数据
|
|||
|
|
templateData := struct {
|
|||
|
|
CompanyName string
|
|||
|
|
Amount string
|
|||
|
|
InvoiceType string
|
|||
|
|
FileURL string
|
|||
|
|
FileName string
|
|||
|
|
ApprovedAt string
|
|||
|
|
CurrentTime string
|
|||
|
|
Domain string
|
|||
|
|
}{
|
|||
|
|
CompanyName: data.CompanyName,
|
|||
|
|
Amount: data.Amount,
|
|||
|
|
InvoiceType: data.InvoiceType,
|
|||
|
|
FileURL: data.FileURL,
|
|||
|
|
FileName: data.FileName,
|
|||
|
|
ApprovedAt: data.ApprovedAt,
|
|||
|
|
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
|||
|
|
Domain: s.config.Domain,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行模板
|
|||
|
|
var content strings.Builder
|
|||
|
|
err = tmpl.Execute(&content, templateData)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Error("执行邮件模板失败", zap.Error(err))
|
|||
|
|
return s.buildSimpleInvoiceEmail(data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return content.String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// buildSimpleInvoiceEmail 构建简单的发票邮件内容(备用方案)
|
|||
|
|
func (s *QQEmailService) buildSimpleInvoiceEmail(data *InvoiceEmailData) string {
|
|||
|
|
return fmt.Sprintf(`
|
|||
|
|
发票开具成功通知
|
|||
|
|
|
|||
|
|
尊敬的用户,您好!
|
|||
|
|
|
|||
|
|
您的发票申请已审核通过,发票已成功开具。
|
|||
|
|
|
|||
|
|
发票信息:
|
|||
|
|
- 公司名称:%s
|
|||
|
|
- 发票金额:¥%s
|
|||
|
|
- 发票类型:%s
|
|||
|
|
- 开具时间:%s
|
|||
|
|
|
|||
|
|
发票文件下载链接:%s
|
|||
|
|
文件名:%s
|
|||
|
|
|
|||
|
|
如有疑问,请访问控制台查看详细信息:https://%s
|
|||
|
|
|
|||
|
|
天远数据 API 服务平台
|
|||
|
|
%s
|
|||
|
|
`, data.CompanyName, data.Amount, data.InvoiceType, data.ApprovedAt, data.FileURL, data.FileName, s.config.Domain, time.Now().Format("2006-01-02 15:04:05"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// sendSMTP 通过SMTP发送邮件
|
|||
|
|
func (s *QQEmailService) sendSMTP(to, subject, message string) error {
|
|||
|
|
// 构建认证信息
|
|||
|
|
auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host)
|
|||
|
|
|
|||
|
|
// 构建收件人列表
|
|||
|
|
toList := []string{to}
|
|||
|
|
|
|||
|
|
// 发送邮件
|
|||
|
|
if s.config.UseSSL {
|
|||
|
|
// QQ邮箱587端口使用STARTTLS,465端口使用直接SSL
|
|||
|
|
if s.config.Port == 587 {
|
|||
|
|
// 使用STARTTLS (587端口)
|
|||
|
|
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port))
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("连接SMTP服务器失败: %w", err)
|
|||
|
|
}
|
|||
|
|
defer conn.Close()
|
|||
|
|
|
|||
|
|
client, err := smtp.NewClient(conn, s.config.Host)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("创建SMTP客户端失败: %w", err)
|
|||
|
|
}
|
|||
|
|
defer client.Close()
|
|||
|
|
|
|||
|
|
// 启用STARTTLS
|
|||
|
|
if err = client.StartTLS(&tls.Config{
|
|||
|
|
ServerName: s.config.Host,
|
|||
|
|
InsecureSkipVerify: false,
|
|||
|
|
}); err != nil {
|
|||
|
|
return fmt.Errorf("启用STARTTLS失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 认证
|
|||
|
|
if err = client.Auth(auth); err != nil {
|
|||
|
|
return fmt.Errorf("SMTP认证失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置发件人
|
|||
|
|
if err = client.Mail(s.config.FromEmail); err != nil {
|
|||
|
|
return fmt.Errorf("设置发件人失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置收件人
|
|||
|
|
for _, recipient := range toList {
|
|||
|
|
if err = client.Rcpt(recipient); err != nil {
|
|||
|
|
return fmt.Errorf("设置收件人失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送邮件内容
|
|||
|
|
writer, err := client.Data()
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("准备发送邮件内容失败: %w", err)
|
|||
|
|
}
|
|||
|
|
defer writer.Close()
|
|||
|
|
|
|||
|
|
_, err = writer.Write([]byte(message))
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("发送邮件内容失败: %w", err)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 使用直接SSL连接 (465端口)
|
|||
|
|
tlsConfig := &tls.Config{
|
|||
|
|
ServerName: s.config.Host,
|
|||
|
|
InsecureSkipVerify: false,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), tlsConfig)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("连接SMTP服务器失败: %w", err)
|
|||
|
|
}
|
|||
|
|
defer conn.Close()
|
|||
|
|
|
|||
|
|
client, err := smtp.NewClient(conn, s.config.Host)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("创建SMTP客户端失败: %w", err)
|
|||
|
|
}
|
|||
|
|
defer client.Close()
|
|||
|
|
|
|||
|
|
// 认证
|
|||
|
|
if err = client.Auth(auth); err != nil {
|
|||
|
|
return fmt.Errorf("SMTP认证失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置发件人
|
|||
|
|
if err = client.Mail(s.config.FromEmail); err != nil {
|
|||
|
|
return fmt.Errorf("设置发件人失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置收件人
|
|||
|
|
for _, recipient := range toList {
|
|||
|
|
if err = client.Rcpt(recipient); err != nil {
|
|||
|
|
return fmt.Errorf("设置收件人失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送邮件内容
|
|||
|
|
writer, err := client.Data()
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("准备发送邮件内容失败: %w", err)
|
|||
|
|
}
|
|||
|
|
defer writer.Close()
|
|||
|
|
|
|||
|
|
_, err = writer.Write([]byte(message))
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("发送邮件内容失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 使用普通连接
|
|||
|
|
err := smtp.SendMail(
|
|||
|
|
fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
|
|||
|
|
auth,
|
|||
|
|
s.config.FromEmail,
|
|||
|
|
toList,
|
|||
|
|
[]byte(message),
|
|||
|
|
)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("发送邮件失败: %w", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|