Files
BYW-VoiceHub/server/api/notifications/index.ts
T

161 lines
5.2 KiB
TypeScript

import { and, count, desc, eq, ilike, or } from 'drizzle-orm'
import { defineEventHandler, getQuery } from 'h3'
import { db } from '~/drizzle/db'
import { notifications, songCollaborators, songs } from '~/drizzle/schema'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
import { createApiError } from '~~/server/utils/apiError'
import { serializeNotificationSender } from '~~/server/utils/important-notification-policy'
const serializeNotification = <T extends {
senderId: number | null
senderName: string | null
senderUsername: string | null
}>(notification: T) => {
const { senderId, senderName, senderUsername, ...data } = notification
return {
...data,
sender: serializeNotificationSender({ senderId, senderName, senderUsername })
}
}
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createApiError(401, SERVER_ERROR_CODES.NOTIFICATION_AUTH_REQUIRED, '需要登录才能获取通知')
}
try {
// 获取查询参数
const query = getQuery(event)
const page = Math.max(1, parseInt(query.page as string) || 1)
const limit = Math.min(50, Math.max(1, parseInt(query.limit as string) || 10))
const offset = (page - 1) * limit
const unreadOnly = query.filter === 'unread'
const search = typeof query.search === 'string' ? query.search.trim().slice(0, 100) : ''
// Drizzle ORM 的 ilike 已对参数做参数化绑定,无需手动转义 % 和 _
const searchCondition = search
? or(
ilike(notifications.title, `%${search}%`),
ilike(notifications.message, `%${search}%`)
)
: undefined
const notificationCondition = and(
eq(notifications.userId, user.id),
eq(notifications.userDeleted, false),
unreadOnly ? eq(notifications.read, false) : undefined,
searchCondition
)
// 获取总通知数量
const totalCountResult = await db
.select({ count: count() })
.from(notifications)
.where(notificationCondition)
const totalCount = totalCountResult[0]?.count || 0
const totalPages = Math.ceil(totalCount / limit)
// 获取分页通知数据
const userNotifications = await db
.select()
.from(notifications)
.where(notificationCondition)
.orderBy(desc(notifications.createdAt))
.limit(limit)
.offset(offset)
// 丰富通知数据,特别是对于联合投稿邀请
const enrichedNotifications = await Promise.all(
userNotifications.map(async (notification) => {
const serializedNotification = serializeNotification(notification)
if (notification.type === 'COLLABORATION_INVITE' && notification.songId) {
// 1. 检查歌曲是否存在
const songExists = await db
.select({ id: songs.id })
.from(songs)
.where(eq(songs.id, notification.songId))
.limit(1)
.then((res) => res.length > 0)
if (!songExists) {
return {
...serializedNotification,
handled: true,
status: 'INVALID',
isValid: false,
message: notification.message // 保持原消息,前端会显示已失效
}
}
// 2. 查询对应的邀请状态
const collab = await db
.select()
.from(songCollaborators)
.where(
and(
eq(songCollaborators.songId, notification.songId),
eq(songCollaborators.userId, user.id)
)
)
.limit(1)
if (collab.length > 0) {
return {
...serializedNotification,
handled: collab[0].status !== 'PENDING',
status: collab[0].status,
isValid: true,
repliedAt: collab[0].updatedAt || collab[0].createdAt // 假设更新时间为回复时间
}
} else {
// 如果找不到邀请记录,说明可能已经被撤回或删除
return {
...serializedNotification,
handled: true,
status: 'INVALID',
isValid: false,
message: notification.message
}
}
}
return {
...serializedNotification,
handled: false, // 默认未处理,或者不适用
isValid: true
}
})
)
// 计算未读通知数量
const unreadCountResult = await db
.select({ count: count() })
.from(notifications)
.where(
and(
eq(notifications.userId, user.id),
eq(notifications.userDeleted, false),
eq(notifications.read, false)
)
)
const unreadCount = unreadCountResult[0]?.count || 0
return {
notifications: enrichedNotifications,
unreadCount,
pagination: {
currentPage: page,
totalPages,
totalCount,
limit,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
}
}
} catch (err) {
console.error('获取通知失败:', err)
throw createApiError(500, SERVER_ERROR_CODES.NOTIFICATION_FETCH_FAILED, '获取通知失败')
}
})