Files
BYW-VoiceHub/server/api/songs/replay.post.ts
T

140 lines
5.5 KiB
TypeScript

import { and, db, desc, eq, songs, songReplayRequests, semesters, playTimes } from '~/drizzle/db'
import { getSystemSettingsCached } from '~~/server/utils/system-settings-helper'
import { createApiError } from '~~/server/utils/apiError'
import { SUBMISSION_NOTE_STATUS } from '~~/server/config/constants'
import { z } from 'zod'
const replayRequestSchema = z.object({
songId: z.number().int().gt(0, '歌曲ID无效'),
submissionNote: z.string().trim().max(300, '备注留言不能超过300个字符').optional().nullable(),
submissionNotePublic: z.boolean().optional(),
preferredPlayTimeId: z.number().int().gt(0, '播出时段 ID 无效').optional().nullable()
})
export default defineEventHandler(async (event) => {
// 1. 检查用户认证
const user = event.context.user
if (!user) {
throw createApiError(401, 'SONG_LOGIN_REQUIRED_REPLAY', '需要登录才能申请重播')
}
// 2. 读取请求体
const body = await readBody(event)
const parsedBody = replayRequestSchema.safeParse(body || {})
if (!parsedBody.success) {
const issueText = (parsedBody.error.issues || []).map((issue) => issue.message).join(', ')
throw createApiError(400, 'USER_REQUEST_VALIDATION_FAILED', `请求参数验证失败:${issueText}`, {
params: [issueText]
})
}
const { songId, preferredPlayTimeId } = parsedBody.data
// 3. 检查系统设置
const settings = await getSystemSettingsCached()
if (!settings?.enableReplayRequests) {
throw createApiError(403, 'SONG_REPLAY_DISABLED', '重播申请功能未开启')
}
let preferredPlayTime = null
if (preferredPlayTimeId) {
if (!settings.enablePlayTimeSelection) {
throw createApiError(400, 'SONG_REPLAY_PLAY_TIME_DISABLED', '播出时段选择功能未启用')
}
const playTimeResult = await db
.select()
.from(playTimes)
.where(and(eq(playTimes.id, preferredPlayTimeId), eq(playTimes.enabled, true)))
.limit(1)
preferredPlayTime = playTimeResult[0]
if (!preferredPlayTime) {
throw createApiError(400, 'SONG_REPLAY_PLAY_TIME_INVALID', '选择的播出时段不存在或未启用')
}
}
const rawSubmissionNote = parsedBody.data.submissionNote || ''
const submissionNote = settings.enableSubmissionRemarks && rawSubmissionNote ? rawSubmissionNote : null
// 公开留言审核:开关开启时申请留言不立即公开,进入待审后由管理员通过
const noteRequiresApproval = settings.submissionNoteRequiresApproval === true
const wantsPublic = submissionNote !== null ? parsedBody.data.submissionNotePublic !== false : false
const submissionNotePublic = noteRequiresApproval ? false : wantsPublic
// 仅用户勾选公开的留言进入审核,私密留言只供管理员查看
const submissionNotePublicStatus =
noteRequiresApproval && submissionNote !== null && wantsPublic
? SUBMISSION_NOTE_STATUS.PENDING
: null
// 4. 检查歌曲和学期
const songResult = await db.select().from(songs).where(eq(songs.id, songId)).limit(1)
const song = songResult[0]
if (!song) {
throw createApiError(404, 'SONG_NOT_FOUND', '歌曲不存在')
}
if (!song.played) {
throw createApiError(400, 'SONG_NOT_PLAYED_NO_REPLAY', '该歌曲尚未播放,无法申请重播')
}
// 获取当前学期
const currentSemesterResult = await db
.select()
.from(semesters)
.where(eq(semesters.isActive, true))
.limit(1)
const currentSemester = currentSemesterResult[0]
// 验证学期
if (currentSemester) {
if (song.semester !== currentSemester.name) {
throw createApiError(400, 'SONG_REPLAY_CURRENT_SEMESTER_ONLY', '只能申请重播当前学期的歌曲')
}
} else {
throw createApiError(400, 'SONG_NO_ACTIVE_SEMESTER_REPLAY', '当前没有活跃学期,无法申请重播')
}
// 5. 检查重复申请和冷却期(insert-only:历史申请不覆写,每次申请新增一条记录)
const latestRequestResult = await db
.select()
.from(songReplayRequests)
.where(and(eq(songReplayRequests.songId, songId), eq(songReplayRequests.userId, user.id)))
.orderBy(desc(songReplayRequests.createdAt))
.limit(1)
const latestRequest = latestRequestResult[0]
if (latestRequest) {
if (latestRequest.status === 'PENDING') {
throw createApiError(400, 'SONG_REPLAY_ALREADY_REQUESTED', '您已经申请过重播该歌曲')
}
// REJECTED 或 FULFILLED 均需冷却 24 小时后才能再次申请
const COOLDOWN_HOURS = 24
const cooldownTime = COOLDOWN_HOURS * 60 * 60 * 1000
const timeSinceUpdate = Date.now() - new Date(latestRequest.updatedAt).getTime()
if (timeSinceUpdate < cooldownTime) {
const remainingHours = Math.ceil((cooldownTime - timeSinceUpdate) / (60 * 60 * 1000))
throw createApiError(429, 'SONG_REPLAY_COOLDOWN', `重播申请冷却中,还需等待 ${remainingHours} 小时`, { params: [remainingHours] })
}
}
// 6. 插入申请记录
try {
await db.insert(songReplayRequests).values({
songId,
userId: user.id,
preferredPlayTimeId: preferredPlayTime?.id || null,
submissionNote,
submissionNotePublic,
submissionNotePublicStatus
})
return { success: true, message: latestRequest ? '重新申请重播成功' : '申请重播成功' }
} catch (error: any) {
// 并发提交时命中待处理申请的部分唯一索引
if (error.code === '23505') {
throw createApiError(400, 'SONG_REPLAY_ALREADY_REQUESTED', '您已经申请过重播该歌曲')
}
throw error
}
})