85 lines
3.0 KiB
Go
85 lines
3.0 KiB
Go
package mail
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"ws_usernode/internal/model"
|
|
)
|
|
|
|
// 邮件状态(mail_logs.status)。
|
|
const (
|
|
StatusPending = "pending" // 已入队,等待直发
|
|
StatusSent = "sent" // 发送成功
|
|
StatusFailed = "failed" // 发送失败,等待重试
|
|
)
|
|
|
|
// MaxRetries 单封邮件最大重试次数;超出后停止重试,保留失败记录(PLAN F5)。
|
|
const MaxRetries = 5
|
|
|
|
// Retryable 可由 cron 定时调用的邮件队列重试接口。
|
|
type Retryable interface {
|
|
Retry(ctx context.Context, limit int) error
|
|
}
|
|
|
|
// QueuedMailer 邮件队列实现(PLAN F5):每封邮件先落 mail_logs 再同步尝试直发;
|
|
// 失败记录 error 与 retry_count,由 cron 定时重试;成功标记 sent。
|
|
// Send 恒返回 nil(除 DB 落库失败):邮件失败不阻断业务
|
|
// (OTP 双通道、审批结果等,见 AuthService.UserOTPSend 的既有约定)。
|
|
type QueuedMailer struct {
|
|
base Mailer
|
|
db *gorm.DB
|
|
log *slog.Logger
|
|
}
|
|
|
|
// NewQueuedMailer 创建邮件队列,包装底层直发实现(SMTP 或 LogMailer)。
|
|
func NewQueuedMailer(base Mailer, db *gorm.DB, log *slog.Logger) *QueuedMailer {
|
|
return &QueuedMailer{base: base, db: db, log: log}
|
|
}
|
|
|
|
// Send 入队并尝试直发。返回 nil 表示已受理;实际送达与否见 mail_logs。
|
|
func (m *QueuedMailer) Send(ctx context.Context, to, subject, body string) error {
|
|
entry := model.MailLog{To: to, Subject: subject, Body: body, Status: StatusPending}
|
|
if err := m.db.WithContext(ctx).Create(&entry).Error; err != nil {
|
|
m.log.Error("mail: 记录 mail_log 失败", "err", err)
|
|
return err
|
|
}
|
|
if err := m.base.Send(ctx, to, subject, body); err != nil {
|
|
_ = m.db.WithContext(ctx).Model(&entry).Updates(map[string]any{
|
|
"status": StatusFailed, "error": err.Error(), "retry_count": 1,
|
|
}).Error
|
|
m.log.Warn("mail: 直发失败,进入重试队列", "to", to, "subject", subject, "err", err)
|
|
return nil // 非阻断
|
|
}
|
|
_ = m.db.WithContext(ctx).Model(&entry).Update("status", StatusSent).Error
|
|
return nil
|
|
}
|
|
|
|
// Retry 重试失败邮件(retry_count < limit,按原内容重发)。成功标记 sent,
|
|
// 失败累加 retry_count 并刷新 error;达到上限后不再挑选,保留失败记录待人工处理。
|
|
func (m *QueuedMailer) Retry(ctx context.Context, limit int) error {
|
|
if limit <= 0 {
|
|
return nil
|
|
}
|
|
var rows []model.MailLog
|
|
if err := m.db.WithContext(ctx).
|
|
Where("status = ? AND retry_count < ?", StatusFailed, limit).
|
|
Find(&rows).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, r := range rows {
|
|
if err := m.base.Send(ctx, r.To, r.Subject, r.Body); err != nil {
|
|
_ = m.db.WithContext(ctx).Model(&model.MailLog{}).Where("id = ?", r.ID).Updates(map[string]any{
|
|
"retry_count": r.RetryCount + 1, "error": err.Error(),
|
|
}).Error
|
|
m.log.Warn("mail: 重试仍失败", "id", r.ID, "to", r.To, "retry_count", r.RetryCount+1, "err", err)
|
|
continue
|
|
}
|
|
_ = m.db.WithContext(ctx).Model(&model.MailLog{}).Where("id = ?", r.ID).Update("status", StatusSent).Error
|
|
m.log.Info("mail: 重试成功", "id", r.ID, "to", r.To)
|
|
}
|
|
return nil
|
|
}
|