feat: 迁移自定义功能至 1.5.9.9 版本
包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
@@ -1,53 +1,46 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { songs, users } from '~/drizzle/schema'
|
||||
import { and, eq, or } from 'drizzle-orm'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
import { cacheService } from '~~/server/services/cacheService'
|
||||
import { normalizeCoverUrl } from '~~/server/utils/url'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// 验证请求方法
|
||||
if (event.node.req.method !== 'POST') {
|
||||
throw createApiError(405, 'HTTP_METHOD_NOT_ALLOWED', 'Method Not Allowed')
|
||||
throw createError({
|
||||
statusCode: 405,
|
||||
statusMessage: 'Method Not Allowed'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取已验证的用户信息(由中间件提供)
|
||||
const user = event.context.user
|
||||
if (!user) {
|
||||
throw createApiError(401, 'AUTH_UNAUTHORIZED_ACCESS', '未授权访问')
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: '未授权访问'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (!['ADMIN', 'SONG_ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createApiError(403, 'COMMON_INSUFFICIENT_PERMISSION', '权限不足')
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: '权限不足'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取请求体
|
||||
const body = await readBody(event)
|
||||
const {
|
||||
title,
|
||||
artist,
|
||||
requester,
|
||||
semester,
|
||||
musicPlatform,
|
||||
musicId,
|
||||
cover,
|
||||
playUrl,
|
||||
durationSeconds,
|
||||
preferredPlayTimeId
|
||||
} = body
|
||||
const { title, artist, requester, semester, musicPlatform, musicId, cover, playUrl } = body
|
||||
|
||||
// 验证必填字段
|
||||
if (!title || !artist) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.SONG_TITLE_ARTIST_REQUIRED, 'Title and artist are required')
|
||||
}
|
||||
|
||||
// 校验时长范围(30秒~1小时)
|
||||
if (durationSeconds !== null && durationSeconds !== undefined) {
|
||||
const d = Number(durationSeconds)
|
||||
if (!Number.isFinite(d) || d < 30 || d > 3600) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'Invalid song duration (30s–1h)')
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Title and artist are required'
|
||||
})
|
||||
}
|
||||
|
||||
// 查找投稿人用户
|
||||
@@ -89,22 +82,28 @@ export default defineEventHandler(async (event) => {
|
||||
const existingSong = existingSongResult[0]
|
||||
|
||||
if (existingSong) {
|
||||
throw createApiError(409, 'SONG_ALREADY_EXISTS', 'Song already exists')
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'Song already exists'
|
||||
})
|
||||
}
|
||||
|
||||
const normalizedCover = normalizeCoverUrl(cover, `add-song:${title || ''}`)
|
||||
|
||||
// 创建歌曲
|
||||
const newSongResult = await db
|
||||
.insert(songs)
|
||||
.values({
|
||||
title: title.trim(),
|
||||
artist: artist.trim(),
|
||||
singer: artist.trim(),
|
||||
requesterId,
|
||||
semester: semester || null,
|
||||
preferredPlayTimeId: preferredPlayTimeId || null,
|
||||
musicPlatform: musicPlatform || null,
|
||||
musicId: musicId || null,
|
||||
durationSeconds: durationSeconds ? Number(durationSeconds) : null,
|
||||
cover: cover || null,
|
||||
kgHash: musicPlatform === 'kugou' ? musicId : null,
|
||||
cover: normalizedCover,
|
||||
coverUrl: normalizedCover,
|
||||
playUrl: playUrl || null
|
||||
})
|
||||
.returning()
|
||||
@@ -132,6 +131,14 @@ export default defineEventHandler(async (event) => {
|
||||
requester: requesterInfo
|
||||
}
|
||||
|
||||
// 清除歌曲相关缓存
|
||||
try {
|
||||
await cacheService.clearSongsCache()
|
||||
console.log('[Cache] 歌曲缓存已清除(添加歌曲)')
|
||||
} catch (error) {
|
||||
console.error('清除歌曲缓存失败:', error)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
song: songWithRequester
|
||||
@@ -145,7 +152,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Internal server error'
|
||||
statusMessage: error.message || 'Internal server error'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,14 +1,542 @@
|
||||
import { requestSongForUser } from '~~/server/services/songRequestService'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import {
|
||||
collaborationLogs,
|
||||
db,
|
||||
playTimes,
|
||||
requestTimes,
|
||||
semesters,
|
||||
songBlacklists,
|
||||
songCollaborators,
|
||||
songs,
|
||||
systemSettings
|
||||
} from '~/drizzle/db'
|
||||
import { and, eq, gt, gte, inArray, lt, lte, sql } from 'drizzle-orm'
|
||||
import { createCollaborationInvitationNotification } from '~~/server/services/notificationService'
|
||||
import { isLimitReached } from '~~/server/utils/submissionLimit'
|
||||
import { getBeijingTimeISOString } from '~/utils/timeUtils'
|
||||
import { moderateSubmissionNote } from '~~/server/utils/submissionNoteAutoModeration'
|
||||
import { normalizeCoverUrl } from '~~/server/utils/url'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 检查用户认证
|
||||
const user = event.context.user
|
||||
|
||||
if (!user) {
|
||||
throw createApiError(401, 'SONG_LOGIN_REQUIRED_REQUEST', '需要登录才能点歌')
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: '需要登录才能点歌'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
return await requestSongForUser(event, user, body)
|
||||
if (!body.title || !body.artist) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '歌曲名称和艺术家不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// 标准化字符串用于精确匹配
|
||||
const normalizeForMatch = (str: string): string => {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[\s\-_\(\)\[\]【】()「」『』《》〈〉""''""''、,。!?:;~·]/g, '')
|
||||
.replace(/[&&]/g, 'and')
|
||||
.replace(/[feat\.?|ft\.?]/gi, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
const normalizedTitle = normalizeForMatch(body.title)
|
||||
const normalizedArtist = normalizeForMatch(body.artist)
|
||||
|
||||
// 获取当前学期
|
||||
const currentSemester = await getCurrentSemesterName()
|
||||
|
||||
// 哔哩哔哩平台判定逻辑
|
||||
const isBilibili =
|
||||
body.musicPlatform === 'bilibili' ||
|
||||
String(body.musicId || '').startsWith('BV') ||
|
||||
String(body.musicId || '').startsWith('av')
|
||||
|
||||
// 对于哔哩哔哩平台,如果有 musicId,使用 musicId 进行精确匹配
|
||||
if (isBilibili && body.musicId) {
|
||||
// 构建完整的 musicId(包含分P信息)
|
||||
let fullMusicId = String(body.musicId)
|
||||
const bvId = fullMusicId.split(':')[0]
|
||||
|
||||
if (body.bilibiliCid) {
|
||||
const musicIdParts = [bvId, body.bilibiliCid]
|
||||
if (body.bilibiliPage && Number(body.bilibiliPage) > 1) {
|
||||
musicIdParts.push(String(body.bilibiliPage))
|
||||
}
|
||||
fullMusicId = musicIdParts.join(':')
|
||||
}
|
||||
|
||||
// 检查是否已有相同 musicId 的歌曲
|
||||
const existingSongs = await db
|
||||
.select({
|
||||
id: songs.id,
|
||||
musicId: songs.musicId,
|
||||
played: songs.played
|
||||
})
|
||||
.from(songs)
|
||||
.where(
|
||||
and(
|
||||
eq(songs.semester, currentSemester),
|
||||
eq(songs.musicPlatform, 'bilibili'),
|
||||
eq(songs.musicId, fullMusicId)
|
||||
)
|
||||
)
|
||||
|
||||
if (existingSongs.length > 0) {
|
||||
const isSuperAdmin = user.role === 'SUPER_ADMIN'
|
||||
const hasUnplayedDuplicate = existingSongs.some((s) => !s.played)
|
||||
if (!isSuperAdmin || hasUnplayedDuplicate) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `《${body.title}》已经在列表中,不能重复投稿`
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 对于其他平台,使用标题和艺术家进行匹配
|
||||
const allSongs = await db
|
||||
.select({
|
||||
id: songs.id,
|
||||
title: songs.title,
|
||||
artist: songs.artist,
|
||||
semester: songs.semester,
|
||||
played: songs.played
|
||||
})
|
||||
.from(songs)
|
||||
.where(eq(songs.semester, currentSemester))
|
||||
|
||||
const matchingSongs = allSongs.filter((song) => {
|
||||
const songTitle = normalizeForMatch(song.title)
|
||||
const songArtist = normalizeForMatch(song.artist)
|
||||
return songTitle === normalizedTitle && songArtist === normalizedArtist
|
||||
})
|
||||
|
||||
if (matchingSongs.length > 0) {
|
||||
const isSuperAdmin = user.role === 'SUPER_ADMIN'
|
||||
const hasUnplayedDuplicate = matchingSongs.some((s) => !s.played)
|
||||
if (!isSuperAdmin || hasUnplayedDuplicate) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `《${body.title}》已经在列表中,不能重复投稿`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查投稿限额(管理员不受限制)
|
||||
const systemSettingsResult = await db.select().from(systemSettings).limit(1)
|
||||
const systemSettingsData = systemSettingsResult[0]
|
||||
const isAdmin = user.role === 'SUPER_ADMIN' || user.role === 'ADMIN'
|
||||
|
||||
// 加载自动审查需要的黑名单(KEYWORD / LANGUAGE / GENRE)
|
||||
let moderationBlacklists: any[] = []
|
||||
try {
|
||||
moderationBlacklists = await db
|
||||
.select()
|
||||
.from(songBlacklists)
|
||||
.where(inArray(songBlacklists.type, ['KEYWORD', 'LANGUAGE', 'GENRE']))
|
||||
} catch (err) {
|
||||
console.warn('[Request API] 加载自动审查黑名单失败,将跳过自动审查:', err)
|
||||
moderationBlacklists = []
|
||||
}
|
||||
|
||||
// 1. 检查强制关闭
|
||||
if (systemSettingsData?.forceBlockAllRequests && !isAdmin) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '投稿功能已关闭'
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 检查投稿时段限制
|
||||
let hitRequestTime: any = null
|
||||
if (systemSettingsData?.enableRequestTimeLimitation && !isAdmin) {
|
||||
// 使用 ISO 字符串格式进行比较,确保数据库能够正确处理
|
||||
const currentTime = getBeijingTimeISOString()
|
||||
|
||||
const hitRequestTimeResult = await db
|
||||
.select()
|
||||
.from(requestTimes)
|
||||
.where(
|
||||
and(
|
||||
lte(requestTimes.startTime, currentTime),
|
||||
gt(requestTimes.endTime, currentTime),
|
||||
eq(requestTimes.enabled, true)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
hitRequestTime = hitRequestTimeResult[0]
|
||||
|
||||
if (!hitRequestTime) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '当前不在投稿开放时段'
|
||||
})
|
||||
}
|
||||
|
||||
if (hitRequestTime.expected > 0 && hitRequestTime.accepted >= hitRequestTime.expected) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: `当前时段投稿名额已满(${hitRequestTime.accepted}/${hitRequestTime.expected})`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 确定生效的限额类型(三选一逻辑)
|
||||
let effectiveLimit: number | null = null
|
||||
let limitType: 'daily' | 'weekly' | 'monthly' | null = null
|
||||
|
||||
if (systemSettingsData?.enableSubmissionLimit && !isAdmin) {
|
||||
const dailyLimit = systemSettingsData.dailySubmissionLimit
|
||||
const weeklyLimit = systemSettingsData.weeklySubmissionLimit
|
||||
const monthlyLimit = systemSettingsData.monthlySubmissionLimit
|
||||
|
||||
if (dailyLimit !== null && dailyLimit !== undefined) {
|
||||
effectiveLimit = dailyLimit
|
||||
limitType = 'daily'
|
||||
} else if (weeklyLimit !== null && weeklyLimit !== undefined) {
|
||||
effectiveLimit = weeklyLimit
|
||||
limitType = 'weekly'
|
||||
} else if (monthlyLimit !== null && monthlyLimit !== undefined) {
|
||||
effectiveLimit = monthlyLimit
|
||||
limitType = 'monthly'
|
||||
}
|
||||
|
||||
// 如果生效的限额为0,则关闭投稿
|
||||
if (effectiveLimit === 0) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '投稿功能已关闭'
|
||||
})
|
||||
}
|
||||
|
||||
// 如果有生效的限额且大于0,检查使用量
|
||||
// 注意:这里仅作初步检查,严格检查在事务内进行
|
||||
}
|
||||
|
||||
// 检查期望的播出时段是否存在
|
||||
let preferredPlayTime = null
|
||||
if (body.preferredPlayTimeId) {
|
||||
// 检查系统设置是否允许选择播出时段
|
||||
if (!systemSettingsData?.enablePlayTimeSelection) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '播出时段选择功能未启用'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查播出时段是否存在且已启用
|
||||
const playTimeResult = await db
|
||||
.select()
|
||||
.from(playTimes)
|
||||
.where(and(eq(playTimes.id, body.preferredPlayTimeId), eq(playTimes.enabled, true)))
|
||||
.limit(1)
|
||||
preferredPlayTime = playTimeResult[0]
|
||||
|
||||
if (!preferredPlayTime) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '选择的播出时段不存在或未启用'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const rawSubmissionNote = typeof body.submissionNote === 'string' ? body.submissionNote.trim() : ''
|
||||
if (rawSubmissionNote.length > 300) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '备注留言不能超过300个字符'
|
||||
})
|
||||
}
|
||||
const submissionNote =
|
||||
systemSettingsData?.enableSubmissionRemarks && rawSubmissionNote ? rawSubmissionNote : null
|
||||
const rawSubmissionNotePublic = submissionNote !== null ? body.submissionNotePublic !== false : false
|
||||
const requiresApproval = Boolean(systemSettingsData?.submissionNoteRequiresApproval)
|
||||
|
||||
let finalSubmissionNotePublic = rawSubmissionNotePublic
|
||||
let submissionNotePublicStatus: string | null = null
|
||||
|
||||
if (submissionNote && rawSubmissionNotePublic && requiresApproval) {
|
||||
const mod = moderateSubmissionNote(
|
||||
submissionNote,
|
||||
rawSubmissionNotePublic,
|
||||
requiresApproval,
|
||||
{ title: body.title, artist: body.artist, genre: body.genre || null },
|
||||
moderationBlacklists
|
||||
)
|
||||
finalSubmissionNotePublic = mod.notePublic
|
||||
submissionNotePublicStatus = mod.publicStatus
|
||||
if (mod.rejectedReasons.length > 0) {
|
||||
console.log(
|
||||
`[Request API] 投稿留言自动审查未通过,歌曲=${body.title} - ${body.artist},原因:`,
|
||||
mod.rejectedReasons
|
||||
)
|
||||
}
|
||||
} else if (submissionNote) {
|
||||
// 没开自动审查,也记录一下状态
|
||||
submissionNotePublicStatus = null
|
||||
}
|
||||
|
||||
const submissionNotePublic = finalSubmissionNotePublic
|
||||
|
||||
// 准备发送通知的列表
|
||||
const notificationsToSend: { userId: number; songId: number; songTitle: string }[] = []
|
||||
|
||||
// 创建歌曲和更新状态(使用事务)
|
||||
const song = await db.transaction(async (tx) => {
|
||||
// 检查投稿限额(在事务内进行,防止并发绕过限制)
|
||||
if (
|
||||
systemSettingsData?.enableSubmissionLimit &&
|
||||
!isAdmin &&
|
||||
effectiveLimit &&
|
||||
effectiveLimit > 0 &&
|
||||
limitType
|
||||
) {
|
||||
if (await isLimitReached(tx, user.id, limitType, effectiveLimit)) {
|
||||
const labelMap: Record<string, string> = { daily: '每日', weekly: '每周', monthly: '每月' }
|
||||
const timeMap: Record<string, string> = { daily: '今日', weekly: '本周', monthly: '本月' }
|
||||
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `${labelMap[limitType]}投稿限额为${effectiveLimit}首,您${timeMap[limitType]}已达到限额`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有时段限制,再次检查并更新已接纳数量
|
||||
if (hitRequestTime) {
|
||||
const latestRequestTimeResult = await tx
|
||||
.select()
|
||||
.from(requestTimes)
|
||||
.where(eq(requestTimes.id, hitRequestTime.id))
|
||||
.limit(1)
|
||||
const latestRequestTime = latestRequestTimeResult[0]
|
||||
|
||||
if (!latestRequestTime || !latestRequestTime.enabled) {
|
||||
throw createError({ statusCode: 403, message: '投稿时段已失效' })
|
||||
}
|
||||
|
||||
// 使用原子更新确保并发安全
|
||||
const updateResult = await tx
|
||||
.update(requestTimes)
|
||||
.set({
|
||||
accepted: sql`${requestTimes.accepted} + 1`
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(requestTimes.id, hitRequestTime.id),
|
||||
// 如果有限额,确保更新时未超限(针对 expected > 0 的情况)
|
||||
latestRequestTime.expected > 0
|
||||
? lt(requestTimes.accepted, latestRequestTime.expected)
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
.returning()
|
||||
|
||||
if (updateResult.length === 0) {
|
||||
throw createError({ statusCode: 403, message: '当前时段投稿名额已满' })
|
||||
}
|
||||
}
|
||||
|
||||
if (!body.expectedPlayDate || body.expectedPlaySequence === undefined || body.expectedPlaySequence === null) {
|
||||
throw createError({ statusCode: 400, message: '请选择期望播出日期与期望播放序号' })
|
||||
}
|
||||
|
||||
// 检查 expectedPlaySequence 是否已被占用
|
||||
if (body.expectedPlayDate && body.expectedPlaySequence !== undefined && body.expectedPlaySequence !== null) {
|
||||
const expectedSequence = Number(body.expectedPlaySequence)
|
||||
const occupiedCheck = await tx
|
||||
.select({ id: songs.id })
|
||||
.from(songs)
|
||||
.where(
|
||||
and(
|
||||
eq(songs.expectedPlayDate, body.expectedPlayDate),
|
||||
eq(songs.expectedPlaySequence, expectedSequence),
|
||||
eq(songs.played, false) // 只有未播放的才算被占用
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (occupiedCheck.length > 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `日期 ${body.expectedPlayDate} 的第 ${expectedSequence} 序号已被占用,请选择其他序号`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 创建歌曲
|
||||
let finalMusicId = body.musicId ? String(body.musicId) : null
|
||||
|
||||
// 如果是 Bilibili 平台,处理 musicId 格式
|
||||
if (isBilibili) {
|
||||
const bvId = finalMusicId?.split(':')[0]
|
||||
if (bvId) {
|
||||
const musicIdParts = [bvId]
|
||||
if (body.bilibiliCid) {
|
||||
musicIdParts.push(body.bilibiliCid)
|
||||
if (body.bilibiliPage && Number(body.bilibiliPage) > 1) {
|
||||
musicIdParts.push(String(body.bilibiliPage))
|
||||
}
|
||||
}
|
||||
finalMusicId = musicIdParts.join(':')
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedCover = normalizeCoverUrl(body.cover, `request-song:${body.title || ''}`)
|
||||
|
||||
const songResult = await tx
|
||||
.insert(songs)
|
||||
.values({
|
||||
title: body.title,
|
||||
artist: body.artist,
|
||||
singer: body.artist,
|
||||
requesterId: user.id,
|
||||
preferredPlayTimeId: preferredPlayTime?.id || null,
|
||||
semester: currentSemester, // 使用外部获取的学期名称
|
||||
cover: normalizedCover,
|
||||
coverUrl: normalizedCover,
|
||||
musicPlatform: isBilibili ? 'bilibili' : (body.musicPlatform || null),
|
||||
musicId: finalMusicId,
|
||||
kgHash: body.musicPlatform === 'kugou' ? body.musicId : null,
|
||||
playUrl: body.playUrl || null,
|
||||
submissionNote,
|
||||
submissionNotePublic,
|
||||
submissionNotePublicStatus,
|
||||
hitRequestId: hitRequestTime?.id || null,
|
||||
expectedPlayDate: body.expectedPlayDate || null,
|
||||
expectedPlaySequence: body.expectedPlaySequence !== undefined ? Number(body.expectedPlaySequence) : null
|
||||
})
|
||||
.returning()
|
||||
const newSong = songResult[0]
|
||||
|
||||
// 处理联合投稿人
|
||||
if (
|
||||
body.collaborators &&
|
||||
Array.isArray(body.collaborators) &&
|
||||
body.collaborators.length > 0
|
||||
) {
|
||||
// 去重
|
||||
const uniqueCollaboratorIds = [...new Set(body.collaborators.map((id: any) => Number(id)))]
|
||||
|
||||
for (const collaboratorId of uniqueCollaboratorIds) {
|
||||
// 跳过自己或无效ID
|
||||
if (isNaN(collaboratorId) || collaboratorId === user.id) continue
|
||||
|
||||
try {
|
||||
// 检查是否已经是联合投稿人
|
||||
const existingCollab = await tx
|
||||
.select()
|
||||
.from(songCollaborators)
|
||||
.where(
|
||||
and(
|
||||
eq(songCollaborators.songId, newSong.id),
|
||||
eq(songCollaborators.userId, collaboratorId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingCollab.length > 0) continue
|
||||
|
||||
// 创建联合投稿记录
|
||||
const collabResult = await tx
|
||||
.insert(songCollaborators)
|
||||
.values({
|
||||
songId: newSong.id,
|
||||
userId: collaboratorId,
|
||||
status: 'PENDING'
|
||||
})
|
||||
.returning()
|
||||
|
||||
const collab = collabResult[0]
|
||||
|
||||
// 记录审计日志
|
||||
await tx.insert(collaborationLogs).values({
|
||||
collaboratorId: collab.id,
|
||||
action: 'INVITE',
|
||||
operatorId: user.id,
|
||||
ipAddress:
|
||||
(event.node.req.headers['x-forwarded-for'] as string) ||
|
||||
event.node.req.socket.remoteAddress
|
||||
})
|
||||
|
||||
// 添加到通知列表,事务结束后发送
|
||||
notificationsToSend.push({
|
||||
userId: collaboratorId,
|
||||
songId: newSong.id,
|
||||
songTitle: newSong.title
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`邀请用户 ${collaboratorId} 失败:`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newSong
|
||||
})
|
||||
|
||||
// 事务提交成功后,发送通知
|
||||
// 即使通知发送失败,也不影响点歌结果
|
||||
for (const notification of notificationsToSend) {
|
||||
try {
|
||||
await createCollaborationInvitationNotification(
|
||||
user.id,
|
||||
notification.userId,
|
||||
notification.songId,
|
||||
notification.songTitle
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`发送邀请通知给用户 ${notification.userId} 失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return song
|
||||
} catch (error: any) {
|
||||
console.error('点歌失败:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
} else if (error.message === '未设置活跃学期') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '系统未设置当前活跃学期,请联系管理员'
|
||||
})
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '点歌失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前学期名称
|
||||
async function getCurrentSemesterName() {
|
||||
try {
|
||||
// 获取当前活跃的学期
|
||||
const currentSemesterResult = await db
|
||||
.select()
|
||||
.from(semesters)
|
||||
.where(eq(semesters.isActive, true))
|
||||
.limit(1)
|
||||
const currentSemester = currentSemesterResult[0]
|
||||
|
||||
if (currentSemester) {
|
||||
return currentSemester.name
|
||||
}
|
||||
|
||||
// 如果没有活跃学期,抛出错误
|
||||
throw new Error('未设置活跃学期')
|
||||
} catch (error) {
|
||||
console.error('获取当前学期失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user