feat(排期): 新增批量发布全部草稿接口 publish-drafts

给小程序端「一键全部发布」用:一个事务把所有草稿翻成已发布并处理待处理重播申请,只清一次缓存、批量发订阅消息。避免前端逐条调用单条 publish 时每次都重建整张排期缓存导致的卡顿。
This commit is contained in:
2026-10-24 13:39:13 +08:00
committed by trae-solo-agent
parent f5a43b37cc
commit 4fe807eb39
@@ -0,0 +1,130 @@
import { db } from '~/drizzle/db'
import { schedules, songReplayRequests, songs } from '~/drizzle/schema'
import { and, eq, inArray } from 'drizzle-orm'
import { createSongSelectedNotification } from '~~/server/services/notificationService'
import { cacheService } from '~~/server/services/cacheService'
import { getClientIP } from '~~/server/utils/ip-utils'
/**
* 一键发布「所有日期的草稿」。
*
* 与单条 publish.post.ts 的区别:
* · 一次事务把所有草稿翻成已发布(不删数据、不影响已发布内容)
* · 只清一次缓存、批量发订阅消息 —— 单条 publish 每调一次都会重建整张排期缓存,
* 草稿多时前端逐条调用会非常慢,所以这里给小程序一个批量入口。
*
* 也刻意不用 bulk-publish:那是「按日期 ± 时段整桶先删后插」的破坏性操作,
* 会丢掉 played 标记和原有排期 id,不适合「只是把草稿翻状态」。
*/
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 clientIP = getClientIP(event)
try {
// 取出所有草稿及其歌曲信息(订阅消息要用)
const drafts = await db
.select({
id: schedules.id,
playDate: schedules.playDate,
sequence: schedules.sequence,
songId: songs.id,
songTitle: songs.title,
songArtist: songs.artist,
songRequesterId: songs.requesterId
})
.from(schedules)
.innerJoin(songs, eq(schedules.songId, songs.id))
.where(eq(schedules.isDraft, true))
if (drafts.length === 0) {
return {
success: true,
publishedCount: 0,
notifiedCount: 0,
message: '没有待发布的草稿'
}
}
const publishedAt = new Date()
const songIds = Array.from(new Set(drafts.map((d) => d.songId)))
await db.transaction(async (tx) => {
// 1. 全部草稿翻成已发布
await tx
.update(schedules)
.set({ isDraft: false, publishedAt, updatedAt: publishedAt })
.where(eq(schedules.isDraft, true))
// 2. 涉及的歌曲:待处理重播申请标记为已完成
await tx
.update(songReplayRequests)
.set({ status: 'FULFILLED', updatedAt: publishedAt })
.where(
and(
inArray(songReplayRequests.songId, songIds),
eq(songReplayRequests.status, 'PENDING')
)
)
})
// 清除缓存(读接口会自行重建)
try {
await cacheService.clearSchedulesCache()
await cacheService.clearSongsCache()
} catch (cacheError) {
console.error('[Cache] 清除缓存失败(批量发布草稿):', cacheError)
}
// 每条草稿对应一条订阅消息,异步发送不阻塞响应
Promise.allSettled(
drafts.map((d) =>
createSongSelectedNotification(
d.songRequesterId,
d.songId,
{
title: d.songTitle,
artist: d.songArtist,
playDate: d.playDate,
sequence: d.sequence
},
clientIP
)
)
)
.then((results) => {
const ok = results.filter((r) => r.status === 'fulfilled').length
console.log(`[Notification] 批量发布全部草稿通知完成: ${ok}/${drafts.length} 成功`)
})
.catch((err) => {
console.error('[Notification] 批量发布全部草稿时发送通知出错:', err)
})
return {
success: true,
publishedCount: drafts.length,
notifiedCount: drafts.length,
message: `已发布 ${drafts.length} 条草稿`
}
} catch (error: any) {
console.error('批量发布全部草稿失败:', error)
throw createError({
statusCode: 500,
message: error.message || '批量发布失败'
})
}
})