feat: 迁移自定义功能至 1.5.9.9 版本

包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
2026-09-12 19:26:42 +08:00
parent 34f0679117
commit 1106aeea6c
161 changed files with 18963 additions and 27900 deletions
+206 -109
View File
@@ -1,10 +1,10 @@
import { db } from '~/drizzle/db'
import { schedules, songs } from '~/drizzle/schema'
import { and, eq, ne } from 'drizzle-orm'
import { createSongSelectedNotification, createReplaySongSelectedNotification } from '~~/server/services/notificationService'
import { redeemCardCodeForSchedule } from '~~/server/services/cardCodeLifecycleService'
import { fulfillReplayRequestsForSchedule } from '~~/server/utils/scheduleReplayBinding'
import { playTimes, schedules, songs, users, votes, songReplayRequests } from '~/drizzle/schema'
import { and, asc, count, eq } from 'drizzle-orm'
import { createSongSelectedNotification } from '~~/server/services/notificationService'
import { cacheService } from '~~/server/services/cacheService'
import { getBeijingTimestamp } from '~/utils/timeUtils'
import { getClientIP } from '~~/server/utils/ip-utils'
export default defineEventHandler(async (event) => {
// 检查用户认证和权限
@@ -44,13 +44,11 @@ export default defineEventHandler(async (event) => {
playTimeId: schedules.playTimeId,
isDraft: schedules.isDraft,
publishedAt: schedules.publishedAt,
replayRequestId: schedules.replayRequestId,
song: {
id: songs.id,
title: songs.title,
artist: songs.artist,
requesterId: songs.requesterId,
cardCodeId: songs.cardCodeId
requesterId: songs.requesterId
}
})
.from(schedules)
@@ -72,120 +70,219 @@ export default defineEventHandler(async (event) => {
})
}
// 使用事务包装更新排期和重播申请状态操作,保证原子性
const publishResult = await db.transaction(async (tx) => {
// 更新草稿为已发布状态
const publishedAt = new Date(getBeijingTimestamp())
// 更新草稿为已发布状态
const publishedAt = new Date(getBeijingTimestamp())
const publishResult = await tx
.update(schedules)
.set({
isDraft: false,
publishedAt: publishedAt,
updatedAt: publishedAt
const publishResult = await db
.update(schedules)
.set({
isDraft: false,
publishedAt: publishedAt,
updatedAt: publishedAt
})
.where(eq(schedules.id, body.scheduleId))
.returning()
const publishedSchedule = publishResult[0]
if (!publishedSchedule) {
throw createError({
statusCode: 500,
message: '发布排期失败'
})
}
// 获取客户端IP地址
const clientIP = getClientIP(event)
// 发布后发送通知(这是与草稿保存的主要区别)
await createSongSelectedNotification(
draft.song.requesterId,
draft.song.id,
{
title: draft.song.title,
artist: draft.song.artist,
playDate: draft.playDate
},
clientIP
)
// 将该歌曲的所有待处理重播申请标记为已完成
const updatedRequests = await db
.update(songReplayRequests)
.set({
status: 'FULFILLED',
updatedAt: new Date()
})
.where(
and(eq(songReplayRequests.songId, draft.song.id), eq(songReplayRequests.status, 'PENDING'))
)
.returning()
if (updatedRequests.length > 0) {
console.log(`发布排期:将 ${updatedRequests.length} 个重播申请标记为 FULFILLED`)
}
// 清除相关缓存
try {
await cacheService.clearSchedulesCache()
await cacheService.clearSongsCache()
console.log('[Cache] 排期缓存和歌曲列表缓存已清除(发布排期)')
} catch (cacheError) {
console.error('[Cache] 清除缓存失败:', cacheError)
}
// 重新缓存完整的排期列表
try {
// 获取所有已发布的排期数据
const schedulesData = await db
.select({
id: schedules.id,
playDate: schedules.playDate,
sequence: schedules.sequence,
played: schedules.played,
playTimeId: schedules.playTimeId,
isDraft: schedules.isDraft,
publishedAt: schedules.publishedAt,
songId: schedules.songId,
songTitle: songs.title,
songArtist: songs.artist,
songRequesterId: songs.requesterId,
songPlayed: songs.played,
songCover: songs.cover,
songMusicPlatform: songs.musicPlatform,
songMusicId: songs.musicId,
songSemester: songs.semester,
requesterName: users.name,
requesterGrade: users.grade,
requesterClass: users.class,
playTimeName: playTimes.name,
playTimeStartTime: playTimes.startTime,
playTimeEndTime: playTimes.endTime,
playTimeEnabled: playTimes.enabled
})
.where(and(eq(schedules.id, body.scheduleId), eq(schedules.isDraft, true)))
.returning()
const schedule = publishResult[0]
if (!schedule) {
throw createError({ statusCode: 409, message: '排期已发布或状态已变化,请刷新后重试' })
}
// 检查该歌曲是否已经有其他已发布的排期
const existingPublished = await tx
.select({ id: schedules.id })
.from(schedules)
.where(
and(
eq(schedules.songId, draft.song.id),
eq(schedules.isDraft, false),
ne(schedules.id, body.scheduleId)
)
)
.limit(1)
.innerJoin(songs, eq(schedules.songId, songs.id))
.innerJoin(users, eq(songs.requesterId, users.id))
.leftJoin(playTimes, eq(schedules.playTimeId, playTimes.id))
.where(eq(schedules.isDraft, false)) // 只缓存已发布的排期
.orderBy(asc(schedules.playDate))
const shouldNotify = existingPublished.length === 0
// 履行该歌曲全部待处理重播申请,优先绑定草稿指定的申请
let replayRequesterIds: number[] = []
const replayBinding = await fulfillReplayRequestsForSchedule({
tx,
songId: draft.song.id,
scheduleId: body.scheduleId,
at: publishedAt,
preferredRequestId: draft.replayRequestId || undefined,
fallbackRequestId: draft.replayRequestId || undefined
})
if (replayBinding) {
replayRequesterIds = replayBinding.replayRequesterIds
console.log(`发布排期:重播申请 #${replayBinding.replayRequestId} 绑定到排期 #${body.scheduleId}`)
}
if (existingPublished.length > 0) {
console.log(`歌曲 ${draft.song.id} 已有其他正式排期,不再重复发送通知`)
}
await redeemCardCodeForSchedule(tx, {
songId: draft.song.id,
cardCodeId: draft.song.cardCodeId,
operatorId: user.id,
at: publishedAt
})
return { schedule, shouldNotify, replayRequesterIds, replayRequestId: replayBinding?.replayRequestId ?? null }
})
// 发送重播申请已安排通知
if (publishResult.replayRequesterIds?.length > 0) {
for (const replayUserId of publishResult.replayRequesterIds) {
try {
await createReplaySongSelectedNotification(replayUserId, draft.song.id, {
title: draft.song.title,
artist: draft.song.artist,
playDate: publishResult.schedule.playDate
}, publishResult.schedule.id)
} catch (error) {
console.error(`发送重播安排通知给用户 ${replayUserId} 失败:`, error)
}
}
}
let notificationSent = true
if (publishResult.shouldNotify) {
try {
await createSongSelectedNotification(draft.song.requesterId, draft.song.id, {
title: draft.song.title,
artist: draft.song.artist,
playDate: publishResult.schedule.playDate
// 获取所有用户的姓名列表,用于检测同名用户
const allUsers = await db
.select({
id: users.id,
name: users.name,
grade: users.grade,
class: users.class
})
} catch (notificationError) {
notificationSent = false
console.error('排期已发布,但发送歌曲入选通知失败:', notificationError)
}
}
.from(users)
const publishedSchedule = publishResult.schedule
// 创建姓名到用户数组的映射
const nameToUsers = new Map()
allUsers.forEach((user) => {
if (user.name) {
if (!nameToUsers.has(user.name)) {
nameToUsers.set(user.name, [])
}
nameToUsers.get(user.name).push(user)
}
})
// 获取每首歌的投票数
const songIds = schedulesData.map((s) => s.songId)
const voteCounts = await Promise.all(
songIds.map(async (songId) => {
const voteCountResult = await db
.select({ count: count() })
.from(votes)
.where(eq(votes.songId, songId))
return { songId, count: voteCountResult[0].count }
})
)
const voteCountMap = new Map(voteCounts.map((v) => [v.songId, v.count]))
// 转换数据格式
const formattedSchedules = schedulesData.map((schedule) => {
const dateOnly = schedule.playDate
// 处理投稿人姓名,如果是同名用户则添加后缀
let requesterName = schedule.requesterName || '未知用户'
const sameNameUsers = nameToUsers.get(requesterName)
if (sameNameUsers && sameNameUsers.length > 1) {
if (schedule.requesterGrade) {
const sameGradeUsers = sameNameUsers.filter(
(u: {
id: number
name: string | null
grade: string | null
class: string | null
}) => u.grade === schedule.requesterGrade
)
if (sameGradeUsers.length > 1 && schedule.requesterClass) {
requesterName = `${requesterName}(${schedule.requesterGrade} ${schedule.requesterClass})`
} else {
requesterName = `${requesterName}(${schedule.requesterGrade})`
}
}
}
return {
id: schedule.id,
playDate: dateOnly.toISOString().split('T')[0],
sequence: schedule.sequence || 1,
played: schedule.played || false,
playTimeId: schedule.playTimeId,
isDraft: schedule.isDraft,
publishedAt: schedule.publishedAt,
playTime: schedule.playTimeName
? {
id: schedule.playTimeId,
name: schedule.playTimeName,
startTime: schedule.playTimeStartTime,
endTime: schedule.playTimeEndTime,
enabled: schedule.playTimeEnabled
}
: null,
song: {
id: schedule.songId,
title: schedule.songTitle,
artist: schedule.songArtist,
requester: requesterName,
voteCount: voteCountMap.get(schedule.songId) || 0,
played: schedule.songPlayed || false,
cover: schedule.songCover || null,
musicPlatform: schedule.songMusicPlatform || null,
musicId: schedule.songMusicId || null,
semester: schedule.songSemester || null
}
}
})
const completeSchedules = formattedSchedules
// 重新缓存完整的排期列表
if (completeSchedules && completeSchedules.length > 0) {
await cacheService.setSchedulesList(completeSchedules)
console.log(`[Cache] 已发布排期列表已重新缓存,数量: ${completeSchedules.length}`)
} else {
console.log('[Cache] 没有已发布排期数据需要缓存')
}
} catch (cacheError) {
console.warn('[Cache] 重新缓存完整排期列表失败,但不影响排期发布:', cacheError)
}
return {
...draft,
...publishedSchedule,
song: draft.song,
isDraft: false,
publishedAt: publishedSchedule.publishedAt,
// 绑定在 fulfill 中回写,.returning() 快照早于绑定,需显式覆盖
replayRequestId: publishResult.replayRequestId,
message: publishResult.shouldNotify
? notificationSent
? '排期发布成功,通知已发送'
: '排期发布成功,但通知发送失败'
: '排期发布成功'
publishedAt: publishedAt,
message: '排期发布成功,通知已发送'
}
} catch (error: any) {
console.error('发布排期失败:', error)
if (error.statusCode) throw error
throw createError({
statusCode: 500,
message: error.message || '发布排期失败'