feat: 迁移自定义功能至 1.5.9.9 版本
包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { schedules, songs } from '~/drizzle/schema'
|
||||
import { and, desc, eq, gte, lte, ne } from 'drizzle-orm'
|
||||
import { createSongSelectedNotification, createReplaySongSelectedNotification } from '../../services/notificationService'
|
||||
import { redeemCardCodeForSchedule } from '~~/server/services/cardCodeLifecycleService'
|
||||
import { fulfillReplayRequestsForSchedule } from '~~/server/utils/scheduleReplayBinding'
|
||||
import { getServerDate } from '~~/server/utils/serverTime'
|
||||
import {
|
||||
playTimes,
|
||||
schedules,
|
||||
songs,
|
||||
users,
|
||||
votes,
|
||||
songReplayRequests,
|
||||
scheduleSongPool
|
||||
} from '~/drizzle/schema'
|
||||
import { and, asc, count, desc, eq, gte, lte } from 'drizzle-orm'
|
||||
import { createSongSelectedNotification } from '../../services/notificationService'
|
||||
import { cacheService } from '~~/server/services/cacheService'
|
||||
import { getClientIP } from '~~/server/utils/ip-utils'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 检查用户认证和权限
|
||||
@@ -36,12 +43,6 @@ export default defineEventHandler(async (event) => {
|
||||
// 检查是否为草稿模式(默认直接发布)
|
||||
const isDraft = body.isDraft === true
|
||||
|
||||
// 前端显式选择的重播申请,发布时优先绑定
|
||||
const preferredReplayRequestId =
|
||||
Number.isInteger(Number(body.replayRequestId)) && Number(body.replayRequestId) > 0
|
||||
? Number(body.replayRequestId)
|
||||
: null
|
||||
|
||||
try {
|
||||
// 检查歌曲是否存在
|
||||
const songResult = await db.select().from(songs).where(eq(songs.id, body.songId)).limit(1)
|
||||
@@ -55,7 +56,20 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
const transactionResult = await db.transaction(async (tx) => {
|
||||
await db.transaction(async (tx) => {
|
||||
// 检查是否已经为该歌曲创建过排期,如果有则删除旧的排期
|
||||
const existingScheduleResult = await tx
|
||||
.select()
|
||||
.from(schedules)
|
||||
.where(eq(schedules.songId, body.songId))
|
||||
.limit(1)
|
||||
|
||||
const existingSchedule = existingScheduleResult[0]
|
||||
if (existingSchedule) {
|
||||
// 删除现有排期
|
||||
await tx.delete(schedules).where(eq(schedules.id, existingSchedule.id))
|
||||
}
|
||||
|
||||
// 获取序号,如果未提供则查找当天最大序号+1
|
||||
let sequence = body.sequence || 1
|
||||
|
||||
@@ -81,8 +95,9 @@ export default defineEventHandler(async (event) => {
|
||||
.orderBy(desc(schedules.sequence))
|
||||
.limit(1)
|
||||
|
||||
const latestSchedule = sameDaySchedules[0]
|
||||
if (latestSchedule) sequence = (latestSchedule.sequence || 0) + 1
|
||||
if (sameDaySchedules.length > 0) {
|
||||
sequence = (sameDaySchedules[0].sequence || 0) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 解析输入的日期字符串,确保日期正确
|
||||
@@ -102,120 +117,219 @@ export default defineEventHandler(async (event) => {
|
||||
playTimeId: body.playTimeId || null,
|
||||
// 草稿支持:根据参数决定是否为草稿
|
||||
isDraft: isDraft,
|
||||
publishedAt: isDraft ? null : getServerDate()
|
||||
publishedAt: isDraft ? null : new Date()
|
||||
})
|
||||
.returning()
|
||||
|
||||
const createdSchedule = scheduleResult[0]
|
||||
if (!createdSchedule) {
|
||||
throw createError({ statusCode: 500, message: '创建排期失败' })
|
||||
}
|
||||
|
||||
const schedule = {
|
||||
...createdSchedule,
|
||||
...scheduleResult[0],
|
||||
song: {
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
requesterId: song.requesterId,
|
||||
cardCodeId: song.cardCodeId
|
||||
requesterId: song.requesterId
|
||||
}
|
||||
}
|
||||
|
||||
let shouldNotify = false
|
||||
let replayRequesterIds: number[] = []
|
||||
// 获取客户端IP地址
|
||||
const clientIP = getClientIP(event)
|
||||
|
||||
// 只有在非草稿模式下才更新重播状态
|
||||
await tx
|
||||
.update(scheduleSongPool)
|
||||
.set({
|
||||
status: 'ASSIGNED',
|
||||
targetPlayDate: playDate
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(scheduleSongPool.songId, body.songId),
|
||||
eq(scheduleSongPool.status, 'PENDING')
|
||||
)
|
||||
)
|
||||
|
||||
// 只有在非草稿模式下才创建通知
|
||||
if (!isDraft) {
|
||||
// 检查该歌曲是否已经有其他已发布的排期
|
||||
const existingPublished = await tx
|
||||
.select({ id: schedules.id })
|
||||
.from(schedules)
|
||||
await createSongSelectedNotification(
|
||||
schedule.song.requesterId,
|
||||
schedule.song.id,
|
||||
{
|
||||
title: schedule.song.title,
|
||||
artist: schedule.song.artist,
|
||||
playDate: schedule.playDate
|
||||
},
|
||||
clientIP
|
||||
)
|
||||
|
||||
// 标记该歌曲的所有待处理重播申请为已完成
|
||||
await tx
|
||||
.update(songReplayRequests)
|
||||
.set({ status: 'FULFILLED' })
|
||||
.where(
|
||||
and(
|
||||
eq(schedules.songId, schedule.song.id),
|
||||
eq(schedules.isDraft, false),
|
||||
ne(schedules.id, createdSchedule.id)
|
||||
eq(songReplayRequests.songId, schedule.song.id),
|
||||
eq(songReplayRequests.status, 'PENDING')
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
// 如果之前没有正式发布过,才发送通知(首次排期通知)
|
||||
shouldNotify = existingPublished.length === 0
|
||||
|
||||
const replayBinding = await fulfillReplayRequestsForSchedule({
|
||||
tx,
|
||||
songId: schedule.song.id,
|
||||
scheduleId: createdSchedule.id,
|
||||
at: createdSchedule.publishedAt || getServerDate(),
|
||||
preferredRequestId: preferredReplayRequestId || undefined
|
||||
})
|
||||
|
||||
if (replayBinding) {
|
||||
replayRequesterIds = replayBinding.replayRequesterIds
|
||||
}
|
||||
|
||||
if (existingPublished.length > 0) {
|
||||
console.log(`歌曲 ${schedule.song.id} 已有其他正式排期,不再重复发送通知`)
|
||||
}
|
||||
|
||||
await redeemCardCodeForSchedule(tx, {
|
||||
songId: schedule.song.id,
|
||||
cardCodeId: schedule.song.cardCodeId,
|
||||
operatorId: user.id,
|
||||
at: createdSchedule.publishedAt || getServerDate()
|
||||
})
|
||||
}
|
||||
|
||||
return { schedule, shouldNotify, replayRequesterIds }
|
||||
})
|
||||
|
||||
let notificationSent = true
|
||||
if (transactionResult.shouldNotify) {
|
||||
try {
|
||||
await createSongSelectedNotification(song.requesterId, song.id, {
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
playDate: transactionResult.schedule.playDate
|
||||
// 清除相关缓存
|
||||
try {
|
||||
await cacheService.clearSchedulesCache()
|
||||
await cacheService.clearSongsCache() // 清除歌曲列表缓存,确保scheduled状态更新
|
||||
console.log(`[Cache] 排期缓存和歌曲列表缓存已清除(${isDraft ? '保存草稿' : '创建排期'})`)
|
||||
} 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,
|
||||
songId: schedules.songId,
|
||||
songTitle: songs.title,
|
||||
songArtist: songs.artist,
|
||||
songRequesterId: songs.requesterId,
|
||||
songPlayed: songs.played,
|
||||
songCover: songs.cover,
|
||||
songMusicPlatform: songs.musicPlatform,
|
||||
songMusicId: songs.musicId,
|
||||
songPlayUrl: songs.playUrl,
|
||||
songSemester: songs.semester,
|
||||
requesterName: users.name,
|
||||
requesterGrade: users.grade,
|
||||
requesterClass: users.class,
|
||||
playTimeName: playTimes.name,
|
||||
playTimeStartTime: playTimes.startTime,
|
||||
playTimeEndTime: playTimes.endTime,
|
||||
playTimeEnabled: playTimes.enabled
|
||||
})
|
||||
} catch (notificationError) {
|
||||
notificationSent = false
|
||||
console.error('排期已创建,但发送歌曲入选通知失败:', notificationError)
|
||||
}
|
||||
}
|
||||
.from(schedules)
|
||||
.innerJoin(songs, eq(schedules.songId, songs.id))
|
||||
.innerJoin(users, eq(songs.requesterId, users.id))
|
||||
.leftJoin(playTimes, eq(schedules.playTimeId, playTimes.id))
|
||||
.orderBy(asc(schedules.playDate))
|
||||
|
||||
// 发送重播申请已安排通知
|
||||
if (!isDraft && transactionResult.replayRequesterIds.length > 0) {
|
||||
for (const replayUserId of transactionResult.replayRequesterIds) {
|
||||
try {
|
||||
await createReplaySongSelectedNotification(replayUserId, song.id, {
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
playDate: transactionResult.schedule.playDate
|
||||
}, transactionResult.schedule.id)
|
||||
} catch (error) {
|
||||
console.error(`发送重播安排通知给用户 ${replayUserId} 失败:`, error)
|
||||
// 获取所有用户的姓名列表,用于检测同名用户
|
||||
const allUsers = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
grade: users.grade,
|
||||
class: users.class
|
||||
})
|
||||
.from(users)
|
||||
|
||||
// 创建姓名到用户数组的映射
|
||||
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 schedule = transactionResult.schedule
|
||||
// 获取每首歌的投票数
|
||||
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,
|
||||
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,
|
||||
playUrl: schedule.songPlayUrl || 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 {
|
||||
...schedule,
|
||||
isDraft: isDraft,
|
||||
publishedAt: isDraft ? null : schedule.publishedAt,
|
||||
message: isDraft
|
||||
? '排期草稿保存成功'
|
||||
: transactionResult.shouldNotify
|
||||
? notificationSent
|
||||
? '排期创建成功,通知已发送'
|
||||
: '排期创建成功,但通知发送失败'
|
||||
: '排期创建成功'
|
||||
message: isDraft ? '排期草稿保存成功' : '排期创建成功,通知已发送'
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('创建排期失败:', error)
|
||||
if (error.statusCode) throw error
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || '创建排期失败'
|
||||
|
||||
Reference in New Issue
Block a user