feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { schedules, songs, votes, requestTimes } from '~/drizzle/schema'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
|
||||
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)
|
||||
|
||||
if (!body.songId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '歌曲ID不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用事务确保删除操作的原子性
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// 在事务中重新检查歌曲是否存在
|
||||
const songResult = await tx.select().from(songs).where(eq(songs.id, body.songId)).limit(1)
|
||||
const song = songResult[0]
|
||||
|
||||
if (!song) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: '歌曲不存在或已被删除'
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`开始删除歌曲: ${song.title} (ID: ${body.songId})`)
|
||||
|
||||
// 删除歌曲的所有投票
|
||||
const deletedVotes = await tx.delete(votes).where(eq(votes.songId, body.songId))
|
||||
console.log(`删除了投票记录`)
|
||||
|
||||
// 删除歌曲的所有排期
|
||||
const deletedSchedules = await tx.delete(schedules).where(eq(schedules.songId, body.songId))
|
||||
console.log(`删除了排期记录`)
|
||||
|
||||
// 如果有 hitRequestId,减少对应时段的已接纳数量
|
||||
if (song.hitRequestId) {
|
||||
try {
|
||||
await tx
|
||||
.update(requestTimes)
|
||||
.set({
|
||||
accepted: sql`GREATEST(0, accepted - 1)`
|
||||
})
|
||||
.where(eq(requestTimes.id, song.hitRequestId))
|
||||
console.log(`已减少投稿时段 ${song.hitRequestId} 的接纳数量`)
|
||||
} catch (error) {
|
||||
console.error(`减少投稿时段接纳数量失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除歌曲
|
||||
const deletedSong = await tx.delete(songs).where(eq(songs.id, body.songId)).returning()
|
||||
const deletedSongData = deletedSong[0]
|
||||
|
||||
console.log(`成功删除歌曲: ${deletedSongData?.title}`)
|
||||
|
||||
return {
|
||||
message: '歌曲已成功删除',
|
||||
songId: body.songId,
|
||||
deletedVotes: true,
|
||||
deletedSchedules: true
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error('删除歌曲时发生错误:', error)
|
||||
|
||||
// 如果是已经格式化的错误,直接抛出
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 处理Prisma特定错误
|
||||
if (error.code === 'P2025') {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: '歌曲不存在或已被删除'
|
||||
})
|
||||
}
|
||||
|
||||
// 其他未知错误
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '删除歌曲时发生未知错误'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,291 @@
|
||||
import { readBody } from 'h3'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { songs } from '~/drizzle/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { requireSongAdmin } from '~~/server/utils/requireSongAdmin'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
import {
|
||||
createTxSongDetailBody,
|
||||
normalizeTxMusicId,
|
||||
txHeaders
|
||||
} from '~~/server/utils/native_tx'
|
||||
import { fetchSongDuration } from '~~/server/utils/songDurationFetcher'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
requireSongAdmin(event)
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
// 新增歌曲场景:未传 songId 时按平台+音乐ID获取时长,仅返回不写入
|
||||
if (body?.songId == null) {
|
||||
const platform = body?.platform
|
||||
const musicId = body?.musicId
|
||||
if (!platform || !musicId) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.SONG_DURATION_PLATFORM_REQUIRED,
|
||||
'缺少平台或音乐 ID 信息,无法获取时长'
|
||||
)
|
||||
}
|
||||
const durationSeconds = await fetchSongDuration(String(platform), String(musicId))
|
||||
if (durationSeconds == null) {
|
||||
return { success: false, songId: null, durationSeconds: null, message: '未获取到有效时长' }
|
||||
}
|
||||
return { success: true, songId: null, durationSeconds, message: '时长获取成功' }
|
||||
}
|
||||
|
||||
const songId = body.songId == null ? null : Number(body.songId)
|
||||
|
||||
if (!Number.isFinite(songId) || songId < 1) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.SONG_INVALID_ID, '歌曲 ID 无效')
|
||||
}
|
||||
|
||||
const song = await db.select().from(songs).where(eq(songs.id, songId)).limit(1)
|
||||
if (song.length === 0) {
|
||||
throw createApiError(404, SERVER_ERROR_CODES.SONG_NOT_FOUND, '歌曲不存在')
|
||||
}
|
||||
|
||||
const s = song[0]
|
||||
|
||||
// 浏览器读取或管理员手动输入时,只由服务端负责校验并保存结果。
|
||||
if (Object.prototype.hasOwnProperty.call(body || {}, 'durationSeconds')) {
|
||||
if (body.durationSeconds === null) {
|
||||
await db.update(songs).set({ durationSeconds: null }).where(eq(songs.id, songId))
|
||||
return { success: true, songId, durationSeconds: null, message: '时长已清空' }
|
||||
}
|
||||
|
||||
const durationSeconds = Number(body.durationSeconds)
|
||||
if (!Number.isInteger(durationSeconds) || durationSeconds < 0 || durationSeconds > 7200) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
|
||||
'时长需在 0 秒至 2 小时之间'
|
||||
)
|
||||
}
|
||||
|
||||
if (s.durationSeconds !== durationSeconds) {
|
||||
await db.update(songs).set({ durationSeconds }).where(eq(songs.id, songId))
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
songId,
|
||||
durationSeconds,
|
||||
message: s.durationSeconds === durationSeconds ? '时长未变化' : '时长更新成功'
|
||||
}
|
||||
}
|
||||
|
||||
const platform = s.musicPlatform
|
||||
const musicId = s.musicId
|
||||
|
||||
if (!platform || !musicId) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.SONG_DURATION_PLATFORM_REQUIRED,
|
||||
'歌曲缺少平台或音乐 ID 信息,无法获取时长'
|
||||
)
|
||||
}
|
||||
|
||||
let durationSeconds: number | null = null
|
||||
|
||||
try {
|
||||
if (platform === 'netease' || platform === 'netease-podcast') {
|
||||
const response: any = await $fetch('/api/api-enhanced/netease/song/url/v1', {
|
||||
params: { id: musicId, level: 'standard' },
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
if (response?.code === 200 && Array.isArray(response.data) && response.data.length > 0) {
|
||||
const timeMs = response.data[0]?.time
|
||||
if (typeof timeMs === 'number' && timeMs > 0) {
|
||||
durationSeconds = Math.floor(timeMs / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
if (!durationSeconds) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: '网易云 API 未返回有效时长'
|
||||
}
|
||||
}
|
||||
} else if (platform === 'tencent') {
|
||||
const normalized = normalizeTxMusicId(musicId)
|
||||
const result: any = await $fetch(
|
||||
'https://u.y.qq.com/cgi-bin/musicu.fcg',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: txHeaders,
|
||||
body: createTxSongDetailBody(normalized),
|
||||
responseType: 'json',
|
||||
timeout: 10000
|
||||
}
|
||||
)
|
||||
|
||||
if (result?.code === 0 && result?.req?.code === 0) {
|
||||
const interval = result.req?.data?.track_info?.interval
|
||||
if (typeof interval === 'number' && interval > 0) {
|
||||
durationSeconds = Math.floor(interval)
|
||||
}
|
||||
}
|
||||
|
||||
if (!durationSeconds) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: 'QQ 音乐详情接口未返回有效时长'
|
||||
}
|
||||
}
|
||||
} else if (platform === 'migu') {
|
||||
const songInfo: any = await $fetch(
|
||||
`https://app.c.nf.migu.cn/resource/song/by-contentids/v2.0?contentId=${encodeURIComponent(musicId)}`,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
if (songInfo?.code?.toString() === '000000' && songInfo?.data?.length) {
|
||||
const duration = songInfo.data[0]?.duration
|
||||
if (typeof duration === 'number' && duration > 0) {
|
||||
durationSeconds = Math.floor(duration)
|
||||
}
|
||||
}
|
||||
|
||||
if (!durationSeconds) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: '咪咕歌曲信息接口未返回有效时长'
|
||||
}
|
||||
}
|
||||
} else if (platform === 'bilibili') {
|
||||
// musicId 格式为 bvid:cid 或 bvid:cid:page
|
||||
const parts = musicId.split(':')
|
||||
const bvid = parts[0]
|
||||
const page = parts.length > 2 ? Number(parts[2]) : 1
|
||||
if (parts.length > 2 && (!Number.isInteger(page) || page < 1)) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: `哔哩哔哩视频 ID 格式无效:${musicId}`
|
||||
}
|
||||
}
|
||||
const viewResp: any = await $fetch(
|
||||
'https://api.bilibili.com/x/web-interface/view',
|
||||
{
|
||||
params: { bvid },
|
||||
headers: {
|
||||
Cookie: 'buvid3=0',
|
||||
Referer: 'https://www.bilibili.com/'
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
)
|
||||
|
||||
if (viewResp?.code === 0 && viewResp?.data?.pages?.length) {
|
||||
const pageIdx = page > 1 ? Math.max(0, page - 1) : 0
|
||||
const duration = viewResp.data.pages[pageIdx]?.duration
|
||||
if (typeof duration === 'number' && duration > 0) {
|
||||
durationSeconds = Math.floor(duration)
|
||||
}
|
||||
}
|
||||
|
||||
if (!durationSeconds) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: '哔哩哔哩视频详情接口未返回有效时长'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: `暂不支持的平台:${platform}`
|
||||
}
|
||||
}
|
||||
|
||||
// 网易云固定返回 30 秒时通常是试听片段,不作为歌曲完整时长。
|
||||
if ((platform === 'netease' || platform === 'netease-podcast') && durationSeconds === 30) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: '网易云返回试听片段时长,未写入'
|
||||
}
|
||||
}
|
||||
|
||||
// 校验时长范围(合理的歌曲时长:30秒~1小时)
|
||||
if (durationSeconds < 30 || durationSeconds > 3600) {
|
||||
console.warn(`[时长刷新] 歌曲 #${songId} 时长异常:${durationSeconds}s,跳过写入`)
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: `获取到的时长 ${durationSeconds}s 超出合理范围(30秒~1小时),未写入`
|
||||
}
|
||||
}
|
||||
|
||||
// 避免无变化写入
|
||||
if (s.durationSeconds === durationSeconds) {
|
||||
return {
|
||||
success: true,
|
||||
songId,
|
||||
durationSeconds,
|
||||
message: '时长未变化'
|
||||
}
|
||||
}
|
||||
|
||||
// 写入数据库
|
||||
await db.update(songs).set({ durationSeconds }).where(eq(songs.id, songId))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
songId,
|
||||
durationSeconds,
|
||||
message: '时长获取成功'
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err?.message || '未知错误'
|
||||
const platformName = platform === 'netease' || platform === 'netease-podcast' ? '网易云' : platform === 'tencent' ? 'QQ音乐' : platform === 'migu' ? '咪咕' : platform === 'bilibili' ? 'Bilibili' : platform
|
||||
const statusCode = err.statusCode || err.status || (err.response?.status as number | undefined)
|
||||
// AbortError:请求超时或批量刷新中止
|
||||
if (err.name === 'AbortError' || err.name === 'TimeoutError') {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: `${platformName} 响应超时`
|
||||
}
|
||||
}
|
||||
// 404:平台限流或歌曲不存在
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: `${platformName} 未找到该歌曲`
|
||||
}
|
||||
}
|
||||
if (statusCode === 403 || statusCode === 429) {
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: `${platformName} 接口被限制,请稍后重试`
|
||||
}
|
||||
}
|
||||
console.error(`[时长刷新] 歌曲 #${songId} 获取时长失败 (${platformName}):`, errorMsg)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
songId,
|
||||
durationSeconds: null,
|
||||
message: `${platformName} 接口异常,请稍后重试`
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,136 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { createSongPlayedNotification } from '~~/server/services/notificationService'
|
||||
import { songs, songReplayRequests } from '~/drizzle/schema'
|
||||
import { eq, and, inArray } from 'drizzle-orm'
|
||||
import { getBeijingTime } from '~/utils/timeUtils'
|
||||
import { getClientIP } from '~~/server/utils/ip-utils'
|
||||
import { restoreReplayRequestsToPending } from '~~/server/utils/scheduleReplayBinding'
|
||||
import { z } from 'zod'
|
||||
|
||||
const markPlayedSchema = z.object({
|
||||
songId: z.number().optional(),
|
||||
songIds: z.array(z.number()).max(100, '批量操作单次最多支持 100 首歌曲').optional(),
|
||||
unmark: z.boolean().optional()
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 检查用户认证
|
||||
const user = event.context.user
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: '需要登录才能标记歌曲'
|
||||
})
|
||||
}
|
||||
|
||||
// 检查是否是管理员
|
||||
if (!['ADMIN', 'SUPER_ADMIN', 'SONG_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '只有管理员可以标记歌曲为已播放'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const validatedDataResult = markPlayedSchema.safeParse(body)
|
||||
|
||||
if (!validatedDataResult.success) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: validatedDataResult.error.issues[0]?.message || '请求参数无效'
|
||||
})
|
||||
}
|
||||
|
||||
const validatedData = validatedDataResult.data
|
||||
|
||||
const songIds = Array.from(
|
||||
new Set([
|
||||
...(validatedData.songId !== undefined ? [validatedData.songId] : []),
|
||||
...(validatedData.songIds || [])
|
||||
])
|
||||
)
|
||||
|
||||
if (songIds.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '歌曲ID列表不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
// 撤回已播放
|
||||
const isUnmark = validatedData.unmark === true
|
||||
|
||||
// 过滤需要更新的歌曲
|
||||
const condition = !isUnmark
|
||||
? and(inArray(songs.id, songIds), eq(songs.played, false))
|
||||
: and(inArray(songs.id, songIds), eq(songs.played, true))
|
||||
|
||||
const { updatedSongsResult, updatedSongIds } = await db.transaction(async (tx) => {
|
||||
const updatedSongsResult = await tx
|
||||
.update(songs)
|
||||
.set({
|
||||
played: !isUnmark,
|
||||
playedAt: isUnmark ? null : getBeijingTime()
|
||||
})
|
||||
.where(condition)
|
||||
.returning({ id: songs.id })
|
||||
|
||||
const updatedSongIds = updatedSongsResult.map((s) => s.id)
|
||||
|
||||
if (updatedSongIds.length > 0) {
|
||||
if (!isUnmark) {
|
||||
// 标记已播放:履行全部待处理的重播申请
|
||||
const updatedRequests = await tx
|
||||
.update(songReplayRequests)
|
||||
.set({
|
||||
status: 'FULFILLED',
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
inArray(songReplayRequests.songId, updatedSongIds),
|
||||
eq(songReplayRequests.status, 'PENDING')
|
||||
)
|
||||
)
|
||||
.returning({ id: songReplayRequests.id })
|
||||
|
||||
if (updatedRequests.length > 0) {
|
||||
console.log(`将 ${updatedRequests.length} 个重播申请状态更新为 FULFILLED(歌曲已播放)`)
|
||||
}
|
||||
} else {
|
||||
// 撤回已播放:每人仅恢复最新一条,避免违反部分唯一索引
|
||||
const restoredCount = await restoreReplayRequestsToPending({
|
||||
tx,
|
||||
songIds: updatedSongIds,
|
||||
at: new Date()
|
||||
})
|
||||
|
||||
if (restoredCount > 0) {
|
||||
console.log(`将 ${restoredCount} 个重播申请状态恢复为 PENDING(撤回已播放)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { updatedSongsResult, updatedSongIds }
|
||||
})
|
||||
|
||||
// 异步发送通知
|
||||
if (!isUnmark && updatedSongIds.length > 0) {
|
||||
event.waitUntil(
|
||||
Promise.allSettled(
|
||||
updatedSongIds.map((songId) =>
|
||||
createSongPlayedNotification(songId).catch((err) => {
|
||||
console.error(`发送歌曲(${songId})已播放通知失败:`, err)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
message: isUnmark ? '歌曲已成功撤回已播放状态' : '歌曲已成功标记为已播放',
|
||||
count: updatedSongsResult.length,
|
||||
updatedSongIds: updatedSongIds
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,147 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { schedules, songBlacklists, songs, votes, requestTimes } from '~/drizzle/schema'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { createSongRejectedNotification } from '../../../services/notificationService'
|
||||
|
||||
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)
|
||||
|
||||
if (!body.songId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '歌曲ID不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
if (!body.reason || !body.reason.trim()) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '驳回原因不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
let songForNotification = null
|
||||
|
||||
// 使用事务确保驳回操作的原子性
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// 在事务中重新检查歌曲是否存在
|
||||
const songResult = await tx.select().from(songs).where(eq(songs.id, body.songId)).limit(1)
|
||||
const song = songResult[0]
|
||||
|
||||
if (!song) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: '歌曲不存在或已被删除'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存歌曲信息用于后续发送通知
|
||||
songForNotification = song
|
||||
|
||||
console.log(`开始驳回歌曲: ${song.title} (ID: ${body.songId})`)
|
||||
|
||||
// 如果选择加入黑名单,先添加到黑名单
|
||||
if (body.addToBlacklist) {
|
||||
try {
|
||||
await tx.insert(songBlacklists).values({
|
||||
type: 'SONG',
|
||||
value: `${song.title} - ${song.artist}`,
|
||||
reason: `歌曲驳回: ${body.reason.trim()}`,
|
||||
createdBy: user.id
|
||||
})
|
||||
console.log(`歌曲已添加到黑名单`)
|
||||
} catch (error) {
|
||||
// 如果黑名单中已存在相同歌曲,忽略错误继续执行
|
||||
console.log(`黑名单添加失败,可能已存在: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除歌曲的所有投票
|
||||
await tx.delete(votes).where(eq(votes.songId, body.songId))
|
||||
console.log(`删除了投票记录`)
|
||||
|
||||
// 删除歌曲的所有排期
|
||||
await tx.delete(schedules).where(eq(schedules.songId, body.songId))
|
||||
console.log(`删除了排期记录`)
|
||||
|
||||
// 如果有 hitRequestId,减少对应时段的已接纳数量
|
||||
if (song.hitRequestId) {
|
||||
try {
|
||||
await tx
|
||||
.update(requestTimes)
|
||||
.set({
|
||||
accepted: sql`GREATEST(0, accepted - 1)`
|
||||
})
|
||||
.where(eq(requestTimes.id, song.hitRequestId))
|
||||
console.log(`已减少投稿时段 ${song.hitRequestId} 的接纳数量`)
|
||||
} catch (error) {
|
||||
console.error(`减少投稿时段接纳数量失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除歌曲
|
||||
const deletedSong = await tx.delete(songs).where(eq(songs.id, body.songId)).returning()
|
||||
console.log(`歌曲删除完成`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedSong: deletedSong[0],
|
||||
addedToBlacklist: body.addToBlacklist
|
||||
}
|
||||
})
|
||||
|
||||
// 事务提交后,发送通知
|
||||
// 这样避免邮件发送等耗时操作阻塞数据库事务连接
|
||||
let notificationSent = false
|
||||
if (songForNotification && songForNotification.requesterId) {
|
||||
// 异步发送通知,不阻塞响应
|
||||
createSongRejectedNotification(
|
||||
songForNotification.requesterId,
|
||||
{ title: songForNotification.title, artist: songForNotification.artist },
|
||||
body.reason.trim()
|
||||
)
|
||||
.then(() => {
|
||||
console.log(`已发送驳回通知给用户: ${songForNotification.requesterId}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`发送通知失败: ${error.message}`)
|
||||
})
|
||||
|
||||
notificationSent = true
|
||||
}
|
||||
|
||||
console.log(`歌曲驳回操作完成: ${body.songId}`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '歌曲驳回成功',
|
||||
data: {
|
||||
...result,
|
||||
notificationSent
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('驳回歌曲失败:', error)
|
||||
|
||||
// 如果是我们抛出的错误,直接重新抛出
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 其他错误
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '驳回歌曲失败: ' + error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user