Files
BYW-VoiceHub/server/api/songs/withdraw.post.ts
T

187 lines
5.9 KiB
TypeScript

import { db } from '~/drizzle/db'
import {
schedules,
songs,
votes,
songCollaborators,
collaborationLogs,
requestTimes
} from '~/drizzle/schema'
import { and, eq, sql } from 'drizzle-orm'
import {
getTimeRange,
isCardCodeLimitBypassActive,
type LimitType
} from '~~/server/utils/submissionLimit'
import { releaseCardCodeAfterSongWithdrawal } from '~~/server/services/cardCodeLifecycleService'
import { getSystemSettingsCached } from '~~/server/utils/system-settings-helper'
import { createApiError } from '~~/server/utils/apiError'
import { getClientIP } from '~~/server/utils/ip-utils'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createApiError(401, 'SONG_LOGIN_REQUIRED_WITHDRAW', '需要登录才能撤回歌曲')
}
const body = await readBody(event)
if (!body.songId) {
throw createApiError(400, 'SONG_ID_REQUIRED', '歌曲ID不能为空')
}
// 查找歌曲
const songResult = await db.select().from(songs).where(eq(songs.id, body.songId)).limit(1)
const song = songResult[0]
if (!song) {
throw createApiError(404, 'SONG_NOT_FOUND', '歌曲不存在')
}
// 检查是否是用户自己的投稿或联合投稿
const isRequester = song.requesterId === user.id
let isCollaborator = false
let collaboratorRecord = null
if (!isRequester) {
const collabResult = await db
.select()
.from(songCollaborators)
.where(and(eq(songCollaborators.songId, song.id), eq(songCollaborators.userId, user.id)))
.limit(1)
if (collabResult.length > 0) {
isCollaborator = true
collaboratorRecord = collabResult[0]
}
}
if (
!isRequester &&
!isCollaborator &&
!['SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes(user.role)
) {
throw createApiError(403, 'SONG_WITHDRAW_OWN_ONLY', '只能撤回自己的投稿或退出联合投稿')
}
// 检查歌曲是否已经播放
if (song.played) {
throw createApiError(400, 'SONG_PLAYED_CANNOT_WITHDRAW', '已播放的歌曲不能撤回')
}
// 检查歌曲是否已排期(只检查已发布的排期,草稿不算)
const scheduleResult = await db
.select()
.from(schedules)
.where(and(eq(schedules.songId, body.songId), eq(schedules.isDraft, false)))
.limit(1)
const schedule = scheduleResult[0]
if (schedule) {
throw createApiError(400, 'SONG_SCHEDULED_CANNOT_WITHDRAW', '已排期的歌曲不能撤回')
}
// 如果是联合投稿人撤回(退出)
if (
isCollaborator &&
!isRequester &&
!['SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes(user.role)
) {
await db.delete(songCollaborators).where(eq(songCollaborators.id, collaboratorRecord.id))
// 记录日志
await db.insert(collaborationLogs).values({
collaboratorId: collaboratorRecord.id,
action: 'LEAVE',
operatorId: user.id,
ipAddress: getClientIP(event)
})
return {
message: '已成功退出联合投稿',
songId: body.songId,
action: 'leave'
}
}
// 如果是主投稿人撤回(删除歌曲)
// 获取系统设置以检查限制类型
const settings = await getSystemSettingsCached()
// 检查撤销的歌曲是否在当前限制期间内(用于返还配额)
let canReturnQuota = false
const cardCodeDidNotUseOrdinaryQuota = isCardCodeLimitBypassActive(settings) && !!song.cardCodeId
if (settings?.enableSubmissionLimit && !cardCodeDidNotUseOrdinaryQuota) {
let activeLimit: { type: LimitType; value: number | null } | null = null
if (settings.dailySubmissionLimit !== null && settings.dailySubmissionLimit !== undefined) {
activeLimit = { type: 'daily', value: settings.dailySubmissionLimit }
} else if (
settings.weeklySubmissionLimit !== null &&
settings.weeklySubmissionLimit !== undefined
) {
activeLimit = { type: 'weekly', value: settings.weeklySubmissionLimit }
} else if (
settings.monthlySubmissionLimit !== null &&
settings.monthlySubmissionLimit !== undefined
) {
activeLimit = { type: 'monthly', value: settings.monthlySubmissionLimit }
}
if (activeLimit?.value && activeLimit.value > 0) {
const { start, end } = getTimeRange(activeLimit.type)
canReturnQuota = song.createdAt >= start && song.createdAt <= end
}
}
// 投稿关联数据一起进入事务,避免撤回失败时只删掉一部分数据。
try {
await db.transaction(async (tx) => {
await tx.delete(songCollaborators).where(eq(songCollaborators.songId, body.songId))
await tx.delete(votes).where(eq(votes.songId, body.songId))
if (song.hitRequestId) {
await tx
.update(requestTimes)
.set({
accepted: sql`GREATEST(0, accepted - 1)`
})
.where(eq(requestTimes.id, song.hitRequestId))
console.log(`已减少投稿时段 ${song.hitRequestId} 的接纳数量`)
}
if (song.cardCodeId) {
const releaseResult = await releaseCardCodeAfterSongWithdrawal(tx, {
songId: song.id,
cardCodeId: song.cardCodeId,
operatorId: user.id
})
if (
!releaseResult.changed &&
['CONCURRENT_CHANGE', 'MISSING_CARD_CODE'].includes(String(releaseResult.reason || ''))
) {
throw createApiError(409, 'SONG_CARD_RELEASE_FAILED', '点歌券释放失败,撤回已终止')
}
}
// 删除歌曲(在事务内)
await tx.delete(songs).where(eq(songs.id, body.songId))
})
} catch (txErr: any) {
console.error('撤回事务失败:', txErr)
throw createError({
statusCode: txErr.statusCode || 500,
message: txErr.message || '撤回歌曲失败,请稍后重试'
})
}
return {
message: canReturnQuota ? '歌曲已成功撤回,投稿配额已返还' : '歌曲已成功撤回',
songId: body.songId,
quotaReturned: canReturnQuota
}
})