This removes verbose trace output in handlers/services and keeps only actionable error-level logs.
443 lines
14 KiB
Go
443 lines
14 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"carrot_bbs/internal/cache"
|
||
"carrot_bbs/internal/model"
|
||
"carrot_bbs/internal/pkg/utils"
|
||
"carrot_bbs/internal/repository"
|
||
)
|
||
|
||
// SystemMessageService 系统消息服务接口
|
||
type SystemMessageService interface {
|
||
// 发送互动通知
|
||
SendLikeNotification(ctx context.Context, userID string, operatorID string, postID string) error
|
||
SendCommentNotification(ctx context.Context, userID string, operatorID string, postID string, commentID string) error
|
||
SendReplyNotification(ctx context.Context, userID string, operatorID string, postID string, commentID string, replyID string) error
|
||
SendFollowNotification(ctx context.Context, userID string, operatorID string) error
|
||
SendMentionNotification(ctx context.Context, userID string, operatorID string, postID string) error
|
||
SendFavoriteNotification(ctx context.Context, userID string, operatorID string, postID string) error
|
||
SendLikeCommentNotification(ctx context.Context, userID string, operatorID string, postID string, commentID string, commentContent string) error
|
||
SendLikeReplyNotification(ctx context.Context, userID string, operatorID string, postID string, replyID string, replyContent string) error
|
||
|
||
// 发送系统公告
|
||
SendSystemAnnouncement(ctx context.Context, userIDs []string, title string, content string) error
|
||
SendBroadcastAnnouncement(ctx context.Context, title string, content string) error
|
||
}
|
||
|
||
type systemMessageServiceImpl struct {
|
||
notifyRepo *repository.SystemNotificationRepository
|
||
pushService PushService
|
||
userRepo *repository.UserRepository
|
||
postRepo *repository.PostRepository
|
||
cache cache.Cache
|
||
}
|
||
|
||
// NewSystemMessageService 创建系统消息服务
|
||
func NewSystemMessageService(
|
||
notifyRepo *repository.SystemNotificationRepository,
|
||
pushService PushService,
|
||
userRepo *repository.UserRepository,
|
||
postRepo *repository.PostRepository,
|
||
) SystemMessageService {
|
||
return &systemMessageServiceImpl{
|
||
notifyRepo: notifyRepo,
|
||
pushService: pushService,
|
||
userRepo: userRepo,
|
||
postRepo: postRepo,
|
||
cache: cache.GetCache(),
|
||
}
|
||
}
|
||
|
||
// SendLikeNotification 发送点赞通知
|
||
func (s *systemMessageServiceImpl) SendLikeNotification(ctx context.Context, userID string, operatorID string, postID string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取帖子标题
|
||
postTitle, err := s.getPostTitle(postID)
|
||
if err != nil {
|
||
postTitle = "您的帖子"
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: postID,
|
||
TargetTitle: postTitle,
|
||
TargetType: "post",
|
||
ActionURL: fmt.Sprintf("/posts/%s", postID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 赞了「%s」", actorName, postTitle)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyLikePost, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create like notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendCommentNotification 发送评论通知
|
||
func (s *systemMessageServiceImpl) SendCommentNotification(ctx context.Context, userID string, operatorID string, postID string, commentID string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取帖子标题
|
||
postTitle, err := s.getPostTitle(postID)
|
||
if err != nil {
|
||
postTitle = "您的帖子"
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: postID,
|
||
TargetTitle: postTitle,
|
||
TargetType: "comment",
|
||
ActionURL: fmt.Sprintf("/posts/%s?comment=%s", postID, commentID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 评论了「%s」", actorName, postTitle)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyComment, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create comment notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendReplyNotification 发送回复通知
|
||
func (s *systemMessageServiceImpl) SendReplyNotification(ctx context.Context, userID string, operatorID string, postID string, commentID string, replyID string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取帖子标题
|
||
postTitle, err := s.getPostTitle(postID)
|
||
if err != nil {
|
||
postTitle = "您的帖子"
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: replyID,
|
||
TargetTitle: postTitle,
|
||
TargetType: "reply",
|
||
ActionURL: fmt.Sprintf("/posts/%s?comment=%s&reply=%s", postID, commentID, replyID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 回复了您在「%s」的评论", actorName, postTitle)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyReply, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create reply notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendFollowNotification 发送关注通知
|
||
func (s *systemMessageServiceImpl) SendFollowNotification(ctx context.Context, userID string, operatorID string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: "",
|
||
TargetType: "user",
|
||
ActionURL: fmt.Sprintf("/users/%s", operatorID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 关注了你", actorName)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyFollow, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create follow notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendFavoriteNotification 发送收藏通知
|
||
func (s *systemMessageServiceImpl) SendFavoriteNotification(ctx context.Context, userID string, operatorID string, postID string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取帖子标题
|
||
postTitle, err := s.getPostTitle(postID)
|
||
if err != nil {
|
||
postTitle = "您的帖子"
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: postID,
|
||
TargetTitle: postTitle,
|
||
TargetType: "post",
|
||
ActionURL: fmt.Sprintf("/posts/%s", postID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 收藏了「%s」", actorName, postTitle)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyFavoritePost, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create favorite notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendLikeCommentNotification 发送评论点赞通知
|
||
func (s *systemMessageServiceImpl) SendLikeCommentNotification(ctx context.Context, userID string, operatorID string, postID string, commentID string, commentContent string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 截取评论内容预览(最多50字)
|
||
preview := commentContent
|
||
runes := []rune(preview)
|
||
if len(runes) > 50 {
|
||
preview = string(runes[:50]) + "..."
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: postID,
|
||
TargetTitle: preview,
|
||
TargetType: "comment",
|
||
ActionURL: fmt.Sprintf("/posts/%s?comment=%s", postID, commentID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 赞了您的评论", actorName)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyLikeComment, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create like comment notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendLikeReplyNotification 发送回复点赞通知
|
||
func (s *systemMessageServiceImpl) SendLikeReplyNotification(ctx context.Context, userID string, operatorID string, postID string, replyID string, replyContent string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 截取回复内容预览(最多50字)
|
||
preview := replyContent
|
||
runes := []rune(preview)
|
||
if len(runes) > 50 {
|
||
preview = string(runes[:50]) + "..."
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: postID,
|
||
TargetTitle: preview,
|
||
TargetType: "reply",
|
||
ActionURL: fmt.Sprintf("/posts/%s?reply=%s", postID, replyID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 赞了您的回复", actorName)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyLikeReply, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create like reply notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendMentionNotification 发送@提及通知
|
||
func (s *systemMessageServiceImpl) SendMentionNotification(ctx context.Context, userID string, operatorID string, postID string) error {
|
||
// 获取操作者信息
|
||
actorName, avatarURL, err := s.getActorInfo(ctx, operatorID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取帖子标题
|
||
postTitle, err := s.getPostTitle(postID)
|
||
if err != nil {
|
||
postTitle = "您的帖子"
|
||
}
|
||
|
||
extraData := &model.SystemNotificationExtra{
|
||
ActorIDStr: operatorID,
|
||
ActorName: actorName,
|
||
AvatarURL: avatarURL,
|
||
TargetID: postID,
|
||
TargetTitle: postTitle,
|
||
TargetType: "post",
|
||
ActionURL: fmt.Sprintf("/posts/%s", postID),
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
content := fmt.Sprintf("%s 在「%s」中提到了你", actorName, postTitle)
|
||
|
||
// 创建通知
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyMention, content, extraData)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create mention notification: %w", err)
|
||
}
|
||
|
||
// 推送通知
|
||
return s.pushService.PushSystemNotification(ctx, userID, notification)
|
||
}
|
||
|
||
// SendSystemAnnouncement 发送系统公告给指定用户
|
||
func (s *systemMessageServiceImpl) SendSystemAnnouncement(ctx context.Context, userIDs []string, title string, content string) error {
|
||
for _, userID := range userIDs {
|
||
extraData := &model.SystemNotificationExtra{
|
||
TargetType: "announcement",
|
||
ActionTime: time.Now().Format(time.RFC3339),
|
||
}
|
||
|
||
notification, err := s.createNotification(ctx, userID, model.SysNotifyAnnounce, fmt.Sprintf("【%s】%s", title, content), extraData)
|
||
if err != nil {
|
||
continue // 单个失败不影响其他用户
|
||
}
|
||
|
||
// 推送通知(使用高优先级)
|
||
if err := s.pushService.PushSystemNotification(ctx, userID, notification); err != nil {
|
||
continue
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SendBroadcastAnnouncement 发送广播公告给所有在线用户
|
||
func (s *systemMessageServiceImpl) SendBroadcastAnnouncement(ctx context.Context, title string, content string) error {
|
||
// TODO: 实现广播公告
|
||
// 1. 获取所有在线用户
|
||
// 2. 批量发送公告
|
||
// 3. 对于离线用户,存储为待推送记录
|
||
return fmt.Errorf("broadcast announcement not implemented")
|
||
}
|
||
|
||
// createNotification 创建系统通知(存储到独立表)
|
||
func (s *systemMessageServiceImpl) createNotification(ctx context.Context, userID string, notifyType model.SystemNotificationType, content string, extraData *model.SystemNotificationExtra) (*model.SystemNotification, error) {
|
||
// 生成雪花算法ID
|
||
id, err := utils.GetSnowflake().GenerateID()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to generate notification ID: %w", err)
|
||
}
|
||
|
||
notification := &model.SystemNotification{
|
||
ID: id,
|
||
ReceiverID: userID,
|
||
Type: notifyType,
|
||
Content: content,
|
||
ExtraData: extraData,
|
||
IsRead: false,
|
||
}
|
||
|
||
// 保存通知到数据库
|
||
if err := s.notifyRepo.Create(notification); err != nil {
|
||
return nil, fmt.Errorf("failed to save notification: %w", err)
|
||
}
|
||
|
||
// 失效系统消息未读数缓存
|
||
cache.InvalidateUnreadSystem(s.cache, userID)
|
||
|
||
return notification, nil
|
||
}
|
||
|
||
// getActorInfo 获取操作者信息
|
||
func (s *systemMessageServiceImpl) getActorInfo(ctx context.Context, operatorID string) (string, string, error) {
|
||
// 从用户仓储获取用户信息
|
||
if s.userRepo != nil {
|
||
user, err := s.userRepo.GetByID(operatorID)
|
||
if err != nil {
|
||
return "用户", utils.GenerateDefaultAvatarURL("用户"), nil // 返回默认值,不阻断流程
|
||
}
|
||
avatar := utils.GetAvatarOrDefault(user.Username, user.Nickname, user.Avatar)
|
||
return user.Nickname, avatar, nil
|
||
}
|
||
// 如果没有用户仓储,返回默认值
|
||
return "用户", utils.GenerateDefaultAvatarURL("用户"), nil
|
||
}
|
||
|
||
// getPostTitle 获取帖子标题
|
||
func (s *systemMessageServiceImpl) getPostTitle(postID string) (string, error) {
|
||
if s.postRepo == nil {
|
||
if len(postID) >= 8 {
|
||
return fmt.Sprintf("帖子#%s", postID[:8]), nil
|
||
}
|
||
return fmt.Sprintf("帖子#%s", postID), nil
|
||
}
|
||
post, err := s.postRepo.GetByID(postID)
|
||
if err != nil {
|
||
if len(postID) >= 8 {
|
||
return fmt.Sprintf("帖子#%s", postID[:8]), nil
|
||
}
|
||
return fmt.Sprintf("帖子#%s", postID), nil
|
||
}
|
||
if post.Title != "" {
|
||
return post.Title, nil
|
||
}
|
||
// 如果没有标题,返回内容前20个字符
|
||
if len(post.Content) > 20 {
|
||
return post.Content[:20] + "...", nil
|
||
}
|
||
return post.Content, nil
|
||
}
|