import { db } from '~/drizzle/db' import { schedules, songs } from '~/drizzle/schema' import { and, desc, eq, gte, lte, ne } from 'drizzle-orm' import { createSongSelectedNotification, createReplaySongSelectedNotification } from '../../services/notificationService' import { redeemCardCodeForSchedule } from '~~/server/services/cardCodeLifecycleService' import { fulfillReplayRequestsForSchedule } from '~~/server/utils/scheduleReplayBinding' import { getServerDate } from '~~/server/utils/serverTime' export default defineEventHandler(async (event) => { // 检查用户认证和权限 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) if (!body.songId || !body.playDate) { throw createError({ statusCode: 400, message: '歌曲ID和播放日期不能为空' }) } // 检查是否为草稿模式(默认直接发布) const isDraft = body.isDraft === true // 前端显式选择的重播申请,发布时优先绑定 const preferredReplayRequestId = Number.isInteger(Number(body.replayRequestId)) && Number(body.replayRequestId) > 0 ? Number(body.replayRequestId) : null try { // 检查歌曲是否存在 const songResult = await db.select().from(songs).where(eq(songs.id, body.songId)).limit(1) const song = songResult[0] if (!song) { throw createError({ statusCode: 404, message: '歌曲不存在' }) } const transactionResult = await db.transaction(async (tx) => { // 获取序号,如果未提供则查找当天最大序号+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') console.log('查询当天排期范围:', { 输入日期: body.playDate, 开始时间: startOfDay.toISOString(), 结束时间: endOfDay.toISOString() }) const sameDaySchedules = await tx .select() .from(schedules) .where(and(gte(schedules.playDate, startOfDay), lte(schedules.playDate, endOfDay))) .orderBy(desc(schedules.sequence)) .limit(1) const latestSchedule = sameDaySchedules[0] if (latestSchedule) sequence = (latestSchedule.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: isDraft, publishedAt: isDraft ? null : getServerDate() }) .returning() const createdSchedule = scheduleResult[0] if (!createdSchedule) { throw createError({ statusCode: 500, message: '创建排期失败' }) } const schedule = { ...createdSchedule, song: { id: song.id, title: song.title, artist: song.artist, requesterId: song.requesterId, cardCodeId: song.cardCodeId } } let shouldNotify = false let replayRequesterIds: number[] = [] // 只有在非草稿模式下才更新重播状态 if (!isDraft) { // 检查该歌曲是否已经有其他已发布的排期 const existingPublished = await tx .select({ id: schedules.id }) .from(schedules) .where( and( eq(schedules.songId, schedule.song.id), eq(schedules.isDraft, false), ne(schedules.id, createdSchedule.id) ) ) .limit(1) // 如果之前没有正式发布过,才发送通知(首次排期通知) shouldNotify = existingPublished.length === 0 const replayBinding = await fulfillReplayRequestsForSchedule({ tx, songId: schedule.song.id, scheduleId: createdSchedule.id, at: createdSchedule.publishedAt || getServerDate(), preferredRequestId: preferredReplayRequestId || undefined }) if (replayBinding) { replayRequesterIds = replayBinding.replayRequesterIds } if (existingPublished.length > 0) { console.log(`歌曲 ${schedule.song.id} 已有其他正式排期,不再重复发送通知`) } await redeemCardCodeForSchedule(tx, { songId: schedule.song.id, cardCodeId: schedule.song.cardCodeId, operatorId: user.id, at: createdSchedule.publishedAt || getServerDate() }) } return { schedule, shouldNotify, replayRequesterIds } }) let notificationSent = true if (transactionResult.shouldNotify) { try { await createSongSelectedNotification(song.requesterId, song.id, { title: song.title, artist: song.artist, playDate: transactionResult.schedule.playDate }) } catch (notificationError) { notificationSent = false console.error('排期已创建,但发送歌曲入选通知失败:', notificationError) } } // 发送重播申请已安排通知 if (!isDraft && transactionResult.replayRequesterIds.length > 0) { for (const replayUserId of transactionResult.replayRequesterIds) { try { await createReplaySongSelectedNotification(replayUserId, song.id, { title: song.title, artist: song.artist, playDate: transactionResult.schedule.playDate }, transactionResult.schedule.id) } catch (error) { console.error(`发送重播安排通知给用户 ${replayUserId} 失败:`, error) } } } const schedule = transactionResult.schedule return { ...schedule, isDraft: isDraft, publishedAt: isDraft ? null : schedule.publishedAt, message: isDraft ? '排期草稿保存成功' : transactionResult.shouldNotify ? notificationSent ? '排期创建成功,通知已发送' : '排期创建成功,但通知发送失败' : '排期创建成功' } } catch (error: any) { console.error('创建排期失败:', error) if (error.statusCode) throw error throw createError({ statusCode: 500, message: error.message || '创建排期失败' }) } })