- 新增 /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 接入自动审查逻辑
113 lines
3.2 KiB
TypeScript
113 lines
3.2 KiB
TypeScript
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
|
|
}
|