247 lines
7.3 KiB
TypeScript
247 lines
7.3 KiB
TypeScript
import { db } from '~/drizzle/db'
|
|
import { playTimes, schedules, songs, songReplayRequests } from '~/drizzle/schema'
|
|
import { and, desc, eq, gte, lte } from 'drizzle-orm'
|
|
import { cacheService } from '~~/server/services/cacheService'
|
|
import { getClientIP } from '~~/server/utils/ip-utils'
|
|
|
|
// 输入验证函数
|
|
function validateInput(body: any) {
|
|
const errors: string[] = []
|
|
|
|
// 验证songId
|
|
if (!body.songId) {
|
|
errors.push('歌曲ID不能为空')
|
|
} else if (typeof body.songId !== 'number' || body.songId <= 0) {
|
|
errors.push('歌曲ID必须是正整数')
|
|
}
|
|
|
|
// 验证playDate
|
|
if (!body.playDate) {
|
|
errors.push('播放日期不能为空')
|
|
} else {
|
|
const dateStr = typeof body.playDate === 'string' ? body.playDate : body.playDate.toString()
|
|
const dateRegex = /^\d{4}-\d{2}-\d{2}$/
|
|
if (!dateRegex.test(dateStr)) {
|
|
errors.push('播放日期格式必须为YYYY-MM-DD')
|
|
} else {
|
|
const date = new Date(dateStr)
|
|
if (isNaN(date.getTime())) {
|
|
errors.push('播放日期格式无效')
|
|
}
|
|
}
|
|
}
|
|
|
|
// 验证sequence(可选)
|
|
if (body.sequence !== undefined && body.sequence !== null) {
|
|
if (typeof body.sequence !== 'number' || body.sequence <= 0) {
|
|
errors.push('序号必须是正整数')
|
|
}
|
|
}
|
|
|
|
// 验证playTimeId(可选)
|
|
if (body.playTimeId !== undefined && body.playTimeId !== null) {
|
|
if (typeof body.playTimeId !== 'number' || body.playTimeId <= 0) {
|
|
errors.push('播放时间ID必须是正整数')
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const startTime = Date.now()
|
|
const clientIP = getClientIP(event)
|
|
|
|
try {
|
|
// 检查用户认证和权限
|
|
const user = event.context.user
|
|
|
|
if (!user) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
message: '未授权访问'
|
|
})
|
|
}
|
|
|
|
if (!['SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: '权限不足'
|
|
})
|
|
}
|
|
|
|
const body = await readBody(event)
|
|
|
|
// 输入验证
|
|
const validationErrors = validateInput(body)
|
|
if (validationErrors.length > 0) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: validationErrors.join('; ')
|
|
})
|
|
}
|
|
|
|
// 使用数据库事务确保数据一致性
|
|
const result = await db.transaction(async (tx) => {
|
|
// 检查歌曲是否存在
|
|
const songResult = await tx.select().from(songs).where(eq(songs.id, body.songId)).limit(1)
|
|
|
|
const song = songResult[0]
|
|
|
|
if (!song) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
message: '歌曲不存在'
|
|
})
|
|
}
|
|
|
|
// 检查播放时间ID是否有效(如果提供)
|
|
if (body.playTimeId) {
|
|
const playTimeResult = await tx
|
|
.select()
|
|
.from(playTimes)
|
|
.where(eq(playTimes.id, body.playTimeId))
|
|
.limit(1)
|
|
|
|
if (playTimeResult.length === 0) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
message: '播放时间不存在'
|
|
})
|
|
}
|
|
}
|
|
|
|
// 检查是否已经为该歌曲创建过排期或草稿,如果有则删除旧的
|
|
const existingScheduleResult = await tx
|
|
.select()
|
|
.from(schedules)
|
|
.where(eq(schedules.songId, body.songId))
|
|
.limit(1)
|
|
|
|
const existingSchedule = existingScheduleResult[0]
|
|
if (existingSchedule) {
|
|
// 删除现有排期或草稿
|
|
await tx.delete(schedules).where(eq(schedules.id, existingSchedule.id))
|
|
|
|
// 恢复该歌曲的重播申请状态为 PENDING
|
|
// 只恢复状态为 FULFILLED 的申请
|
|
const updatedRequests = await tx
|
|
.update(songReplayRequests)
|
|
.set({
|
|
status: 'PENDING',
|
|
updatedAt: new Date()
|
|
})
|
|
.where(
|
|
and(
|
|
eq(songReplayRequests.songId, body.songId),
|
|
eq(songReplayRequests.status, 'FULFILLED')
|
|
)
|
|
)
|
|
.returning()
|
|
|
|
if (updatedRequests.length > 0) {
|
|
console.log(
|
|
`恢复了 ${updatedRequests.length} 个重播申请状态为 PENDING(草稿保存时删除旧排期)`
|
|
)
|
|
}
|
|
}
|
|
|
|
// 获取序号,如果未提供则查找当天最大序号+1
|
|
let sequence = body.sequence || 1
|
|
|
|
if (!body.sequence) {
|
|
// 解析输入的日期字符串
|
|
const inputDateStr =
|
|
typeof body.playDate === 'string' ? body.playDate : body.playDate.toISOString()
|
|
|
|
// 创建当天的开始和结束时间
|
|
const startOfDay = new Date(inputDateStr + 'T00:00:00.000Z')
|
|
const endOfDay = new Date(inputDateStr + 'T23:59:59.999Z')
|
|
|
|
// 查找当天的所有排期和草稿(包括草稿状态的)
|
|
const sameDaySchedules = await tx
|
|
.select()
|
|
.from(schedules)
|
|
.where(and(gte(schedules.playDate, startOfDay), lte(schedules.playDate, endOfDay)))
|
|
.orderBy(desc(schedules.sequence))
|
|
.limit(1)
|
|
|
|
if (sameDaySchedules.length > 0) {
|
|
sequence = (sameDaySchedules[0].sequence || 0) + 1
|
|
}
|
|
}
|
|
|
|
// 解析输入的日期字符串,确保日期正确
|
|
const inputDateStr =
|
|
typeof body.playDate === 'string' ? body.playDate : body.playDate.toISOString()
|
|
|
|
// 直接使用输入的日期字符串,添加时间部分以避免时区问题
|
|
const playDate = new Date(inputDateStr + 'T00:00:00.000Z')
|
|
|
|
// 创建草稿排期
|
|
const scheduleResult = await tx
|
|
.insert(schedules)
|
|
.values({
|
|
songId: body.songId,
|
|
playDate: playDate,
|
|
sequence: sequence,
|
|
playTimeId: body.playTimeId || null,
|
|
isDraft: true, // 标记为草稿
|
|
publishedAt: null // 草稿状态下没有发布时间
|
|
})
|
|
.returning()
|
|
|
|
return {
|
|
schedule: scheduleResult[0],
|
|
song: {
|
|
id: song.id,
|
|
title: song.title,
|
|
artist: song.artist,
|
|
requesterId: song.requesterId
|
|
}
|
|
}
|
|
})
|
|
|
|
// 清除相关缓存(在事务外执行,避免影响事务性能)
|
|
try {
|
|
await cacheService.clearSchedulesCache()
|
|
await cacheService.clearSongsCache()
|
|
console.log('[Cache] 排期缓存和歌曲列表缓存已清除(保存草稿)')
|
|
} catch (cacheError) {
|
|
console.error('[Cache] 清除缓存失败:', cacheError)
|
|
// 缓存清除失败不应该影响主要操作的成功
|
|
}
|
|
|
|
const responseData = {
|
|
...result.schedule,
|
|
song: result.song,
|
|
isDraft: true,
|
|
publishedAt: null,
|
|
message: '排期草稿保存成功'
|
|
}
|
|
|
|
console.log(`[Performance] 保存排期草稿耗时: ${Date.now() - startTime}ms`)
|
|
|
|
return responseData
|
|
} catch (error: any) {
|
|
console.error('保存排期草稿失败:', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
userId: event.context.user?.id,
|
|
ip: clientIP,
|
|
duration: Date.now() - startTime
|
|
})
|
|
|
|
// 根据错误类型返回适当的错误信息
|
|
if (error.statusCode) {
|
|
throw error // 重新抛出已经格式化的错误
|
|
}
|
|
|
|
// 对于未知错误,返回具体的错误信息
|
|
throw createError({
|
|
statusCode: 500,
|
|
message: error.message || '服务器内部错误,请稍后重试'
|
|
})
|
|
}
|
|
})
|