Files
BYW-VoiceHub/server/services/notificationService.ts
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

788 lines
21 KiB
TypeScript

import { db } from '~/drizzle/db'
import {
notifications,
notificationSettings,
playTimes,
schedules,
songCollaborators,
songs,
systemSettings,
users,
votes
} from '~/drizzle/schema'
import { and, eq, gte, inArray } from 'drizzle-orm'
import { sendBatchMeowNotifications, sendMeowNotificationToUser } from './meowNotificationService'
import { sendBatchEmailNotifications, sendEmailNotificationToUser } from './smtpService'
import { formatDateTime, getBeijingTime } from '~/utils/timeUtils'
/**
* 创建联合投稿邀请通知
*/
export async function createCollaborationInvitationNotification(
inviterId: number,
inviteeId: number,
songId: number,
songTitle: string
) {
try {
const inviter = await db
.select()
.from(users)
.where(eq(users.id, inviterId))
.limit(1)
.then((res) => res[0])
const message = `用户 ${inviter?.name || '未知用户'} 邀请您共同投稿歌曲《${songTitle}》。`
// 创建通知
const notificationResult = await db
.insert(notifications)
.values({
userId: inviteeId,
type: 'COLLABORATION_INVITE',
message,
songId
})
.returning()
// 发送外部通知(MeoW, Email等)
try {
await sendMeowNotificationToUser(inviteeId, '收到联合投稿邀请', message)
} catch (error) {
console.error('发送 MeoW 通知失败:', error)
}
try {
await sendEmailNotificationToUser(
inviteeId,
'收到联合投稿邀请',
message,
undefined,
'notification.collaborationInvite',
{
inviterName: inviter?.name || '未知用户',
songTitle: songTitle
}
)
} catch (error) {
console.error('发送邮件通知失败:', error)
}
return notificationResult[0]
} catch (error) {
console.error('创建联合投稿邀请通知失败:', error)
return null
}
}
/**
* 创建联合投稿回复通知
*/
export async function createCollaborationResponseNotification(
inviteeId: number,
inviterId: number,
songTitle: string,
accepted: boolean
) {
try {
const invitee = await db
.select()
.from(users)
.where(eq(users.id, inviteeId))
.limit(1)
.then((res) => res[0])
const actionText = accepted ? '接受' : '拒绝'
const message = `用户 ${invitee?.name || '未知用户'} ${actionText}了您的歌曲《${songTitle}》的联合投稿邀请。`
// 创建通知
const notificationResult = await db
.insert(notifications)
.values({
userId: inviterId,
type: 'COLLABORATION_RESPONSE',
message
// songId // 这里可能不需要songId,或者需要传进来
})
.returning()
return notificationResult[0]
} catch (error) {
console.error('创建联合投稿回复通知失败:', error)
return null
}
}
/**
* 创建歌曲被选中的通知
*/
export async function createSongSelectedNotification(
userId: number,
songId: number,
songInfo: {
title: string
artist: string
playDate: Date
},
ipAddress?: string
) {
try {
// 获取系统设置,检查是否启用播出时段功能
const systemSettingsResult = await db.select().from(systemSettings).limit(1)
const systemConfig = systemSettingsResult[0]
const isPlayTimeEnabled = systemConfig?.enablePlayTimeSelection || false
// 获取排期对应的播出时段
const scheduleResult = await db
.select({
id: schedules.id,
songId: schedules.songId,
playDate: schedules.playDate,
playTimeId: schedules.playTimeId,
playTime: {
id: playTimes.id,
name: playTimes.name,
startTime: playTimes.startTime,
endTime: playTimes.endTime
}
})
.from(schedules)
.leftJoin(playTimes, eq(schedules.playTimeId, playTimes.id))
.where(and(eq(schedules.songId, songId), eq(schedules.playDate, songInfo.playDate)))
.limit(1)
const schedule = scheduleResult[0]
// 获取用户通知设置
const settingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, userId))
.limit(1)
const settings = settingsResult[0]
// 如果用户关闭了此类通知,则不发送
if (settings && !settings.enabled) {
return null
}
// 创建通知,根据播出时段功能启用状态决定显示内容
let message = `您投稿的歌曲《${songInfo.title}》已被安排播放,播放日期:${formatDate(songInfo.playDate)}。`
// 只有在启用播出时段功能且有播出时段信息时,才添加播出时段详情
if (isPlayTimeEnabled && schedule?.playTime) {
let timeInfo = ''
// 根据开始和结束时间的情况,格式化显示
if (schedule.playTime.startTime && schedule.playTime.endTime) {
timeInfo = `(${schedule.playTime.startTime}-${schedule.playTime.endTime})`
} else if (schedule.playTime.startTime) {
timeInfo = `(开始时间:${schedule.playTime.startTime})`
} else if (schedule.playTime.endTime) {
timeInfo = `(结束时间:${schedule.playTime.endTime})`
}
message += `播出时段:${schedule.playTime.name}${timeInfo ? ' ' + timeInfo : ''}。`
}
let notification
try {
const notificationResult = await db
.insert(notifications)
.values({
userId,
type: 'SONG_SELECTED',
message,
songId
})
.returning()
notification = notificationResult[0]
} catch (error) {
throw error
}
// 同步发送 MeoW 通知
try {
await sendMeowNotificationToUser(userId, '收到新选中', message)
} catch (error) {
console.error('发送 MeoW 通知失败:', error)
}
// 同步发送 邮件通知
try {
await sendEmailNotificationToUser(
userId,
'歌曲被选中',
message,
ipAddress,
'notification.songSelected',
{
songTitle: songInfo.title,
playDate: formatDate(songInfo.playDate),
playTimeName: isPlayTimeEnabled && schedule?.playTime ? schedule.playTime.name : '',
playTimeRange:
isPlayTimeEnabled &&
schedule?.playTime &&
(schedule.playTime.startTime || schedule.playTime.endTime)
? `${schedule.playTime.startTime || ''}${schedule.playTime.startTime && schedule.playTime.endTime ? '-' : ''}${schedule.playTime.endTime || ''}`
: ''
}
)
} catch (error) {
console.error('发送邮件通知失败:', error)
}
return notification
} catch (err) {
return null
}
}
// 格式化日期为 yyyy-MM-dd 格式(北京时间)
function formatDate(date: Date): string {
return formatDateTime(date, 'YYYY-MM-DD')
}
/**
* 创建歌曲已播放的通知
*/
export async function createSongPlayedNotification(songId: number, ipAddress?: string) {
try {
// 获取歌曲信息
const songResult = await db.select().from(songs).where(eq(songs.id, songId)).limit(1)
const song = songResult[0]
if (!song) {
return null
}
// 获取用户通知设置
const settingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, song.requesterId))
.limit(1)
const settings = settingsResult[0]
// 如果用户关闭了此类通知,则不发送
if (settings && !settings.songPlayedEnabled) {
return null
}
// 创建通知
const message = `您投稿的歌曲《${song.title}》已播放。`
// 获取所有关联用户(投稿人 + 联合投稿人)
const userIdsToNotify = [song.requesterId]
// 获取联合投稿人
const collaborators = await db
.select()
.from(songCollaborators)
.where(and(eq(songCollaborators.songId, songId), eq(songCollaborators.status, 'ACCEPTED')))
collaborators.forEach((c) => {
if (!userIdsToNotify.includes(c.userId)) {
userIdsToNotify.push(c.userId)
}
})
const notificationsCreated = []
for (const targetUserId of userIdsToNotify) {
try {
// 获取用户通知设置
const settingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, targetUserId))
.limit(1)
const settings = settingsResult[0]
// 如果用户关闭了此类通知,则不发送
if (settings && !settings.songPlayedEnabled) {
continue
}
const userMessage =
targetUserId === song.requesterId
? message
: `您参与联合投稿的歌曲《${song.title}》已播放。`
const notificationResult = await db
.insert(notifications)
.values({
userId: targetUserId,
type: 'SONG_PLAYED',
message: userMessage,
songId: songId
})
.returning()
notificationsCreated.push(notificationResult[0])
// 同步发送 MeoW 通知
try {
await sendMeowNotificationToUser(targetUserId, '歌曲已播放', userMessage)
} catch (error) {
console.error(`发送 MeoW 通知失败 (User: ${targetUserId}):`, error)
}
// 同步发送 邮件通知
try {
await sendEmailNotificationToUser(
targetUserId,
'歌曲已播放',
userMessage,
ipAddress,
'notification.songPlayed',
{ songTitle: song.title }
)
} catch (error) {
console.error(`发送邮件通知失败 (User: ${targetUserId}):`, error)
}
} catch (err) {
console.error(`处理播放通知失败 (User: ${targetUserId}):`, err)
}
}
return notificationsCreated.length > 0 ? notificationsCreated[0] : null
} catch (err) {
return null
}
}
/**
* 创建歌曲获得投票的通知
*/
export async function createSongVotedNotification(
songId: number,
voterId: number,
ipAddress?: string
) {
try {
// 获取歌曲信息
const songResult = await db.select().from(songs).where(eq(songs.id, songId)).limit(1)
const song = songResult[0]
if (!song) {
return null
}
// 获取歌曲的投票信息
const songVotes = await db.select().from(votes).where(eq(votes.songId, songId))
// 获取投票用户信息
const voterResult = await db.select().from(users).where(eq(users.id, voterId)).limit(1)
const voter = voterResult[0]
if (!voter) {
return null
}
// 获取用户通知设置
const settingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, song.requesterId))
.limit(1)
const settings = settingsResult[0]
// 如果用户关闭了此类通知,则不发送
if (settings && !settings.songVotedEnabled) {
return null
}
// 不要给自己投票发通知
if (song.requesterId === voterId) {
return null
}
// 检查投票阈值
const threshold = settings?.songVotedThreshold || 1
if (songVotes.length % threshold !== 0) {
return null
}
const message = `您投稿的歌曲《${song.title}》获得了一个新的投票,当前共有 ${songVotes.length} 个投票。`
// 防重复通知:检查最近5分钟内是否有相同歌曲的投票通知
const fiveMinutesAgo = new Date(getBeijingTime().getTime() - 5 * 60 * 1000)
const existingNotificationResult = await db
.select()
.from(notifications)
.where(
and(
eq(notifications.userId, song.requesterId),
eq(notifications.type, 'SONG_VOTED'),
eq(notifications.songId, songId),
eq(notifications.read, false),
gte(notifications.createdAt, fiveMinutesAgo)
)
)
.orderBy(notifications.createdAt)
.limit(1)
const existingNotification = existingNotificationResult[0]
let notification
if (existingNotification) {
// 更新现有通知
const updateResult = await db
.update(notifications)
.set({
message
})
.where(eq(notifications.id, existingNotification.id))
.returning()
notification = updateResult[0]
} else {
// 创建新通知
const createResult = await db
.insert(notifications)
.values({
userId: song.requesterId,
type: 'SONG_VOTED',
message,
songId: songId
})
.returning()
notification = createResult[0]
}
// 同步发送 MeoW 通知
try {
await sendMeowNotificationToUser(song.requesterId, '收到新投票', message)
} catch (error) {
console.error('发送 MeoW 通知失败:', error)
}
// 同步发送 邮件通知
try {
await sendEmailNotificationToUser(
song.requesterId,
'收到新投票',
message,
ipAddress,
'notification.songVoted',
{ songTitle: song.title, votesCount: songVotes.length }
)
} catch (error) {
console.error('发送邮件通知失败:', error)
}
return notification
} catch (err) {
return null
}
}
/**
* 创建歌曲驳回通知
*/
export async function createSongRejectedNotification(
userId: number,
songInfo: { title: string; artist: string },
reason: string,
ipAddress?: string
) {
try {
// 获取用户通知设置
const settingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, userId))
.limit(1)
const settings = settingsResult[0]
// 如果用户关闭了通知,则不发送
if (settings && !settings.enabled) {
return null
}
// 创建通知消息
const message = `您投稿的歌曲《${songInfo.title} - ${songInfo.artist}》已被管理员驳回。驳回原因:${reason}`
// 创建站内通知
let notification
try {
const notificationResult = await db
.insert(notifications)
.values({
userId,
type: 'SONG_REJECTED',
message
})
.returning()
notification = notificationResult[0]
} catch (error) {
throw error
}
// 同步发送 MeoW 通知
try {
await sendMeowNotificationToUser(userId, '歌曲被驳回', message)
} catch (error) {
console.error('发送 MeoW 通知失败:', error)
}
// 同步发送邮件通知
try {
await sendEmailNotificationToUser(
userId,
'歌曲被驳回',
message,
ipAddress,
'notification.songRejected',
{
songTitle: songInfo.title,
songArtist: songInfo.artist,
reason: reason
}
)
} catch (error) {
console.error('发送邮件通知失败:', error)
}
return notification
} catch (err) {
console.error('创建歌曲驳回通知失败:', err)
return null
}
}
export async function createSubmissionNoteClearedNotification(
userIds: number[],
songInfo: { title: string; artist: string },
reason?: string,
ipAddress?: string
) {
try {
const uniqueUserIds = [...new Set(userIds.filter((userId) => Number.isInteger(userId) && userId > 0))]
if (uniqueUserIds.length === 0) {
return []
}
const title = '歌曲备注已被管理员清空'
const content = reason?.trim()
? `您投稿歌曲《${songInfo.title} - ${songInfo.artist}》的备注已被管理员清空。原因:${reason.trim()}`
: `您投稿歌曲《${songInfo.title} - ${songInfo.artist}》的备注已被管理员清空。`
return await createBatchSystemNotifications(uniqueUserIds, title, content, ipAddress)
} catch (error) {
console.error('创建歌曲备注清空通知失败:', error)
return []
}
}
/**
* 创建系统通知
*/
export async function createSystemNotification(
userId: number,
title: string,
content: string,
ipAddress?: string
) {
try {
// 获取用户通知设置
const settingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, userId))
.limit(1)
const settings = settingsResult[0]
// 如果用户关闭了通知,则不发送
if (settings && !settings.enabled) {
return null
}
// 创建通知
const notificationResult = await db
.insert(notifications)
.values({
userId: userId,
type: 'SYSTEM_NOTICE',
message: content
})
.returning()
const notification = notificationResult[0]
// 同步发送 MeoW 通知
try {
await sendMeowNotificationToUser(userId, title, content)
} catch (error) {
console.error('发送 MeoW 通知失败:', error)
}
// 同步发送邮件通知
try {
await sendEmailNotificationToUser(
userId,
title,
content,
undefined,
undefined,
undefined,
ipAddress
)
} catch (error) {
console.error('发送邮件通知失败:', error)
}
return notification
} catch (err) {
return null
}
}
/**
* 批量创建系统通知
*/
export async function createBatchSystemNotifications(
userIds: number[],
title: string,
content: string,
ipAddress?: string
) {
try {
if (!userIds.length) {
return []
}
// 获取用户通知设置
const userSettings = await db
.select()
.from(notificationSettings)
.where(inArray(notificationSettings.userId, userIds))
// 构建用户ID到通知设置的映射
const settingsMap = new Map()
userSettings.forEach((setting) => {
settingsMap.set(setting.userId, setting)
})
// 准备要创建的通知数据
const notificationsToCreate = []
for (const userId of userIds) {
const settings = settingsMap.get(userId)
// 如果用户关闭了通知,则不发送
if (settings && !settings.enabled) {
continue
}
notificationsToCreate.push({
userId,
type: 'SYSTEM_NOTICE',
message: content
})
}
// 如果没有可发送的通知,直接返回
if (notificationsToCreate.length === 0) {
return []
}
// 批量创建通知
const createdNotifications = await db
.insert(notifications)
.values(notificationsToCreate)
.returning()
const notificationCount = { count: createdNotifications.length }
// 同步发送 MeoW 通知
let meowResults = { success: 0, failed: 0 }
try {
meowResults = await sendBatchMeowNotifications(userIds, title, content)
} catch (error) {
console.error('批量发送 MeoW 通知失败:', error)
}
// 同步发送邮件通知
let emailResults = { success: 0, failed: 0 }
try {
emailResults = await sendBatchEmailNotifications(
userIds,
title,
content,
undefined,
undefined,
ipAddress
)
} catch (error) {
console.error('批量发送邮件通知失败:', error)
}
return {
count: notificationCount.count,
total: userIds.length,
meowNotifications: meowResults,
emailNotifications: emailResults
}
} catch (err) {
return null
}
}
/**
* 创建重播申请拒绝通知
*/
export async function createReplayRequestRejectedNotification(
userId: number,
songInfo: { title: string; artist: string },
ipAddress?: string
) {
try {
// 获取用户通知设置
const settingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, userId))
.limit(1)
const settings = settingsResult[0]
// 如果用户关闭了通知,则不发送
if (settings && !settings.enabled) {
return null
}
// 创建通知消息
const message = `您的重播申请《${songInfo.title}》已被管理员拒绝。`
// 创建站内通知
let notification
try {
const notificationResult = await db
.insert(notifications)
.values({
userId,
type: 'REPLAY_REJECTED',
message
})
.returning()
notification = notificationResult[0]
} catch (error) {
throw error
}
// 同步发送 MeoW 通知
try {
await sendMeowNotificationToUser(userId, '重播申请已拒绝', message)
} catch (error) {
console.error('发送 MeoW 通知失败:', error)
}
// 同步发送邮件通知
try {
await sendEmailNotificationToUser(userId, '重播申请已拒绝', message, ipAddress)
} catch (error) {
console.error('发送邮件通知失败:', error)
}
return notification
} catch (err) {
console.error('创建重播申请拒绝通知失败:', err)
return null
}
}