187 lines
5.9 KiB
TypeScript
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
|
|
}
|
|
})
|