* fix+style(schedule): ScheduleManager 标签页紧凑布局,头部 min-height 防抖,内容区统间距与移动端适配 * feat(schedule): 新增自动排期策略「按投稿人数优先覆盖」,按 requester+collaborators 数排序分配好时段 * feat(schedule): localStorage 键 voicehub:schedule:auto-config 持久化策略/时段/学期/过滤等7项,onMounted 恢复,watch debounce 500ms 保存 * fix(schedule): songs/index.get.ts 新增 use_pool 查询参数,备选池过滤条件 status=PENDING + song.played=false + song未在任何排期中;ScheduleManager/draft-bulk 创建排期时将池记录 UPDATE 为 ASSIGNED * feat(player): ScheduleManager 工具栏新增「构建播放队列」按钮,按 playDate+playTimeId+sequence 排序后调用 useAudioPlayer.playSong 加载播放列表,支持 confirm 确认与 toast 反馈 * refactor(useSongs): fetchSongs 增加第5参数 usePool,透传 use_pool=true 到 /api/songs
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
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
|
|
}
|
|
})
|