- 新增 /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 接入自动审查逻辑
312 lines
9.5 KiB
TypeScript
312 lines
9.5 KiB
TypeScript
import { db } from '~/drizzle/db'
|
||
import {
|
||
songs,
|
||
users,
|
||
songCollaborators,
|
||
collaborationLogs,
|
||
systemSettings,
|
||
songBlacklists
|
||
} from '~/drizzle/schema'
|
||
import { and, eq, inArray, or } from 'drizzle-orm'
|
||
import { cacheService } from '~~/server/services/cacheService'
|
||
import { createSubmissionNoteClearedNotification } from '~~/server/services/notificationService'
|
||
import { moderateSubmissionNote } from '~~/server/utils/submissionNoteAutoModeration'
|
||
|
||
export default defineEventHandler(async (event) => {
|
||
try {
|
||
// 验证请求方法
|
||
if (event.node.req.method !== 'PUT') {
|
||
throw createError({
|
||
statusCode: 405,
|
||
statusMessage: 'Method Not Allowed'
|
||
})
|
||
}
|
||
|
||
// 获取已验证的用户信息(由中间件提供)
|
||
const user = event.context.user
|
||
if (!user) {
|
||
throw createError({
|
||
statusCode: 401,
|
||
statusMessage: '未授权访问'
|
||
})
|
||
}
|
||
|
||
// 检查权限
|
||
if (!['ADMIN', 'SONG_ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||
throw createError({
|
||
statusCode: 403,
|
||
statusMessage: '权限不足'
|
||
})
|
||
}
|
||
|
||
// 获取歌曲ID
|
||
const songId = parseInt(getRouterParam(event, 'id'))
|
||
if (!songId) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: 'Invalid song ID'
|
||
})
|
||
}
|
||
|
||
// 获取请求体
|
||
const body = await readBody(event)
|
||
const { title, artist, requester, semester, musicPlatform, musicId, cover, playUrl } = body
|
||
const ipAddress =
|
||
(event.node.req.headers['x-forwarded-for'] as string) || event.node.req.socket.remoteAddress
|
||
|
||
// 验证必填字段
|
||
if (!title || !artist) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
statusMessage: 'Title and artist are required'
|
||
})
|
||
}
|
||
|
||
const existingSongResult = await db.select().from(songs).where(eq(songs.id, songId)).limit(1)
|
||
const existingSong = existingSongResult[0]
|
||
|
||
if (!existingSong) {
|
||
throw createError({
|
||
statusCode: 404,
|
||
statusMessage: '歌曲不存在'
|
||
})
|
||
}
|
||
|
||
const updateData = {
|
||
title: title.trim(),
|
||
artist: artist.trim(),
|
||
semester: semester || null,
|
||
musicPlatform: musicPlatform || null,
|
||
musicId: musicId || null,
|
||
cover: cover || null,
|
||
playUrl: playUrl || null
|
||
}
|
||
|
||
// 处理投稿人
|
||
if ('requester' in body) {
|
||
const requester = body.requester
|
||
|
||
// 只有在传递有效投稿人信息时才更新
|
||
if (requester && requester !== '' && requester !== 0) {
|
||
// 查找投稿人用户
|
||
const conditions = []
|
||
if (typeof requester === 'number') {
|
||
conditions.push(eq(users.id, requester))
|
||
}
|
||
if (typeof requester === 'string') {
|
||
conditions.push(eq(users.username, requester))
|
||
conditions.push(eq(users.name, requester))
|
||
}
|
||
|
||
const requesterUser = await db
|
||
.select()
|
||
.from(users)
|
||
.where(or(...conditions))
|
||
.limit(1)
|
||
|
||
if (requesterUser.length === 0) {
|
||
throw createError({
|
||
statusCode: 404,
|
||
statusMessage: '投稿人用户不存在'
|
||
})
|
||
}
|
||
|
||
// 设置投稿人
|
||
updateData.requesterId = requesterUser[0].id
|
||
}
|
||
// 如果投稿人为空值,则跳过处理,保持原有投稿人不变
|
||
}
|
||
|
||
const shouldClearSubmissionNote = body.clearSubmissionNote === true
|
||
const submissionNoteClearReason =
|
||
typeof body.submissionNoteClearReason === 'string' ? body.submissionNoteClearReason.trim() : ''
|
||
const shouldNotifySubmissionNoteClear = body.notifyOnSubmissionNoteClear === true
|
||
|
||
if (shouldClearSubmissionNote) {
|
||
updateData.submissionNote = null
|
||
} else if ('submissionNote' in body) {
|
||
updateData.submissionNote =
|
||
typeof body.submissionNote === 'string' && body.submissionNote.trim()
|
||
? body.submissionNote.trim()
|
||
: null
|
||
}
|
||
if ('submissionNotePublic' in body) {
|
||
const wantPublic = updateData.submissionNote ? body.submissionNotePublic !== false : false
|
||
updateData.submissionNotePublic = wantPublic
|
||
|
||
// 自动审查(若 systemSettings 打开 submissionNoteRequiresApproval)
|
||
try {
|
||
const sysSetRes = await db.select().from(systemSettings).limit(1)
|
||
const sysSet = sysSetRes[0]
|
||
if (sysSet?.submissionNoteRequiresApproval && wantPublic && updateData.submissionNote) {
|
||
const bl = await db
|
||
.select()
|
||
.from(songBlacklists)
|
||
.where(inArray(songBlacklists.type, ['KEYWORD', 'LANGUAGE', 'GENRE']))
|
||
const mod = moderateSubmissionNote(
|
||
updateData.submissionNote,
|
||
wantPublic,
|
||
true,
|
||
{
|
||
title: updateData.title || existingSong.title,
|
||
artist: updateData.artist || existingSong.artist
|
||
},
|
||
bl
|
||
)
|
||
updateData.submissionNotePublic = mod.notePublic
|
||
updateData.submissionNotePublicStatus = mod.publicStatus
|
||
} else {
|
||
updateData.submissionNotePublicStatus = null
|
||
}
|
||
} catch (err) {
|
||
console.warn('[Update API] 投稿留言自动审查失败,保持用户设置:', err)
|
||
}
|
||
}
|
||
|
||
const currentRequesterId = updateData.requesterId || existingSong.requesterId
|
||
|
||
// 更新歌曲
|
||
const updatedSongResult = await db
|
||
.update(songs)
|
||
.set(updateData)
|
||
.where(eq(songs.id, songId))
|
||
.returning()
|
||
|
||
// 处理联合投稿人
|
||
if ('collaborators' in body && Array.isArray(body.collaborators)) {
|
||
// 获取现有联合投稿人
|
||
const existingCollaborators = await db
|
||
.select()
|
||
.from(songCollaborators)
|
||
.where(eq(songCollaborators.songId, songId))
|
||
|
||
const existingCollaboratorUserIds = existingCollaborators.map((c) => c.userId)
|
||
const newCollaboratorIds = [
|
||
...new Set(
|
||
body.collaborators
|
||
.map((id: number | string) => Number(id))
|
||
.filter((id: number) => Number.isInteger(id) && id > 0 && id !== currentRequesterId)
|
||
)
|
||
]
|
||
|
||
// 需要添加的
|
||
const toAdd = newCollaboratorIds.filter(
|
||
(id: number) => !existingCollaboratorUserIds.includes(id)
|
||
)
|
||
// 需要删除的
|
||
const toDelete = existingCollaborators.filter((c) => !newCollaboratorIds.includes(c.userId))
|
||
|
||
// 执行添加
|
||
for (const userId of toAdd) {
|
||
// 检查用户是否存在
|
||
const userExists = await db.select().from(users).where(eq(users.id, userId)).limit(1)
|
||
if (userExists.length > 0) {
|
||
const [newCollab] = await db
|
||
.insert(songCollaborators)
|
||
.values({
|
||
songId: songId,
|
||
userId: userId,
|
||
status: 'ACCEPTED'
|
||
})
|
||
.returning()
|
||
|
||
// 记录日志
|
||
await db.insert(collaborationLogs).values({
|
||
collaboratorId: newCollab.id,
|
||
action: 'ADMIN_ADD',
|
||
operatorId: user.id,
|
||
ipAddress
|
||
})
|
||
}
|
||
}
|
||
|
||
// 执行删除
|
||
for (const collab of toDelete) {
|
||
await db.delete(songCollaborators).where(eq(songCollaborators.id, collab.id))
|
||
|
||
// 记录日志
|
||
await db.insert(collaborationLogs).values({
|
||
collaboratorId: collab.id,
|
||
action: 'ADMIN_REMOVE',
|
||
operatorId: user.id,
|
||
ipAddress
|
||
})
|
||
}
|
||
}
|
||
|
||
if (shouldClearSubmissionNote && existingSong.submissionNote && shouldNotifySubmissionNoteClear) {
|
||
const latestCollaborators = await db
|
||
.select({
|
||
userId: songCollaborators.userId
|
||
})
|
||
.from(songCollaborators)
|
||
.where(eq(songCollaborators.songId, songId))
|
||
|
||
const notifyUserIds = [
|
||
currentRequesterId,
|
||
...latestCollaborators.map((collaborator) => collaborator.userId)
|
||
].filter((userId): userId is number => Number.isInteger(userId) && userId > 0)
|
||
|
||
createSubmissionNoteClearedNotification(
|
||
notifyUserIds,
|
||
{ title: updatedSongResult[0].title, artist: updatedSongResult[0].artist },
|
||
submissionNoteClearReason,
|
||
ipAddress
|
||
).catch((error) => {
|
||
console.error('发送歌曲备注清空通知失败:', error)
|
||
})
|
||
}
|
||
|
||
// 获取完整的歌曲信息(包含投稿人)
|
||
const updatedSong = await db
|
||
.select({
|
||
id: songs.id,
|
||
title: songs.title,
|
||
artist: songs.artist,
|
||
semester: songs.semester,
|
||
musicPlatform: songs.musicPlatform,
|
||
musicId: songs.musicId,
|
||
cover: songs.cover,
|
||
playUrl: songs.playUrl,
|
||
submissionNote: songs.submissionNote,
|
||
submissionNotePublic: songs.submissionNotePublic,
|
||
requesterId: songs.requesterId,
|
||
createdAt: songs.createdAt,
|
||
updatedAt: songs.updatedAt,
|
||
requester: {
|
||
id: users.id,
|
||
username: users.username,
|
||
name: users.name
|
||
}
|
||
})
|
||
.from(songs)
|
||
.leftJoin(users, eq(songs.requesterId, users.id))
|
||
.where(eq(songs.id, songId))
|
||
.limit(1)
|
||
|
||
// 清除歌曲相关缓存
|
||
try {
|
||
await cacheService.clearSongsCache()
|
||
console.log('[Cache] 歌曲缓存已清除(更新歌曲)')
|
||
} catch (cacheError) {
|
||
console.error('[Cache] 清除歌曲缓存失败:', cacheError)
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
song: updatedSong[0]
|
||
}
|
||
} catch (error) {
|
||
console.error('Update song error:', error)
|
||
|
||
if (error.statusCode) {
|
||
throw error
|
||
}
|
||
|
||
throw createError({
|
||
statusCode: 500,
|
||
statusMessage: error.message || 'Internal server error'
|
||
})
|
||
}
|
||
})
|