103 lines
4.5 KiB
TypeScript
103 lines
4.5 KiB
TypeScript
import { defineEventHandler, readBody } from 'h3'
|
|
import { db } from '~/drizzle/db'
|
|
import { songs, scheduleSongPool } from '~/drizzle/schema'
|
|
import { eq, inArray } from 'drizzle-orm'
|
|
import { getServerDate } from '~~/server/utils/serverTime'
|
|
import { createApiError } from '~~/server/utils/apiError'
|
|
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
|
import { requireSongAdmin } from '~~/server/utils/requireSongAdmin'
|
|
import { fetchPoolCount } from '~~/server/utils/scheduleSongPool'
|
|
import { fetchSongDuration } from '~~/server/utils/songDurationFetcher'
|
|
export default defineEventHandler(async (event) => {
|
|
requireSongAdmin(event)
|
|
const user = event.context.user as any
|
|
const body = await readBody(event)
|
|
if (!Array.isArray(body.songIds) || body.songIds.length === 0) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'songIds 必须为非空数组')
|
|
}
|
|
const songIds = body.songIds.map(Number).filter((n) => Number.isInteger(n) && n > 0)
|
|
if (songIds.length === 0) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'songIds 全部无效')
|
|
}
|
|
if (songIds.length > 1000) {
|
|
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'songIds 数量不能超过 1000')
|
|
}
|
|
const now = getServerDate()
|
|
// 批量查询歌曲,避免 N+1
|
|
const songsRows = await db.select().from(songs).where(inArray(songs.id, songIds))
|
|
// 时长补全放到响应后的后台任务,避免批量加入时串行等待外部平台
|
|
const missingDurationSongs = songsRows.filter(
|
|
(s) => s.durationSeconds == null && s.musicPlatform && s.musicId
|
|
)
|
|
const frontendDurationMap = new Map(
|
|
(Array.isArray(body.songDurations) ? body.songDurations : [])
|
|
.map((item) => [Number(item?.songId), Number(item?.durationSeconds)])
|
|
.filter(([songId, duration]) => Number.isInteger(songId) && Number.isFinite(duration))
|
|
)
|
|
const durationTolerance = 3
|
|
const clientDurationUpdates = missingDurationSongs
|
|
.map((song) => {
|
|
const duration = frontendDurationMap.get(song.id)
|
|
return Number.isInteger(duration) && duration >= 30 && duration <= 3600
|
|
? { song, duration }
|
|
: null
|
|
})
|
|
.filter(Boolean)
|
|
await Promise.all(
|
|
clientDurationUpdates.map(({ song, duration }) =>
|
|
db.update(songs).set({ durationSeconds: duration }).where(eq(songs.id, song.id))
|
|
)
|
|
)
|
|
if (missingDurationSongs.length > 0) {
|
|
const durationTask = (async () => {
|
|
for (const song of missingDurationSongs.filter(
|
|
(item) => !clientDurationUpdates.some((update) => update.song.id === item.id)
|
|
)) {
|
|
const duration = await fetchSongDuration(song.musicPlatform, song.musicId)
|
|
if (duration != null) {
|
|
const frontendDuration = frontendDurationMap.get(song.id)
|
|
if (frontendDuration != null && Math.abs(duration - frontendDuration) > durationTolerance) {
|
|
console.warn(`[备选池] 歌曲 #${song.id} 前端时长 ${frontendDuration}s 与后端获取 ${duration}s 不一致,以后端为准`)
|
|
}
|
|
await db.update(songs).set({ durationSeconds: duration }).where(eq(songs.id, song.id))
|
|
}
|
|
}
|
|
})()
|
|
if (typeof event.waitUntil === 'function') {
|
|
event.waitUntil(durationTask)
|
|
} else {
|
|
durationTask.catch((error) => console.error('[备选池] 后台补全时长失败:', error))
|
|
}
|
|
}
|
|
const refreshedMap = new Map(songsRows.map((s) => [s.id, s]))
|
|
// 构建待插入值和跳过列表
|
|
const insertValues = []
|
|
const skipped = []
|
|
for (const songId of songIds) {
|
|
const song = refreshedMap.get(songId)
|
|
if (!song) {
|
|
skipped.push({ songId, reason: '歌曲不存在(无法加入备选池)' })
|
|
continue
|
|
}
|
|
insertValues.push({ songId, title: song.title, artist: song.artist, createdAt: now, addedBy: user.id })
|
|
}
|
|
// 批量插入
|
|
const added = []
|
|
if (insertValues.length > 0) {
|
|
const inserted = await db
|
|
.insert(scheduleSongPool)
|
|
.values(insertValues.map((v) => ({ songId: v.songId, createdAt: v.createdAt, addedBy: v.addedBy })))
|
|
.onConflictDoNothing({ target: [scheduleSongPool.songId] })
|
|
.returning()
|
|
const insertedIds = new Set(inserted.map((r) => r.songId))
|
|
for (const v of insertValues) {
|
|
if (insertedIds.has(v.songId)) {
|
|
added.push({ songId: v.songId, title: v.title, artist: v.artist })
|
|
} else {
|
|
skipped.push({ songId: v.songId, reason: '已存在于备选池' })
|
|
}
|
|
}
|
|
}
|
|
return { added, skipped, total: await fetchPoolCount() }
|
|
})
|