chore: 初始化仓库,排除二进制文件和覆盖率文件
This commit is contained in:
162
pkg/email/email.go
Normal file
162
pkg/email/email.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
|
||||
"carrotskin/pkg/config"
|
||||
|
||||
"github.com/jordan-wright/email"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Service 邮件服务
|
||||
type Service struct {
|
||||
cfg config.EmailConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewService 创建邮件服务
|
||||
func NewService(cfg config.EmailConfig, logger *zap.Logger) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendVerificationCode 发送验证码邮件
|
||||
func (s *Service) SendVerificationCode(to, code, purpose string) error {
|
||||
if !s.cfg.Enabled {
|
||||
s.logger.Warn("邮件服务未启用,跳过发送", zap.String("to", to))
|
||||
return fmt.Errorf("邮件服务未启用")
|
||||
}
|
||||
|
||||
subject := s.getSubject(purpose)
|
||||
body := s.getBody(code, purpose)
|
||||
|
||||
return s.send([]string{to}, subject, body)
|
||||
}
|
||||
|
||||
// SendResetPassword 发送重置密码邮件
|
||||
func (s *Service) SendResetPassword(to, code string) error {
|
||||
return s.SendVerificationCode(to, code, "reset_password")
|
||||
}
|
||||
|
||||
// SendEmailVerification 发送邮箱验证邮件
|
||||
func (s *Service) SendEmailVerification(to, code string) error {
|
||||
return s.SendVerificationCode(to, code, "email_verification")
|
||||
}
|
||||
|
||||
// SendChangeEmail 发送更换邮箱验证码
|
||||
func (s *Service) SendChangeEmail(to, code string) error {
|
||||
return s.SendVerificationCode(to, code, "change_email")
|
||||
}
|
||||
|
||||
// send 发送邮件
|
||||
func (s *Service) send(to []string, subject, body string) error {
|
||||
e := email.NewEmail()
|
||||
e.From = fmt.Sprintf("%s <%s>", s.cfg.FromName, s.cfg.Username)
|
||||
e.To = to
|
||||
e.Subject = subject
|
||||
e.HTML = []byte(body)
|
||||
e.Headers = textproto.MIMEHeader{}
|
||||
|
||||
// SMTP认证
|
||||
auth := smtp.PlainAuth("", s.cfg.Username, s.cfg.Password, s.cfg.SMTPHost)
|
||||
|
||||
// 发送邮件
|
||||
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, s.cfg.SMTPPort)
|
||||
|
||||
// 判断端口决定发送方式
|
||||
// 465端口使用SSL/TLS(隐式TLS)
|
||||
// 587端口使用STARTTLS(显式TLS)
|
||||
var err error
|
||||
if s.cfg.SMTPPort == 465 {
|
||||
// 使用SSL/TLS连接(适用于465端口)
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: s.cfg.SMTPHost,
|
||||
InsecureSkipVerify: false, // 生产环境建议设置为false
|
||||
}
|
||||
err = e.SendWithTLS(addr, auth, tlsConfig)
|
||||
} else {
|
||||
// 使用STARTTLS连接(适用于587端口等)
|
||||
err = e.Send(addr, auth)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("发送邮件失败",
|
||||
zap.Strings("to", to),
|
||||
zap.String("subject", subject),
|
||||
zap.String("smtp_host", s.cfg.SMTPHost),
|
||||
zap.Int("smtp_port", s.cfg.SMTPPort),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("发送邮件失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("邮件发送成功",
|
||||
zap.Strings("to", to),
|
||||
zap.String("subject", subject),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSubject 获取邮件主题
|
||||
func (s *Service) getSubject(purpose string) string {
|
||||
switch purpose {
|
||||
case "email_verification":
|
||||
return "【CarrotSkin】邮箱验证"
|
||||
case "reset_password":
|
||||
return "【CarrotSkin】重置密码"
|
||||
case "change_email":
|
||||
return "【CarrotSkin】更换邮箱验证"
|
||||
default:
|
||||
return "【CarrotSkin】验证码"
|
||||
}
|
||||
}
|
||||
|
||||
// getBody 获取邮件正文
|
||||
func (s *Service) getBody(code, purpose string) string {
|
||||
var message string
|
||||
switch purpose {
|
||||
case "email_verification":
|
||||
message = "感谢注册CarrotSkin!请使用以下验证码完成邮箱验证:"
|
||||
case "reset_password":
|
||||
message = "您正在重置密码,请使用以下验证码:"
|
||||
case "change_email":
|
||||
message = "您正在更换邮箱,请使用以下验证码验证新邮箱:"
|
||||
default:
|
||||
message = "您的验证码为:"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>验证码</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
||||
<div style="max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div style="text-align: center; padding-bottom: 20px;">
|
||||
<h1 style="color: #ff6b35; margin: 0;">CarrotSkin</h1>
|
||||
</div>
|
||||
<div style="padding: 20px 0; border-top: 2px solid #ff6b35; border-bottom: 2px solid #ff6b35;">
|
||||
<p style="font-size: 16px; color: #333; margin: 0 0 20px 0;">%s</p>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; text-align: center; border-radius: 4px; margin: 20px 0;">
|
||||
<span style="font-size: 32px; font-weight: bold; color: #ff6b35; letter-spacing: 5px;">%s</span>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: #666; margin: 20px 0 0 0;">验证码有效期为10分钟,请及时使用。</p>
|
||||
<p style="font-size: 14px; color: #666; margin: 10px 0 0 0;">如果这不是您的操作,请忽略此邮件。</p>
|
||||
</div>
|
||||
<div style="text-align: center; padding-top: 20px;">
|
||||
<p style="font-size: 12px; color: #999; margin: 0;">© 2025 CarrotSkin. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, message, code)
|
||||
}
|
||||
47
pkg/email/manager.go
Normal file
47
pkg/email/manager.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"carrotskin/pkg/config"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
// serviceInstance 全局邮件服务实例
|
||||
serviceInstance *Service
|
||||
// once 确保只初始化一次
|
||||
once sync.Once
|
||||
// initError 初始化错误
|
||||
initError error
|
||||
)
|
||||
|
||||
// Init 初始化邮件服务(线程安全,只会执行一次)
|
||||
func Init(cfg config.EmailConfig, logger *zap.Logger) error {
|
||||
once.Do(func() {
|
||||
serviceInstance = NewService(cfg, logger)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetService 获取邮件服务实例(线程安全)
|
||||
func GetService() (*Service, error) {
|
||||
if serviceInstance == nil {
|
||||
return nil, fmt.Errorf("邮件服务未初始化,请先调用 email.Init()")
|
||||
}
|
||||
return serviceInstance, nil
|
||||
}
|
||||
|
||||
// MustGetService 获取邮件服务实例,如果未初始化则panic
|
||||
func MustGetService() *Service {
|
||||
service, err := GetService()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
61
pkg/email/manager_test.go
Normal file
61
pkg/email/manager_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"carrotskin/pkg/config"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
// TestGetService_NotInitialized 测试未初始化时获取邮件服务
|
||||
func TestGetService_NotInitialized(t *testing.T) {
|
||||
_, err := GetService()
|
||||
if err == nil {
|
||||
t.Error("未初始化时应该返回错误")
|
||||
}
|
||||
|
||||
expectedError := "邮件服务未初始化,请先调用 email.Init()"
|
||||
if err.Error() != expectedError {
|
||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMustGetService_Panic 测试MustGetService在未初始化时panic
|
||||
func TestMustGetService_Panic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("MustGetService 应该在未初始化时panic")
|
||||
}
|
||||
}()
|
||||
|
||||
_ = MustGetService()
|
||||
}
|
||||
|
||||
// TestInit_Email 测试邮件服务初始化
|
||||
func TestInit_Email(t *testing.T) {
|
||||
cfg := config.EmailConfig{
|
||||
Enabled: false,
|
||||
SMTPHost: "smtp.example.com",
|
||||
SMTPPort: 587,
|
||||
Username: "user@example.com",
|
||||
Password: "password",
|
||||
FromName: "noreply@example.com",
|
||||
}
|
||||
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
err := Init(cfg, logger)
|
||||
if err != nil {
|
||||
t.Errorf("Init() 错误 = %v, want nil", err)
|
||||
}
|
||||
|
||||
// 验证可以获取服务
|
||||
service, err := GetService()
|
||||
if err != nil {
|
||||
t.Errorf("GetService() 错误 = %v, want nil", err)
|
||||
}
|
||||
if service == nil {
|
||||
t.Error("GetService() 返回的服务不应为nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user