140 lines
5.5 KiB
TypeScript
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
|
|
}
|
|
})
|