package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"goalfymax-admin/internal/config"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"html"
"html/template"
"io"
"net/http"
"time"
"unicode/utf8"
)
// MessagePushService 消息推送服务接口
type MessagePushService interface {
SendMessage(ctx context.Context, req *models.MessagePushRequest, senderID int, senderName string) (*models.MessagePushResponse, error)
GetPushLogs(ctx context.Context, req *models.MessagePushListRequest) (*models.MessagePushListResponse, error)
GetPushLogByID(ctx context.Context, id int64) (*models.MessagePushLog, error)
SearchUsers(ctx context.Context, req *models.UserSearchRequest) (*models.UserSearchResponse, error)
}
type messagePushService struct {
storage storage.MessagePushStorage
goalfymaxClient *http.Client
goalfymaxURL string
retryCount int
retryInterval time.Duration
emailService *EmailService
goalfyMaxUserStorage storage.GoalfyMaxUserStorage
}
// NewMessagePushService 创建消息推送服务实例
func NewMessagePushService() MessagePushService {
cfg := config.GetConfig()
return &messagePushService{
storage: storage.NewMessagePushStorage(),
goalfymaxClient: &http.Client{
Timeout: time.Duration(cfg.MessagePush.Timeout) * time.Second,
},
goalfymaxURL: cfg.MessagePush.GoalfyMaxBaseURL,
retryCount: cfg.MessagePush.RetryCount,
retryInterval: time.Duration(cfg.MessagePush.RetryInterval) * time.Millisecond,
emailService: NewEmailService(),
goalfyMaxUserStorage: storage.NewGoalfyMaxUserStorage(),
}
}
// SendMessage 发送消息
func (s *messagePushService) SendMessage(ctx context.Context, req *models.MessagePushRequest, senderID int, senderName string) (*models.MessagePushResponse, error) {
// 参数校验
if len(req.UserIDs) == 0 {
return nil, fmt.Errorf("目标用户不能为空")
}
if len(req.Title) == 0 {
return nil, fmt.Errorf("消息标题不能为空")
}
titleRuneCount := utf8.RuneCountInString(req.Title)
if titleRuneCount > 100 {
return nil, fmt.Errorf("消息标题不能超过100字符")
}
if len(req.Content) == 0 {
return nil, fmt.Errorf("消息内容不能为空")
}
contentRuneCount := utf8.RuneCountInString(req.Content)
if contentRuneCount > 2000 {
return nil, fmt.Errorf("消息内容不能超过2000字符")
}
// 序列化目标用户
targetUsersJSON, err := storage.SerializeTargetUsers(req.UserIDs)
if err != nil {
return nil, fmt.Errorf("序列化目标用户失败: %w", err)
}
// 创建推送记录
log := &models.MessagePushLog{
Title: req.Title,
Content: req.Content,
TargetUsers: targetUsersJSON,
SenderID: senderID,
SenderName: senderName,
Status: 0, // 待发送
}
if err := s.storage.Create(ctx, log); err != nil {
return nil, fmt.Errorf("创建推送记录失败: %w", err)
}
// 异步发送消息
go s.sendToGoalfyMaxAsync(context.Background(), log.ID, req.Title, req.Content, req.UserIDs)
return &models.MessagePushResponse{
LogID: log.ID,
SuccessCount: 0,
FailCount: 0,
}, nil
}
// sendToGoalfyMaxAsync 异步发送到GoalfyMax
func (s *messagePushService) sendToGoalfyMaxAsync(ctx context.Context, logID int64, title string, content string, userIDs []int) {
// 更新状态为发送中
s.storage.UpdateStatus(ctx, logID, 1, 0, 0, "")
successCount := 0
failCount := 0
var errorMessage string
// 调用GoalfyMax接口
err := s.callGoalfyMaxAPI(ctx, title, content, userIDs)
if err != nil {
failCount = len(userIDs)
errorMessage = err.Error()
s.storage.UpdateStatus(ctx, logID, 3, successCount, failCount, errorMessage)
return
}
// 发送成功,同时发送邮件
s.sendEmails(ctx, title, content, userIDs)
// 发送成功
successCount = len(userIDs)
s.storage.UpdateStatus(ctx, logID, 2, successCount, failCount, "")
}
// sendEmails 发送邮件给用户
func (s *messagePushService) sendEmails(ctx context.Context, title string, content string, userIDs []int) {
// 生成邮件HTML内容
htmlContent := s.generateMessageEmailHTML(title, content)
// 循环发送邮件给每个用户
for _, userID := range userIDs {
user, err := s.goalfyMaxUserStorage.GetByUserID(userID)
if err != nil {
// 如果获取用户失败,跳过该用户
continue
}
if user.Email != "" {
// 发送邮件
if err := s.emailService.sendEmail(user.Email, title, htmlContent); err != nil {
// 记录错误但不中断其他邮件的发送
continue
}
// 避免发送过快,每次发送后稍作延迟
time.Sleep(100 * time.Millisecond)
}
}
}
// generateMessageEmailHTML 生成消息通知邮件的HTML内容
func (s *messagePushService) generateMessageEmailHTML(title, content string) string {
// 转义HTML特殊字符,然后将换行符转换为
escapedContent := html.EscapeString(content)
// 将换行符转换为HTML换行
contentHTML := fmt.Sprintf(`