195 lines
6.1 KiB
TypeScript
195 lines
6.1 KiB
TypeScript
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 { getBeijingTimestamp } from '~/utils/timeUtils'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
// 检查用户认证和权限
|
|
const user = event.context.user
|
|
|
|
if (!user) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
message: '未授权访问'
|
|
})
|
|
}
|
|
|
|
if (!['SONG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: '只有歌曲管理员及以上权限才能发布排期'
|
|
})
|
|
}
|
|
|
|
const body = await readBody(event)
|
|
|
|
if (!body.scheduleId) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: '排期ID不能为空'
|
|
})
|
|
}
|
|
|
|
try {
|
|
// 检查草稿是否存在
|
|
const draftResult = await db
|
|
.select({
|
|
id: schedules.id,
|
|
songId: schedules.songId,
|
|
playDate: schedules.playDate,
|
|
sequence: schedules.sequence,
|
|
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
|
|
}
|
|
})
|
|
.from(schedules)
|
|
.innerJoin(songs, eq(schedules.songId, songs.id))
|
|
.where(
|
|
and(
|
|
eq(schedules.id, body.scheduleId),
|
|
eq(schedules.isDraft, true) // 确保是草稿状态
|
|
)
|
|
)
|
|
.limit(1)
|
|
|
|
const draft = draftResult[0]
|
|
|
|
if (!draft) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
message: '找不到指定的排期草稿'
|
|
})
|
|
}
|
|
|
|
// 使用事务包装更新排期和重播申请状态操作,保证原子性
|
|
const publishResult = await db.transaction(async (tx) => {
|
|
// 更新草稿为已发布状态
|
|
const publishedAt = new Date(getBeijingTimestamp())
|
|
|
|
const publishResult = await tx
|
|
.update(schedules)
|
|
.set({
|
|
isDraft: false,
|
|
publishedAt: publishedAt,
|
|
updatedAt: publishedAt
|
|
})
|
|
.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)
|
|
|
|
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
|
|
})
|
|
} catch (notificationError) {
|
|
notificationSent = false
|
|
console.error('排期已发布,但发送歌曲入选通知失败:', notificationError)
|
|
}
|
|
}
|
|
|
|
const publishedSchedule = publishResult.schedule
|
|
|
|
return {
|
|
...draft,
|
|
...publishedSchedule,
|
|
song: draft.song,
|
|
isDraft: false,
|
|
publishedAt: publishedSchedule.publishedAt,
|
|
// 绑定在 fulfill 中回写,.returning() 快照早于绑定,需显式覆盖
|
|
replayRequestId: publishResult.replayRequestId,
|
|
message: publishResult.shouldNotify
|
|
? notificationSent
|
|
? '排期发布成功,通知已发送'
|
|
: '排期发布成功,但通知发送失败'
|
|
: '排期发布成功'
|
|
}
|
|
} catch (error: any) {
|
|
console.error('发布排期失败:', error)
|
|
if (error.statusCode) throw error
|
|
throw createError({
|
|
statusCode: 500,
|
|
message: error.message || '发布排期失败'
|
|
})
|
|
}
|
|
})
|