diff --git a/server/api/admin/schedule/publish-drafts.post.ts b/server/api/admin/schedule/publish-drafts.post.ts new file mode 100644 index 0000000..0591253 --- /dev/null +++ b/server/api/admin/schedule/publish-drafts.post.ts @@ -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 || '批量发布失败' + }) + } +})