初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
import { db } from '~/drizzle/db'
import { songs, requestTimes } from '~/drizzle/schema'
import { eq, sql } from 'drizzle-orm'
import { cacheService } from '../../../services/cacheService'
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 || !['approved', 'rejected'].includes(body.status)) {
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
if (body.status === 'rejected') {
// 如果有 hitRequestId,减少对应时段的已接纳数量
if (song.hitRequestId) {
try {
await tx
.update(requestTimes)
.set({
accepted: sql`GREATEST(0, accepted - 1)`
})
.where(eq(requestTimes.id, song.hitRequestId))
} catch (error) {
console.error(`减少投稿时段接纳数量失败: ${error.message}`)
}
}
const note = song.submissionNote ? `[审核未通过] ${body.reason || ''}\n${song.submissionNote}` : `[审核未通过] ${body.reason || ''}`
// 释放排期序号并更新备注
const updatedSong = await tx.update(songs).set({
expectedPlayDate: null,
expectedPlaySequence: null,
hitRequestId: null,
submissionNote: note
}).where(eq(songs.id, body.songId)).returning()
return { success: true, song: updatedSong[0] }
} else {
const note = song.submissionNote ? `[审核已通过]\n${song.submissionNote}` : `[审核已通过]`
const updatedSong = await tx.update(songs).set({
submissionNote: note
}).where(eq(songs.id, body.songId)).returning()
return { success: true, song: updatedSong[0] }
}
})
if (body.status === 'rejected' && songForNotification && songForNotification.requesterId) {
createSongRejectedNotification(
songForNotification.requesterId,
{ title: songForNotification.title, artist: songForNotification.artist },
body.reason?.trim() || '未过审释放排期',
getClientIP(event)
).catch(e => console.error(e))
}
await cacheService.clearSongsCache()
return {
success: true,
message: body.status === 'rejected' ? '已驳回并释放排期' : '审核已通过',
data: result.song
}
} catch (error) {
console.error('审核歌曲失败:', error)
if (error.statusCode) throw error
throw createError({
statusCode: 500,
message: '审核失败: ' + error.message
})
}
})
+112
View File
@@ -0,0 +1,112 @@
import { db } from '~/drizzle/db'
import { schedules, songs, votes, requestTimes } from '~/drizzle/schema'
import { eq, sql } from 'drizzle-orm'
import { cacheService } from '../../../services/cacheService'
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
}
})
// 清除相关缓存
try {
await cacheService.clearSongsCache()
if (result.deletedSchedules) {
await cacheService.clearSchedulesCache()
}
console.log('[Cache] 歌曲和排期缓存已清除(删除歌曲)')
} catch (cacheError) {
console.warn('[Cache] 清除缓存失败:', cacheError)
}
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: '删除歌曲时发生未知错误'
})
}
})
+126
View File
@@ -0,0 +1,126 @@
import { db } from '~/drizzle/db'
import { createSongPlayedNotification } from '~~/server/services/notificationService'
import { CacheService } from '~~/server/services/cacheService'
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'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能标记歌曲'
})
}
// 检查是否是管理员
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw createError({
statusCode: 403,
message: '只有管理员可以标记歌曲为已播放'
})
}
const body = await readBody(event)
let songIds: number[] = []
if (body.songId) {
songIds = [body.songId]
} else if (body.songIds && Array.isArray(body.songIds)) {
songIds = body.songIds
}
if (songIds.length === 0) {
throw createError({
statusCode: 400,
message: '歌曲ID列表不能为空'
})
}
// 检查是否是撤回操作
const isUnmark = body.unmark === true
// 构建更新条件:只更新状态不符合预期的歌曲
// 如果是标记为已播放(!isUnmark),则只更新 played = false 的歌曲
// 如果是撤回(isUnmark),则只更新 played = 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列表
const updatedSongIds = updatedSongsResult.map(s => s.id)
if (updatedSongIds.length > 0) {
// 根据操作类型更新重播申请状态
const targetStatus = !isUnmark ? 'PENDING' : 'FULFILLED'
const newStatus = !isUnmark ? 'FULFILLED' : 'PENDING'
const updatedRequests = await tx
.update(songReplayRequests)
.set({
status: newStatus,
updatedAt: new Date()
})
.where(
and(inArray(songReplayRequests.songId, updatedSongIds), eq(songReplayRequests.status, targetStatus))
)
.returning()
if (updatedRequests.length > 0) {
const logMessage = !isUnmark
? `将 ${updatedRequests.length} 个重播申请状态更新为 FULFILLED(歌曲已播放)`
: `将 ${updatedRequests.length} 个重播申请状态恢复为 PENDING(撤回已播放)`
console.log(logMessage)
}
}
return { updatedSongsResult, updatedSongIds }
})
// 清除歌曲和排期相关缓存
try {
const cacheService = CacheService.getInstance()
await cacheService.clearSongsCache()
await cacheService.clearSchedulesCache()
} catch (error) {
console.error('清除缓存失败:', error)
}
// 如果是标记为已播放,则异步发送通知(不阻塞响应)
if (!isUnmark && updatedSongIds.length > 0) {
// 获取客户端IP地址
const clientIP = getClientIP(event)
// 异步发送通知,不等待完成
setImmediate(() => {
Promise.allSettled(updatedSongIds.map(songId =>
createSongPlayedNotification(songId, clientIP).catch((err) => {
console.error(`发送歌曲(${songId})已播放通知失败:`, err)
})
))
})
}
return {
message: isUnmark ? '歌曲已成功撤回已播放状态' : '歌曲已成功标记为已播放',
count: updatedSongsResult.length,
updatedSongIds: updatedSongIds
}
})
+153
View File
@@ -0,0 +1,153 @@
import { db } from '~/drizzle/db'
import { schedules, songBlacklists, songs, votes, requestTimes } from '~/drizzle/schema'
import { eq, sql } from 'drizzle-orm'
import { cacheService } from '../../../services/cacheService'
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(),
getClientIP(event)
)
.then(() => {
console.log(`已发送驳回通知给用户: ${songForNotification.requesterId}`)
})
.catch((error) => {
console.error(`发送通知失败: ${error.message}`)
})
notificationSent = true
}
// 清除相关缓存
await cacheService.clearSongsCache()
await cacheService.clearSchedulesCache()
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
})
}
})