feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 6m31s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s

- 新增 /api/admin/songs/batch-reject.post 批量驳回接口(事务清排期/投票/扣接纳数/黑名单去重/异步通知)
- requireSongAdmin 三角色校验 + createApiError 统一错误包装
- scheduleDaysBefore/After 四字段排期可见范围限制(开放 API 与公开排期接口三处缓存路径 + DB 条件过滤)
- submissionNoteAutoModeration 基于黑名单 KEYWORD/LANGUAGE/GENRE 自动判定留言 publicStatus,命中自动关闭 submissionNotePublic
- 更新 songs/request.post 与 songs/[id]/update.put 接入自动审查逻辑
This commit is contained in:
2026-09-06 17:03:20 +08:00
parent 43e2a05666
commit bd8df1f026
53 changed files with 10486 additions and 245 deletions
+112
View File
@@ -0,0 +1,112 @@
import { randomUUID } from 'node:crypto'
import { asc, eq, sql } from 'drizzle-orm'
import { db } from '~/drizzle/db'
import { systemSettings } from '~/drizzle/schema'
import { SYSTEM_SETTINGS_DEFAULTS } from './system-settings-defaults'
import { getServerTimestamp } from './serverTime'
export interface InstanceIdInfo {
instanceId: string
persisted: boolean
}
let cachedInstanceIdInfo: InstanceIdInfo | null = null
let cachedInstanceIdExpiresAt = 0
let pendingInstanceIdPromise: Promise<string> | null = null
let pendingInstanceIdInfoPromise: Promise<InstanceIdInfo> | null = null
const TEMPORARY_INSTANCE_ID_CACHE_TTL_MS = 5 * 60 * 1000
const loadOrCreateInstanceId = async (): Promise<string> => {
const settingsResult = await db
.select({ id: systemSettings.id, instanceId: systemSettings.instanceId })
.from(systemSettings)
.orderBy(asc(systemSettings.id))
.limit(1)
const settings = settingsResult[0]
if (settings?.instanceId) {
return settings.instanceId
}
const instanceId = randomUUID()
if (!settings) {
await db
.insert(systemSettings)
.values({ id: 1, ...SYSTEM_SETTINGS_DEFAULTS, instanceId })
.onConflictDoUpdate({
target: systemSettings.id,
set: { instanceId, updatedAt: new Date() }
})
await db.execute(sql`
SELECT setval(pg_get_serial_sequence('"SystemSettings"', 'id'), (SELECT MAX(id) FROM "SystemSettings"))
`)
} else {
await db
.update(systemSettings)
.set({ instanceId, updatedAt: new Date() })
.where(eq(systemSettings.id, settings.id))
}
return instanceId
}
const getCachedInstanceIdInfo = (): InstanceIdInfo | null => {
if (!cachedInstanceIdInfo) {
return null
}
if (cachedInstanceIdInfo.persisted || cachedInstanceIdExpiresAt > getServerTimestamp()) {
return cachedInstanceIdInfo
}
return null
}
export const getInstanceIdInfo = async (): Promise<InstanceIdInfo> => {
const cached = getCachedInstanceIdInfo()
if (cached) {
return cached
}
if (!pendingInstanceIdInfoPromise) {
pendingInstanceIdInfoPromise = loadOrCreateInstanceId()
.then((instanceId) => {
const info = { instanceId, persisted: true }
cachedInstanceIdInfo = info
cachedInstanceIdExpiresAt = Number.POSITIVE_INFINITY
return info
})
.catch((error) => {
const fallbackInstanceId = cachedInstanceIdInfo?.persisted === false
? cachedInstanceIdInfo.instanceId
: randomUUID()
const info = { instanceId: fallbackInstanceId, persisted: false }
cachedInstanceIdInfo = info
cachedInstanceIdExpiresAt = getServerTimestamp() + TEMPORARY_INSTANCE_ID_CACHE_TTL_MS
console.warn('[Instance ID] Failed to persist instance ID, using temporary fallback value:', error)
return info
})
.finally(() => {
pendingInstanceIdInfoPromise = null
})
}
return pendingInstanceIdInfoPromise
}
export const getInstanceId = async (): Promise<string> => {
if (!pendingInstanceIdPromise) {
pendingInstanceIdPromise = getInstanceIdInfo()
.then(({ instanceId }) => instanceId)
.finally(() => {
pendingInstanceIdPromise = null
})
}
return pendingInstanceIdPromise
}