Files
backend/internal/service/system_message_service.go
lan 4c0177149a Clean backend debug logging and standardize error reporting.
This removes verbose trace output in handlers/services and keeps only actionable error-level logs.
2026-03-09 22:20:44 +08:00

443 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}