Files
BYW-VoiceHub/server/api/admin/songs/batch-reject.post.ts
T
Henry_Du bd8df1f026
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 6m31s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
- 新增 /api/admin/songs/batch-reject.post 批量驳回接口(事务清排期/投票/扣接纳数/黑名单去重/异步通知)
- requireSongAdmin 三角色校验 + createApiError 统一错误包装
- scheduleDaysBefore/After 四字段排期可见范围限制(开放 API 与公开排期接口三处缓存路径 + DB 条件过滤)
- submissionNoteAutoModeration 基于黑名单 KEYWORD/LANGUAGE/GENRE 自动判定留言 publicStatus,命中自动关闭 submissionNotePublic
- 更新 songs/request.post 与 songs/[id]/update.put 接入自动审查逻辑
2026-09-06 17:03:20 +08:00

136 lines
4.9 KiB
TypeScript

import { db } from '~/drizzle/db'
import { schedules, songBlacklists, songs, votes, requestTimes } from '~/drizzle/schema'
import { and, eq, inArray, sql } from 'drizzle-orm'
import { cacheService } from '../../../services/cacheService'
import { createSongRejectedNotification } from '../../../services/notificationService'
import { requireSongAdmin } from '~~/server/utils/requireSongAdmin'
import { createApiError } from '~~/server/utils/apiError'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
const MAX_BATCH_REJECT_COUNT = 1000
export default defineEventHandler(async (event) => {
const user = requireSongAdmin(event)
const body = await readBody(event)
const songIds = Array.isArray(body?.songIds)
? [...new Set(body.songIds.filter((id: any) => Number.isInteger(id) && id > 0))]
: []
const reason = typeof body?.reason === 'string' ? body.reason.trim() : ''
const addToBlacklist = body?.addToBlacklist === true
if (songIds.length === 0) {
throw createApiError(400, SERVER_ERROR_CODES.SONG_BATCH_REJECT_IDS_REQUIRED, '歌曲ID列表不能为空')
}
if (songIds.length > MAX_BATCH_REJECT_COUNT) {
throw createApiError(
400,
SERVER_ERROR_CODES.SONG_BATCH_REJECT_LIMIT_EXCEEDED,
`单次最多驳回 ${MAX_BATCH_REJECT_COUNT} 首歌曲`,
{ params: [MAX_BATCH_REJECT_COUNT] }
)
}
if (!reason) {
throw createApiError(400, SERVER_ERROR_CODES.SONG_BATCH_REJECT_REASON_REQUIRED, '驳回原因不能为空')
}
try {
let rejectedSongs: any[] = []
await db.transaction(async (tx) => {
const existingSongs = await tx.select().from(songs).where(inArray(songs.id, songIds))
if (existingSongs.length === 0) {
throw createApiError(404, SERVER_ERROR_CODES.SONG_NOT_FOUND, '歌曲不存在或已被删除')
}
const existingIds = existingSongs.map((song) => song.id)
console.log(`开始批量驳回歌曲,共 ${existingIds.length} 首`)
if (addToBlacklist) {
const blacklistValues = [...new Set(existingSongs.map((song: any) => `${song.title} - ${song.artist}`))]
const existingBlacklist = await tx
.select({ value: songBlacklists.value })
.from(songBlacklists)
.where(and(eq(songBlacklists.type, 'SONG'), inArray(songBlacklists.value, blacklistValues)))
const existingSet = new Set(existingBlacklist.map((item) => item.value))
const valuesToInsert = blacklistValues.filter((value) => !existingSet.has(value))
for (const value of valuesToInsert) {
try {
await tx.insert(songBlacklists).values({
type: 'SONG',
value,
reason: `歌曲驳回: ${reason}`,
createdBy: user.id
})
} catch (error: any) {
console.log(`黑名单添加失败,可能已存在: ${error.message}`)
}
}
console.log(`黑名单添加处理完成`)
}
await tx.delete(votes).where(inArray(votes.songId, existingIds))
console.log(`删除了投票记录`)
await tx.delete(schedules).where(inArray(schedules.songId, existingIds))
console.log(`删除了排期记录`)
const hitRequestIds = [...new Set(existingSongs.map((song: any) => song.hitRequestId).filter(Boolean))]
for (const hitRequestId of hitRequestIds) {
const count = existingSongs.filter((song: any) => song.hitRequestId === hitRequestId).length
try {
await tx
.update(requestTimes)
.set({
accepted: sql`GREATEST(0, accepted - ${count})`
})
.where(eq(requestTimes.id, hitRequestId as any))
} catch (error: any) {
console.error(`减少投稿时段 ${hitRequestId} 接纳数量失败: ${error.message}`)
}
}
rejectedSongs = await tx.delete(songs).where(inArray(songs.id, existingIds)).returning()
console.log(`歌曲批量删除完成`)
})
const songsToNotify = rejectedSongs.filter((song) => song.requesterId)
for (const song of songsToNotify) {
createSongRejectedNotification(
song.requesterId,
{ title: song.title, artist: song.artist },
reason
).catch((error: any) => {
console.error(`发送驳回通知失败: ${error.message}`)
})
}
await cacheService.clearSongsCache()
await cacheService.clearSchedulesCache()
console.log(`批量驳回操作完成,共 ${rejectedSongs.length} 首`)
return {
success: true,
message: '歌曲批量驳回成功',
data: {
rejected: rejectedSongs.length,
missing: songIds.length - rejectedSongs.length,
notificationSent: songsToNotify.length
}
}
} catch (error: any) {
console.error('批量驳回歌曲失败:', error)
if (error.statusCode) {
throw error
}
throw createApiError(500, SERVER_ERROR_CODES.SONG_OPERATION_FAILED, '批量驳回失败: ' + error.message)
}
})