- 新增 /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 接入自动审查逻辑
136 lines
4.9 KiB
TypeScript
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)
|
|
}
|
|
})
|