173 lines
4.8 KiB
Go
173 lines
4.8 KiB
Go
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)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|