import { db } from '~/drizzle/db' import { schedules, songReplayRequests, scheduleSongPool } from '~/drizzle/schema' import { and, eq, gte, inArray, lte } from 'drizzle-orm' import { cacheService } from '~~/server/services/cacheService' function isValidDateString(dateStr: unknown) { if (typeof dateStr !== 'string') return false const trimmed = dateStr.trim() if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return false const date = new Date(trimmed + 'T00:00:00.000Z') return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === trimmed } export default defineEventHandler(async (event) => { const user = event.context.user if (!user || !['SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes(user.role)) { throw createError({ statusCode: 403, message: '权限不足' }) } const body = await readBody(event) const playDate = body?.playDate const playTimeId = body?.playTimeId const songIds = body?.songIds if (!isValidDateString(playDate)) { throw createError({ statusCode: 400, message: '播放日期格式必须为YYYY-MM-DD' }) } if (!Array.isArray(songIds) || !songIds.every((id) => typeof id === 'number' && id > 0)) { throw createError({ statusCode: 400, message: '歌曲ID列表不能为空且必须为正整数数组' }) } const playTimeFilterEnabled = playTimeId !== undefined && playTimeId !== null if (playTimeFilterEnabled && (typeof playTimeId !== 'number' || playTimeId <= 0)) { throw createError({ statusCode: 400, message: '播放时间ID必须是正整数' }) } const startOfDay = new Date(playDate + 'T00:00:00.000Z') const endOfDay = new Date(playDate + 'T23:59:59.999Z') const result = await db.transaction(async (tx) => { const whereBase = and(gte(schedules.playDate, startOfDay), lte(schedules.playDate, endOfDay)) const whereCondition = playTimeFilterEnabled ? and(whereBase, eq(schedules.playTimeId, playTimeId)) : whereBase const existingSchedules = await tx .select({ id: schedules.id, songId: schedules.songId }) .from(schedules) .where(whereCondition) const existingSongIds = existingSchedules.map((s) => s.songId).filter(Boolean) as number[] await tx.delete(schedules).where(whereCondition) if (existingSongIds.length > 0) { await tx .update(songReplayRequests) .set({ status: 'PENDING', updatedAt: new Date() }) .where( and(inArray(songReplayRequests.songId, existingSongIds), eq(songReplayRequests.status, 'FULFILLED')) ) } if (songIds.length === 0) { return { deletedCount: existingSchedules.length, insertedCount: 0 } } const rows = songIds.map((songId, index) => ({ songId, playDate: startOfDay, sequence: index + 1, playTimeId: playTimeFilterEnabled ? playTimeId : null, isDraft: true, publishedAt: null })) const inserted = await tx.insert(schedules).values(rows).returning({ id: schedules.id }) if (songIds.length > 0) { await tx .update(scheduleSongPool) .set({ status: 'ASSIGNED', targetPlayDate: startOfDay }) .where( and( inArray(scheduleSongPool.songId, songIds), eq(scheduleSongPool.status, 'PENDING') ) ) } return { deletedCount: existingSchedules.length, insertedCount: inserted.length } }) await Promise.allSettled([cacheService.clearSchedulesCache(), cacheService.clearSongsCache()]) return { success: true, ...result } })