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 := ` 发票开具成功通知

发票已开具完成

尊敬的用户,您好!

您的发票申请已审核通过,发票已成功开具。

📄 发票访问链接

您的发票已准备就绪,请点击下方按钮访问查看页面

🔗 访问发票页面
公司名称 {{.CompanyName}}
发票金额 ¥{{.Amount}}
发票类型 {{.InvoiceType}}
开具时间 {{.ApprovedAt}}

注意事项

  • 访问页面后可在页面内下载发票文件
  • 请妥善保管发票文件,建议打印存档
  • 如有疑问,请回到我们平台进行下载
` // 解析模板 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 }