Files
jiang13-bbs/backend/service/notification.go
freefire 5270fee2b7 feat: 评论/帖子编辑历史与编辑标记,内容锁定
- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal
- 新增 PostEditedMark 编辑标记展示
- 新增内容锁定机制(content_lock)防止并发编辑冲突
- 审核与通知服务适配
2026-09-25 23:41:45 +08:00

198 lines
5.4 KiB
Go
Raw Permalink 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 (
"log"
"time"
"github.com/freefire/jiang13-bbs/model"
"gorm.io/gorm"
)
// LikeNotifyDelay 点赞通知冷却:未满该时长即取消点赞则不通知作者
const LikeNotifyDelay = time.Minute
// NotificationService 通知服务
type NotificationService struct {
db *gorm.DB
// OnNotifyNew 落库成功后回调(用于 WS 推送红点);可为 nil
OnNotifyNew func(userID uint)
}
func NewNotificationService(db *gorm.DB) *NotificationService {
return &NotificationService{db: db}
}
// FlushPendingLikeNotifications 将已冷却且仍有效的点赞落为通知(误赞撤回不会进入此处)。
func (s *NotificationService) FlushPendingLikeNotifications(limit int) {
if limit < 1 {
limit = 50
}
cutoff := time.Now().Add(-LikeNotifyDelay)
type pendingLike struct {
PostID uint
UserID uint
AuthorID uint
}
var rows []pendingLike
err := s.db.Raw(`
SELECT l.post_id AS post_id, l.user_id AS user_id, p.user_id AS author_id
FROM likes l
INNER JOIN posts p ON p.id = l.post_id
WHERE l.created_at <= ?
AND p.deleted_at IS NULL
AND p.status = ?
AND l.user_id <> p.user_id
AND NOT EXISTS (
SELECT 1 FROM notifications n
WHERE n.type = ?
AND n.actor_id = l.user_id
AND n.post_id = l.post_id
AND n.user_id = p.user_id
AND n.created_at >= l.created_at
)
ORDER BY l.created_at ASC
LIMIT ?
`, cutoff, model.ContentStatusPublished, model.NotificationTypeLike, limit).Scan(&rows).Error
if err != nil {
log.Printf("[notif] 扫描待发点赞通知失败: %v", err)
return
}
for _, row := range rows {
s.Create(row.AuthorID, row.UserID, model.NotificationTypeLike, row.PostID, 0, "")
}
}
// StartLikeNotifyFlusher 周期落库冷却后的点赞通知
func StartLikeNotifyFlusher(s *NotificationService) {
go func() {
t := time.NewTicker(20 * time.Second)
defer t.Stop()
for range t.C {
s.FlushPendingLikeNotifications(50)
}
}()
}
// Create 创建通知(actorID == userID 时跳过,不通知自己)
func (s *NotificationService) Create(userID, actorID uint, notifType string, postID, commentID uint, content string) {
if userID == 0 || actorID == 0 || userID == actorID {
return
}
// 截断内容预览
if len(content) > 200 {
content = content[:200]
}
n := &model.Notification{
UserID: userID,
ActorID: actorID,
Type: notifType,
PostID: postID,
CommentID: commentID,
Content: content,
IsRead: false,
}
// 通知创建失败不影响主流程,忽略错误
if err := s.db.Create(n).Error; err != nil {
return
}
if s.OnNotifyNew != nil {
s.OnNotifyNew(userID)
}
}
// CreateSystem 系统通知(ActorID=0,提醒对象即本人,绕过自我通知检查;失败静默)
func (s *NotificationService) CreateSystem(userID uint, notifType string, postID, commentID uint, content string) {
if userID == 0 {
return
}
if len(content) > 200 {
content = content[:200]
}
n := &model.Notification{
UserID: userID,
ActorID: 0,
Type: notifType,
PostID: postID,
CommentID: commentID,
Content: content,
IsRead: false,
}
if err := s.db.Create(n).Error; err != nil {
return
}
if s.OnNotifyNew != nil {
s.OnNotifyNew(userID)
}
}
// CreateMention 创建群聊 @ 提醒(PostID=0,用 RoomID/MessageID 关联)。
// 返回创建出的通知 ID(0 表示未创建),供 handler 实时推送。
func (s *NotificationService) CreateMention(userID, actorID, roomID, messageID uint, content string) uint {
if userID == 0 || actorID == 0 || userID == actorID || roomID == 0 {
return 0
}
if runes := []rune(content); len(runes) > 100 {
content = string(runes[:100])
}
n := &model.Notification{
UserID: userID,
ActorID: actorID,
Type: model.NotificationTypeMention,
RoomID: roomID,
MessageID: messageID,
Content: content,
}
if err := s.db.Create(n).Error; err != nil {
return 0
}
if s.OnNotifyNew != nil {
s.OnNotifyNew(userID)
}
return n.ID
}
// List 获取用户通知列表(分页,按时间倒序)
func (s *NotificationService) List(userID uint, page, size int) ([]model.Notification, int64, error) {
if page < 1 {
page = 1
}
if size < 1 || size > 50 {
size = 20
}
query := s.db.Model(&model.Notification{}).Where("user_id = ?", userID)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []model.Notification
offset := (page - 1) * size
if err := query.Order("created_at DESC").Offset(offset).Limit(size).
Preload("Actor").Preload("Post").Preload("Room").Find(&list).Error; err != nil {
return nil, 0, err
}
return list, total, nil
}
// UnreadCount 获取未读通知数
func (s *NotificationService) UnreadCount(userID uint) (int64, error) {
var total int64
err := s.db.Model(&model.Notification{}).Where("user_id = ? AND is_read = ?", userID, false).Count(&total).Error
return total, err
}
// MarkRead 标记单条通知为已读(校验归属)
func (s *NotificationService) MarkRead(id, userID uint) error {
result := s.db.Model(&model.Notification{}).
Where("id = ? AND user_id = ?", id, userID).
Update("is_read", true)
return result.Error
}
// MarkAllRead 标记用户所有通知为已读
func (s *NotificationService) MarkAllRead(userID uint) error {
result := s.db.Model(&model.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Update("is_read", true)
return result.Error
}