feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { prepareBackup, executeUploads, acquireBackupLock, releaseBackupLock } from '~~/server/services/autoBackupService'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
acquireBackupLock()
|
||||
try {
|
||||
const prepared = await prepareBackup()
|
||||
// 必须等待上传完成再返回响应:
|
||||
// Vercel Serverless 在响应返回后会冻结/回收函数实例,
|
||||
// 若以 fire-and-forget 后台执行,上传会被中途掐断(socket closed)。
|
||||
const result = await executeUploads(prepared)
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.success ? '备份完成' : '备份部分或全部失败',
|
||||
backupId: prepared.historyId,
|
||||
results: result.results
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
throw createApiError(500, SERVER_ERROR_CODES.BACKUP_FAILED, error.message || '备份执行失败')
|
||||
} finally {
|
||||
releaseBackupLock()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { defineEventHandler, getRouterParam } from 'h3'
|
||||
import { eq, desc } from 'drizzle-orm'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { backupHistory } from '~/drizzle/schema'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
|
||||
/** 60 秒超时阈值 */
|
||||
const TIMEOUT_THRESHOLD_MS = 60_000
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = getRouterParam(event, 'id')
|
||||
|
||||
if (!idParam) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '缺少备份 ID')
|
||||
}
|
||||
|
||||
let record
|
||||
|
||||
if (idParam === 'latest') {
|
||||
const [latest] = await db
|
||||
.select()
|
||||
.from(backupHistory)
|
||||
.orderBy(desc(backupHistory.createdAt))
|
||||
.limit(1)
|
||||
record = latest
|
||||
} else {
|
||||
const backupId = parseInt(idParam, 10)
|
||||
if (isNaN(backupId)) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '无效的备份 ID')
|
||||
}
|
||||
const [found] = await db
|
||||
.select()
|
||||
.from(backupHistory)
|
||||
.where(eq(backupHistory.id, backupId))
|
||||
record = found
|
||||
}
|
||||
|
||||
if (!record) {
|
||||
throw createApiError(404, 'BACKUP_NOT_FOUND', '备份记录不存在')
|
||||
}
|
||||
|
||||
const methods: Array<{ method: string; success: boolean; error?: string | null }> = JSON.parse(record.methods)
|
||||
const allCompleted = methods.every(m => m.success || m.error != null)
|
||||
const age = Date.now() - new Date(record.createdAt).getTime()
|
||||
const completedCount = methods.filter(m => m.success || m.error != null).length
|
||||
const succeededCount = methods.filter(m => m.success).length
|
||||
const failedCount = methods.filter(m => m.error != null).length
|
||||
|
||||
const status = allCompleted
|
||||
? (succeededCount === methods.length ? 'completed' : failedCount === methods.length ? 'failed' : 'partial')
|
||||
: (age > TIMEOUT_THRESHOLD_MS ? 'timeout' : 'running')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
id: record.id,
|
||||
filename: record.filename,
|
||||
status,
|
||||
totalRecords: record.totalRecords,
|
||||
backupSize: record.backupSize,
|
||||
createdAt: record.createdAt,
|
||||
progress: {
|
||||
total: methods.length,
|
||||
completed: completedCount,
|
||||
succeeded: succeededCount,
|
||||
failed: failedCount
|
||||
},
|
||||
methods
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { handleOpenCardCodeDelete } from '~~/server/utils/card-code-delete-handler'
|
||||
|
||||
export default defineEventHandler(handleOpenCardCodeDelete)
|
||||
@@ -0,0 +1,98 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodes } from '~/drizzle/schema'
|
||||
import { and, count, desc, eq, ilike, or } from 'drizzle-orm'
|
||||
import { CARD_CODE_STATUSES } from '../../card-codes/statuses'
|
||||
|
||||
const MAX_KEYWORD_LENGTH = 100
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const apiKey = event.context.apiKey
|
||||
if (!apiKey) {
|
||||
throw createError({ statusCode: 401, message: 'API认证失败' })
|
||||
}
|
||||
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const status = typeof query.status === 'string' ? query.status.trim().toUpperCase() : ''
|
||||
const keyword = typeof query.q === 'string' ? query.q.trim() : ''
|
||||
const includeStats = ['1', 'true', 'yes'].includes(String(query.includeStats || query.stats || '').toLowerCase())
|
||||
const page = Math.max(1, Number.parseInt(String(query.page || '1'), 10) || 1)
|
||||
const limitInput = Number.parseInt(String(query.limit || '20'), 10) || 20
|
||||
const limit = Math.min(100, Math.max(1, limitInput))
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
if (status && !CARD_CODE_STATUSES.includes(status as any)) {
|
||||
throw createError({ statusCode: 400, message: '不支持的状态值' })
|
||||
}
|
||||
if (keyword.length > MAX_KEYWORD_LENGTH) {
|
||||
throw createError({ statusCode: 400, message: `搜索关键词不能超过 ${MAX_KEYWORD_LENGTH} 个字符` })
|
||||
}
|
||||
|
||||
const conditions = [] as any[]
|
||||
if (status) {
|
||||
conditions.push(eq(cardCodes.status, status as any))
|
||||
}
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(cardCodes.code, `%${keyword}%`),
|
||||
ilike(cardCodes.note, `%${keyword}%`)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let queryBuilder = db.select().from(cardCodes)
|
||||
let countQueryBuilder = db.select({ count: count() }).from(cardCodes)
|
||||
if (conditions.length > 0) {
|
||||
queryBuilder = queryBuilder.where(and(...conditions))
|
||||
countQueryBuilder = countQueryBuilder.where(and(...conditions))
|
||||
}
|
||||
|
||||
const statsQuery = includeStats
|
||||
? db.select({ status: cardCodes.status, count: count() }).from(cardCodes).groupBy(cardCodes.status)
|
||||
: Promise.resolve([])
|
||||
|
||||
const [items, totalResult, statsRows] = await Promise.all([
|
||||
queryBuilder.orderBy(desc(cardCodes.createdAt)).limit(limit).offset(offset),
|
||||
countQueryBuilder,
|
||||
statsQuery
|
||||
])
|
||||
|
||||
let stats
|
||||
if (includeStats) {
|
||||
stats = { total: 0, available: 0, locked: 0, redeemed: 0, invalid: 0 }
|
||||
for (const row of statsRows) {
|
||||
const value = Number(row.count || 0)
|
||||
stats.total += value
|
||||
if (row.status === 'AVAILABLE') stats.available = value
|
||||
if (row.status === 'LOCKED') stats.locked = value
|
||||
if (row.status === 'REDEEMED') stats.redeemed = value
|
||||
if (row.status === 'INVALID') stats.invalid = value
|
||||
}
|
||||
}
|
||||
|
||||
const total = Number(totalResult[0]?.count || 0)
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
cardCodes: items,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.max(1, Math.ceil(total / limit))
|
||||
},
|
||||
...(includeStats ? { stats } : {}),
|
||||
filters: {
|
||||
status: status || null,
|
||||
q: keyword || null,
|
||||
includeStats
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.statusCode) throw err
|
||||
console.error('开放API获取点歌券列表失败', err)
|
||||
throw createError({ statusCode: 500, message: '获取点歌券列表失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,129 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodes, cardCodeRedeemLogs } from '~/drizzle/schema'
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import { CARD_CODE_STATUSES } from '../../card-codes/statuses'
|
||||
|
||||
const MAX_ID_LENGTH = 32
|
||||
const MAX_NOTE_LENGTH = 500
|
||||
|
||||
const updateCardCodesSchema = z.object({
|
||||
id: z.union([z.number().int().positive(), z.string().max(MAX_ID_LENGTH)]).optional(),
|
||||
ids: z.array(z.union([z.number().int().positive(), z.string().max(MAX_ID_LENGTH)])).max(500, '单次最多更新 500 个点歌券').optional(),
|
||||
status: z.enum(CARD_CODE_STATUSES).optional(),
|
||||
note: z.union([z.string().max(MAX_NOTE_LENGTH), z.null()]).optional()
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const apiKey = event.context.apiKey
|
||||
if (!apiKey) {
|
||||
throw createError({ statusCode: 401, message: 'API认证失败' })
|
||||
}
|
||||
|
||||
const operatorId = Number(apiKey.createdByUserId)
|
||||
if (!Number.isInteger(operatorId) || operatorId <= 0) {
|
||||
throw createError({ statusCode: 400, message: 'API Key 缺少有效创建者,无法执行点歌券状态变更' })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await readBody(event) ?? {}
|
||||
const validatedData = updateCardCodesSchema.parse(body)
|
||||
const rawIds = [
|
||||
...(validatedData.id !== undefined ? [validatedData.id] : []),
|
||||
...(validatedData.ids || [])
|
||||
]
|
||||
const ids = Array.from(new Set(
|
||||
rawIds.map((id) => Number(id)).filter((id) => Number.isInteger(id) && id > 0)
|
||||
))
|
||||
|
||||
if (!ids.length) {
|
||||
throw createError({ statusCode: 400, message: '缺少有效点歌券ID' })
|
||||
}
|
||||
|
||||
const updateObj: any = { updatedAt: new Date() }
|
||||
const status = validatedData.status
|
||||
if (status) {
|
||||
updateObj.status = status
|
||||
if (status === 'REDEEMED') {
|
||||
updateObj.redeemedBy = operatorId
|
||||
updateObj.redeemedAt = new Date()
|
||||
updateObj.lockedBy = null
|
||||
updateObj.lockedAt = null
|
||||
}
|
||||
if (status === 'LOCKED') {
|
||||
updateObj.lockedBy = operatorId
|
||||
updateObj.lockedAt = new Date()
|
||||
updateObj.redeemedBy = null
|
||||
updateObj.redeemedAt = null
|
||||
}
|
||||
if (status === 'AVAILABLE') {
|
||||
updateObj.lockedBy = null
|
||||
updateObj.lockedAt = null
|
||||
updateObj.redeemedBy = null
|
||||
updateObj.redeemedAt = null
|
||||
}
|
||||
}
|
||||
if ('note' in validatedData) {
|
||||
updateObj.note = typeof validatedData.note === 'string' ? validatedData.note.trim() || null : validatedData.note
|
||||
}
|
||||
if (!status && !('note' in validatedData)) {
|
||||
throw createError({ statusCode: 400, message: '没有需要更新的字段' })
|
||||
}
|
||||
|
||||
const updatedCardCodes = await db.transaction(async (tx) => {
|
||||
const beforeRows = await tx
|
||||
.select({
|
||||
id: cardCodes.id,
|
||||
code: cardCodes.code,
|
||||
status: cardCodes.status
|
||||
})
|
||||
.from(cardCodes)
|
||||
.where(inArray(cardCodes.id, ids))
|
||||
const beforeMap = new Map(beforeRows.map((row) => [row.id, row]))
|
||||
|
||||
const updatedRows = await tx.update(cardCodes).set(updateObj).where(inArray(cardCodes.id, ids)).returning()
|
||||
|
||||
if (status === 'REDEEMED' && updatedRows.length > 0) {
|
||||
const logsToInsert = updatedRows
|
||||
.filter((row) => {
|
||||
const before = beforeMap.get(row.id)
|
||||
return before && before.status !== status
|
||||
})
|
||||
.map((row) => ({
|
||||
cardCodeId: row.id,
|
||||
codeSnapshot: row.code,
|
||||
redeemedBy: operatorId,
|
||||
redeemedAt: row.redeemedAt || new Date(),
|
||||
source: 'OPEN_API_MANUAL',
|
||||
songId: null
|
||||
}))
|
||||
|
||||
if (logsToInsert.length > 0) {
|
||||
await tx.insert(cardCodeRedeemLogs).values(logsToInsert)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedRows
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '点歌券更新成功',
|
||||
data: {
|
||||
cardCodes: updatedCardCodes,
|
||||
updated: updatedCardCodes.length,
|
||||
requested: ids.length
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.statusCode) throw err
|
||||
if (err.name === 'ZodError') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `请求参数验证失败:${(err.issues || err.errors).map((issue: any) => issue.message).join(', ')}`
|
||||
})
|
||||
}
|
||||
console.error('开放API更新点歌券失败', err)
|
||||
throw createError({ statusCode: 500, message: '更新点歌券失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,151 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodes } from '~/drizzle/schema'
|
||||
import { z } from 'zod'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const MAX_BATCH_COUNT = 10000
|
||||
const DB_CHUNK_SIZE = 500
|
||||
const MAX_CODE_LENGTH = 128
|
||||
const MAX_PREFIX_LENGTH = 32
|
||||
const MAX_CHARSET_LENGTH = 128
|
||||
const MAX_NOTE_LENGTH = 500
|
||||
const MIN_RANDOM_SPACE_MULTIPLIER = 10
|
||||
const DEFAULT_CHARSET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
|
||||
const createCardCodesSchema = z.object({
|
||||
codes: z
|
||||
.union([
|
||||
z.string().max(MAX_CODE_LENGTH),
|
||||
z.array(z.string().max(MAX_CODE_LENGTH)).max(MAX_BATCH_COUNT)
|
||||
])
|
||||
.optional(),
|
||||
count: z.number().int().min(0).max(MAX_BATCH_COUNT).optional(),
|
||||
prefix: z.string().max(MAX_PREFIX_LENGTH).optional(),
|
||||
length: z.number().int().min(4).max(64).optional(),
|
||||
charset: z.string().max(MAX_CHARSET_LENGTH).optional(),
|
||||
note: z.union([z.string().max(MAX_NOTE_LENGTH), z.null()]).optional()
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const apiKey = event.context.apiKey
|
||||
if (!apiKey) {
|
||||
throw createError({ statusCode: 401, message: 'API认证失败' })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await readBody(event) ?? {}
|
||||
const validatedData = createCardCodesSchema.parse(body)
|
||||
|
||||
const rawCodes = Array.isArray(validatedData.codes)
|
||||
? validatedData.codes
|
||||
: validatedData.codes
|
||||
? [validatedData.codes]
|
||||
: []
|
||||
const codes = rawCodes.map((code) => String(code).trim().toUpperCase()).filter(Boolean)
|
||||
const manualCodeSet = new Set(codes)
|
||||
const generatedCodeSet = new Set<string>()
|
||||
const batchCount = validatedData.count || 0
|
||||
const prefix = typeof validatedData.prefix === 'string' ? validatedData.prefix.trim().toUpperCase() : ''
|
||||
const length = validatedData.length || 12
|
||||
const charsetInput = typeof validatedData.charset === 'string' && validatedData.charset.trim()
|
||||
? validatedData.charset.trim().toUpperCase()
|
||||
: DEFAULT_CHARSET
|
||||
const charset = [...new Set(charsetInput.split(''))].join('')
|
||||
|
||||
if (!charset) {
|
||||
throw createError({ statusCode: 400, message: '字符集不能为空' })
|
||||
}
|
||||
if (manualCodeSet.size + batchCount > MAX_BATCH_COUNT) {
|
||||
throw createError({ statusCode: 400, message: `单次最多创建 ${MAX_BATCH_COUNT} 个点歌券` })
|
||||
}
|
||||
|
||||
if (batchCount > 0) {
|
||||
const charsetSet = new Set(charset.split(''))
|
||||
const generatedNamespaceManualCount = codes.filter((code) => {
|
||||
if (!code.startsWith(prefix) || code.length !== prefix.length + length) return false
|
||||
return code.slice(prefix.length).split('').every((char) => charsetSet.has(char))
|
||||
}).length
|
||||
const maxPossibleCodes = BigInt(charset.length) ** BigInt(length)
|
||||
const requiredSpace = BigInt(batchCount + generatedNamespaceManualCount) * BigInt(MIN_RANDOM_SPACE_MULTIPLIER)
|
||||
if (maxPossibleCodes < requiredSpace) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '可用字符集空间过小或长度不足,容易导致生成碰撞和性能问题,请增大字符集或长度'
|
||||
})
|
||||
}
|
||||
|
||||
const makeRandom = () => {
|
||||
let code = prefix
|
||||
for (let i = 0; i < length; i++) {
|
||||
code += charset.charAt(crypto.randomInt(0, charset.length))
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
let attempts = 0
|
||||
const maxAttempts = batchCount * 100
|
||||
while (generatedCodeSet.size < batchCount && attempts < maxAttempts) {
|
||||
attempts++
|
||||
const next = makeRandom()
|
||||
if (!generatedCodeSet.has(next) && !manualCodeSet.has(next)) {
|
||||
generatedCodeSet.add(next)
|
||||
}
|
||||
}
|
||||
if (generatedCodeSet.size < batchCount) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '可用字符集过小或长度不足,无法生成足够数量的唯一点歌券,请增大字符集或长度'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const finalCodes = [...codes, ...generatedCodeSet]
|
||||
if (!finalCodes.length) {
|
||||
throw createError({ statusCode: 400, message: '请提供要创建的点歌券或生成数量' })
|
||||
}
|
||||
if (finalCodes.length > MAX_BATCH_COUNT) {
|
||||
throw createError({ statusCode: 400, message: `单次最多创建 ${MAX_BATCH_COUNT} 个点歌券` })
|
||||
}
|
||||
|
||||
const uniqueCodes = [...new Set(finalCodes)]
|
||||
const note = typeof validatedData.note === 'string' ? validatedData.note.trim() || null : null
|
||||
const inserts = uniqueCodes.map((code) => ({
|
||||
code,
|
||||
status: 'AVAILABLE' as const,
|
||||
note
|
||||
}))
|
||||
const createdCardCodes = await db.transaction(async (tx) => {
|
||||
const rows: any[] = []
|
||||
for (let i = 0; i < inserts.length; i += DB_CHUNK_SIZE) {
|
||||
const chunk = inserts.slice(i, i + DB_CHUNK_SIZE)
|
||||
const result = await tx.insert(cardCodes).values(chunk).onConflictDoNothing().returning()
|
||||
rows.push(...result)
|
||||
}
|
||||
return rows
|
||||
})
|
||||
|
||||
if (!createdCardCodes.length) {
|
||||
throw createError({ statusCode: 400, message: '这些点歌券已经存在,无需重复创建' })
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '点歌券创建成功',
|
||||
data: {
|
||||
cardCodes: createdCardCodes,
|
||||
created: createdCardCodes.length,
|
||||
skipped: uniqueCodes.length - createdCardCodes.length
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.statusCode) throw err
|
||||
if (err.name === 'ZodError') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `请求参数验证失败:${(err.issues || err.errors).map((issue: any) => issue.message).join(', ')}`
|
||||
})
|
||||
}
|
||||
console.error('开放API创建点歌券失败', err)
|
||||
throw createError({ statusCode: 500, message: '创建点歌券失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { handleOpenCardCodeDelete } from '~~/server/utils/card-code-delete-handler'
|
||||
|
||||
export default defineEventHandler(handleOpenCardCodeDelete)
|
||||
@@ -0,0 +1,243 @@
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { playTimes, schedules, songCollaborators, songs, users } from '~/drizzle/schema'
|
||||
import { and, asc, desc, eq, gte, inArray, like, lt, or, sql } from 'drizzle-orm'
|
||||
import { formatDateTime } from '~/utils/timeUtils'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const apiKey = event.context.apiKey
|
||||
|
||||
console.log(`[Schedules API] 接收到请求,API Key context: ${apiKey ? '存在' : '不存在'}`)
|
||||
|
||||
// API认证中间件已经验证了权限,这里只需要确保有API Key信息
|
||||
if (!apiKey) {
|
||||
console.log(`[Schedules API] API认证失败 - 缺少API Key context`)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'API认证失败'
|
||||
})
|
||||
}
|
||||
|
||||
const semester = (query.semester as string) || ''
|
||||
const date = (query.date as string) || ''
|
||||
const playTimeId = (query.playTimeId as string) || ''
|
||||
const search = (query.search as string) || ''
|
||||
const page = parseInt(query.page as string) || 1
|
||||
const limit = Math.min(parseInt(query.limit as string) || 20, 100) // 最大100条
|
||||
const sortBy = (query.sortBy as string) || 'playDate'
|
||||
const sortOrder = (query.sortOrder as string) || 'asc'
|
||||
|
||||
// 构建查询条件
|
||||
const conditions = [
|
||||
eq(schedules.isDraft, false) // 只查询已发布的排期
|
||||
]
|
||||
|
||||
if (semester) {
|
||||
conditions.push(eq(songs.semester, semester))
|
||||
}
|
||||
|
||||
if (date) {
|
||||
const targetDate = new Date(date)
|
||||
const startOfDay = new Date(
|
||||
targetDate.getFullYear(),
|
||||
targetDate.getMonth(),
|
||||
targetDate.getDate()
|
||||
)
|
||||
const endOfDay = new Date(
|
||||
targetDate.getFullYear(),
|
||||
targetDate.getMonth(),
|
||||
targetDate.getDate() + 1
|
||||
)
|
||||
|
||||
conditions.push(and(gte(schedules.playDate, startOfDay)!, lt(schedules.playDate, endOfDay)!)!)
|
||||
}
|
||||
|
||||
if (playTimeId) {
|
||||
conditions.push(eq(schedules.playTimeId, parseInt(playTimeId)))
|
||||
}
|
||||
|
||||
if (search) {
|
||||
conditions.push(or(like(songs.title, `%${search}%`)!, like(songs.artist, `%${search}%`)!)!)
|
||||
}
|
||||
|
||||
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
|
||||
|
||||
// 计算偏移量
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
// 查询排期数据
|
||||
const schedulesData = await db
|
||||
.select({
|
||||
id: schedules.id,
|
||||
playDate: schedules.playDate,
|
||||
createdAt: schedules.createdAt,
|
||||
updatedAt: schedules.updatedAt,
|
||||
song: {
|
||||
id: songs.id,
|
||||
title: songs.title,
|
||||
artist: songs.artist,
|
||||
cover: songs.cover,
|
||||
musicPlatform: songs.musicPlatform,
|
||||
musicId: songs.musicId,
|
||||
durationSeconds: songs.durationSeconds,
|
||||
played: songs.played,
|
||||
playedAt: songs.playedAt,
|
||||
createdAt: songs.createdAt,
|
||||
semester: songs.semester
|
||||
},
|
||||
requester: {
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
grade: users.grade,
|
||||
class: users.class
|
||||
},
|
||||
playTime: {
|
||||
id: playTimes.id,
|
||||
name: playTimes.name,
|
||||
startTime: playTimes.startTime,
|
||||
endTime: playTimes.endTime,
|
||||
enabled: playTimes.enabled
|
||||
}
|
||||
})
|
||||
.from(schedules)
|
||||
.innerJoin(songs, eq(schedules.songId, songs.id))
|
||||
.leftJoin(users, eq(songs.requesterId, users.id))
|
||||
.leftJoin(playTimes, eq(schedules.playTimeId, playTimes.id))
|
||||
.where(whereCondition)
|
||||
.orderBy(
|
||||
sortBy === 'playDate'
|
||||
? sortOrder === 'desc'
|
||||
? desc(schedules.playDate)
|
||||
: asc(schedules.playDate)
|
||||
: sortBy === 'createdAt'
|
||||
? sortOrder === 'desc'
|
||||
? desc(schedules.createdAt)
|
||||
: asc(schedules.createdAt)
|
||||
: sortBy === 'title'
|
||||
? sortOrder === 'desc'
|
||||
? desc(songs.title)
|
||||
: asc(songs.title)
|
||||
: asc(schedules.playDate)
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
// 获取总数
|
||||
const totalResult = await db
|
||||
.select({ count: sql`count(*)` })
|
||||
.from(schedules)
|
||||
.innerJoin(songs, eq(schedules.songId, songs.id))
|
||||
.leftJoin(users, eq(songs.requesterId, users.id))
|
||||
.where(whereCondition)
|
||||
|
||||
const total = Number(totalResult[0].count)
|
||||
|
||||
// 获取联合投稿人信息
|
||||
const songIds = schedulesData.map((s) => s.song.id)
|
||||
const collaboratorsMap = new Map()
|
||||
|
||||
if (songIds.length > 0) {
|
||||
const collaboratorsData = await db
|
||||
.select({
|
||||
songId: songCollaborators.songId,
|
||||
user: {
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
grade: users.grade,
|
||||
class: users.class
|
||||
}
|
||||
})
|
||||
.from(songCollaborators)
|
||||
.leftJoin(users, eq(songCollaborators.userId, users.id))
|
||||
.where(
|
||||
and(inArray(songCollaborators.songId, songIds), eq(songCollaborators.status, 'ACCEPTED'))
|
||||
)
|
||||
|
||||
collaboratorsData.forEach((c) => {
|
||||
if (!collaboratorsMap.has(c.songId)) {
|
||||
collaboratorsMap.set(c.songId, [])
|
||||
}
|
||||
if (c.user) {
|
||||
collaboratorsMap.get(c.songId).push(c.user)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化数据
|
||||
const formattedSchedules = schedulesData.map((schedule) => {
|
||||
const collaborators = collaboratorsMap.get(schedule.song.id) || []
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
playDate: schedule.playDate,
|
||||
playDateFormatted: formatDateTime(schedule.playDate),
|
||||
semester: schedule.song.semester,
|
||||
song: {
|
||||
id: schedule.song.id,
|
||||
title: schedule.song.title,
|
||||
artist: schedule.song.artist,
|
||||
cover: schedule.song.cover,
|
||||
musicPlatform: schedule.song.musicPlatform,
|
||||
musicId: schedule.song.musicId,
|
||||
durationSeconds: schedule.song.durationSeconds || null,
|
||||
played: schedule.song.played,
|
||||
playedAt: schedule.song.playedAt,
|
||||
requestedAt: formatDateTime(schedule.song.createdAt),
|
||||
collaborators: collaborators.map((c: any) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
grade: c.grade,
|
||||
class: c.class
|
||||
}))
|
||||
},
|
||||
requester: schedule.requester
|
||||
? {
|
||||
id: schedule.requester.id,
|
||||
name: schedule.requester.name,
|
||||
grade: schedule.requester.grade,
|
||||
class: schedule.requester.class
|
||||
}
|
||||
: null,
|
||||
playTime: schedule.playTime
|
||||
? {
|
||||
id: schedule.playTime.id,
|
||||
name: schedule.playTime.name,
|
||||
startTime: schedule.playTime.startTime,
|
||||
endTime: schedule.playTime.endTime,
|
||||
enabled: schedule.playTime.enabled
|
||||
}
|
||||
: null,
|
||||
createdAt: schedule.createdAt,
|
||||
updatedAt: schedule.updatedAt
|
||||
}
|
||||
})
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
data: {
|
||||
schedules: formattedSchedules,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error('[Open API] 获取排期列表失败:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '获取排期列表失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,282 @@
|
||||
import { createError, defineEventHandler, getQuery } from 'h3'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { playTimes, schedules, songs, users, votes } from '~/drizzle/schema'
|
||||
import { and, asc, count, desc, eq, inArray, like, or } from 'drizzle-orm'
|
||||
import { formatDateTime } from '~/utils/timeUtils'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const apiKey = event.context.apiKey
|
||||
|
||||
// API认证中间件已经验证了权限,这里只需要确保有API Key信息
|
||||
if (!apiKey) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'API认证失败'
|
||||
})
|
||||
}
|
||||
|
||||
const search = (query.search as string) || ''
|
||||
const semester = (query.semester as string) || ''
|
||||
const grade = (query.grade as string) || ''
|
||||
const played = query.played as string // 'true', 'false', 或空字符串
|
||||
const scheduled = query.scheduled as string // 'true', 'false', 或空字符串
|
||||
const page = parseInt(query.page as string) || 1
|
||||
const limit = Math.min(parseInt(query.limit as string) || 20, 100) // 最大100条
|
||||
const sortBy = (query.sortBy as string) || 'createdAt'
|
||||
const sortOrder = (query.sortOrder as string) || 'desc'
|
||||
|
||||
// 构建查询条件
|
||||
const conditions = []
|
||||
|
||||
if (search) {
|
||||
conditions.push(or(like(songs.title, `%${search}%`), like(songs.artist, `%${search}%`)))
|
||||
}
|
||||
|
||||
if (semester) {
|
||||
conditions.push(eq(songs.semester, semester))
|
||||
}
|
||||
|
||||
if (grade) {
|
||||
conditions.push(eq(users.grade, grade))
|
||||
}
|
||||
|
||||
if (played === 'true') {
|
||||
conditions.push(eq(songs.played, true))
|
||||
} else if (played === 'false') {
|
||||
conditions.push(eq(songs.played, false))
|
||||
}
|
||||
|
||||
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
|
||||
|
||||
// 计算偏移量
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
// 查询歌曲总数
|
||||
const totalResult = await db
|
||||
.select({ count: count() })
|
||||
.from(songs)
|
||||
.leftJoin(users, eq(songs.requesterId, users.id))
|
||||
.where(whereCondition)
|
||||
const total = totalResult[0]?.count || 0
|
||||
|
||||
// 获取歌曲数据
|
||||
const songsData = await db
|
||||
.select({
|
||||
id: songs.id,
|
||||
title: songs.title,
|
||||
artist: songs.artist,
|
||||
played: songs.played,
|
||||
playedAt: songs.playedAt,
|
||||
semester: songs.semester,
|
||||
createdAt: songs.createdAt,
|
||||
updatedAt: songs.updatedAt,
|
||||
cover: songs.cover,
|
||||
playUrl: songs.playUrl,
|
||||
musicPlatform: songs.musicPlatform,
|
||||
musicId: songs.musicId,
|
||||
durationSeconds: songs.durationSeconds,
|
||||
preferredPlayTimeId: songs.preferredPlayTimeId,
|
||||
requester: {
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
grade: users.grade,
|
||||
class: users.class
|
||||
}
|
||||
})
|
||||
.from(songs)
|
||||
.leftJoin(users, eq(songs.requesterId, users.id))
|
||||
.where(whereCondition)
|
||||
.orderBy(
|
||||
sortBy === 'createdAt'
|
||||
? sortOrder === 'desc'
|
||||
? desc(songs.createdAt)
|
||||
: asc(songs.createdAt)
|
||||
: sortBy === 'title'
|
||||
? sortOrder === 'desc'
|
||||
? desc(songs.title)
|
||||
: asc(songs.title)
|
||||
: sortBy === 'artist'
|
||||
? sortOrder === 'desc'
|
||||
? desc(songs.artist)
|
||||
: asc(songs.artist)
|
||||
: sortBy === 'playedAt'
|
||||
? sortOrder === 'desc'
|
||||
? desc(songs.playedAt)
|
||||
: asc(songs.playedAt)
|
||||
: desc(songs.createdAt)
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
// 获取每首歌的投票数
|
||||
const songIds = songsData.map((song) => song.id)
|
||||
const voteCountsQuery =
|
||||
songIds.length > 0
|
||||
? await db
|
||||
.select({
|
||||
songId: votes.songId,
|
||||
count: count(votes.id)
|
||||
})
|
||||
.from(votes)
|
||||
.where(inArray(votes.songId, songIds))
|
||||
.groupBy(votes.songId)
|
||||
: []
|
||||
|
||||
const voteCounts = new Map(voteCountsQuery.map((v) => [v.songId, v.count]))
|
||||
|
||||
// 获取每首歌的排期状态
|
||||
// 只查询已发布的排期,草稿不算作已排期
|
||||
const schedulesQuery =
|
||||
songIds.length > 0
|
||||
? await db
|
||||
.select({
|
||||
songId: schedules.songId
|
||||
})
|
||||
.from(schedules)
|
||||
.where(and(inArray(schedules.songId, songIds), eq(schedules.isDraft, false)))
|
||||
: []
|
||||
|
||||
const scheduledSongs = new Set(schedulesQuery.map((s) => s.songId))
|
||||
|
||||
// 获取期望播放时段信息
|
||||
const playTimesQuery = await db.select().from(playTimes)
|
||||
|
||||
const playTimesMap = new Map(playTimesQuery.map((pt) => [pt.id, pt]))
|
||||
|
||||
// 获取所有用户的姓名列表,用于检测同名用户
|
||||
const allUsers = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
grade: users.grade,
|
||||
class: users.class
|
||||
})
|
||||
.from(users)
|
||||
|
||||
// 创建姓名到用户数组的映射
|
||||
const nameToUsers = new Map()
|
||||
allUsers.forEach((u) => {
|
||||
if (u.name) {
|
||||
if (!nameToUsers.has(u.name)) {
|
||||
nameToUsers.set(u.name, [])
|
||||
}
|
||||
nameToUsers.get(u.name).push(u)
|
||||
}
|
||||
})
|
||||
|
||||
// 转换数据格式
|
||||
let formattedSongs = songsData.map((song) => {
|
||||
// 处理投稿人姓名,如果是同名用户则添加后缀
|
||||
let requesterName = song.requester?.name || '未知用户'
|
||||
|
||||
// 检查是否有同名用户
|
||||
const sameNameUsers = nameToUsers.get(requesterName)
|
||||
if (sameNameUsers && sameNameUsers.length > 1) {
|
||||
const requesterWithGradeClass = song.requester
|
||||
|
||||
// 如果有年级信息,则添加年级后缀
|
||||
if (requesterWithGradeClass?.grade) {
|
||||
// 检查同一个年级是否有同名
|
||||
const sameGradeUsers = sameNameUsers.filter(
|
||||
(u: { grade?: string; class?: string }) => u.grade === requesterWithGradeClass.grade
|
||||
)
|
||||
|
||||
if (sameGradeUsers.length > 1 && requesterWithGradeClass.class) {
|
||||
// 同一个年级有同名,添加班级后缀
|
||||
requesterName = `${requesterName}(${requesterWithGradeClass.grade} ${requesterWithGradeClass.class})`
|
||||
} else {
|
||||
// 只添加年级后缀
|
||||
requesterName = `${requesterName}(${requesterWithGradeClass.grade})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建歌曲对象
|
||||
const songObject: any = {
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
requester: requesterName,
|
||||
requesterId: song.requester?.id,
|
||||
voteCount: voteCounts.get(song.id) || 0,
|
||||
played: song.played,
|
||||
playedAt: song.playedAt,
|
||||
playedAtFormatted: song.playedAt ? formatDateTime(song.playedAt) : null,
|
||||
semester: song.semester,
|
||||
createdAt: song.createdAt,
|
||||
updatedAt: song.updatedAt,
|
||||
requestedAt: formatDateTime(song.createdAt),
|
||||
scheduled: scheduledSongs.has(song.id),
|
||||
cover: song.cover || null,
|
||||
musicPlatform: song.musicPlatform || null,
|
||||
musicId: song.musicId || null,
|
||||
durationSeconds: song.durationSeconds || null,
|
||||
playUrl: song.playUrl || null,
|
||||
preferredPlayTimeId: song.preferredPlayTimeId
|
||||
}
|
||||
|
||||
// 添加期望播放时段相关字段
|
||||
if (song.preferredPlayTimeId) {
|
||||
const preferredPlayTime = playTimesMap.get(song.preferredPlayTimeId)
|
||||
if (preferredPlayTime) {
|
||||
songObject.preferredPlayTime = {
|
||||
id: preferredPlayTime.id,
|
||||
name: preferredPlayTime.name,
|
||||
startTime: preferredPlayTime.startTime,
|
||||
endTime: preferredPlayTime.endTime,
|
||||
enabled: preferredPlayTime.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return songObject
|
||||
})
|
||||
|
||||
// 如果需要按排期状态筛选
|
||||
if (scheduled === 'true') {
|
||||
formattedSongs = formattedSongs.filter((song) => song.scheduled)
|
||||
} else if (scheduled === 'false') {
|
||||
formattedSongs = formattedSongs.filter((song) => !song.scheduled)
|
||||
}
|
||||
|
||||
// 如果按投票数排序,需要重新排序
|
||||
if (sortBy === 'votes') {
|
||||
formattedSongs.sort((a, b) => {
|
||||
const diff = sortOrder === 'desc' ? b.voteCount - a.voteCount : a.voteCount - b.voteCount
|
||||
if (diff === 0) {
|
||||
// 投票数相同时,按提交时间排序(较早提交的优先)
|
||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
}
|
||||
return diff
|
||||
})
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
data: {
|
||||
songs: formattedSongs,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error('[Open API] 获取歌曲列表失败:', error)
|
||||
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '获取歌曲列表失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { createSongPlayedNotification } from '~~/server/services/notificationService'
|
||||
import { songs, songReplayRequests } from '~/drizzle/schema'
|
||||
import { eq, and, inArray } from 'drizzle-orm'
|
||||
import { getBeijingTime } from '~/utils/timeUtils'
|
||||
import { getClientIP } from '~~/server/utils/ip-utils'
|
||||
import { restoreReplayRequestsToPending } from '~~/server/utils/scheduleReplayBinding'
|
||||
import { z } from 'zod'
|
||||
|
||||
const markPlayedSchema = z.object({
|
||||
songId: z.number().optional(),
|
||||
songIds: z.array(z.number()).max(100, '批量操作单次最多支持 100 首歌曲').optional(),
|
||||
unmark: z.boolean().optional()
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event)
|
||||
const validatedDataResult = markPlayedSchema.safeParse(body)
|
||||
|
||||
if (!validatedDataResult.success) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: validatedDataResult.error.issues[0]?.message || '请求参数无效'
|
||||
})
|
||||
}
|
||||
|
||||
const validatedData = validatedDataResult.data
|
||||
|
||||
const songIds = Array.from(
|
||||
new Set([
|
||||
...(validatedData.songId !== undefined ? [validatedData.songId] : []),
|
||||
...(validatedData.songIds || [])
|
||||
])
|
||||
)
|
||||
|
||||
if (songIds.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '歌曲ID列表不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
// 撤回已播放
|
||||
const isUnmark = validatedData.unmark === true
|
||||
|
||||
// 过滤需要更新的歌曲
|
||||
const condition = !isUnmark
|
||||
? and(inArray(songs.id, songIds), eq(songs.played, false))
|
||||
: and(inArray(songs.id, songIds), eq(songs.played, true))
|
||||
|
||||
const { updatedSongsResult, updatedSongIds } = await db.transaction(async (tx) => {
|
||||
const updatedSongsResult = await tx
|
||||
.update(songs)
|
||||
.set({
|
||||
played: !isUnmark,
|
||||
playedAt: isUnmark ? null : getBeijingTime()
|
||||
})
|
||||
.where(condition)
|
||||
.returning({ id: songs.id })
|
||||
|
||||
const updatedSongIds = updatedSongsResult.map((s) => s.id)
|
||||
|
||||
if (updatedSongIds.length > 0) {
|
||||
if (!isUnmark) {
|
||||
// 标记已播放:履行全部待处理的重播申请
|
||||
const updatedRequests = await tx
|
||||
.update(songReplayRequests)
|
||||
.set({
|
||||
status: 'FULFILLED',
|
||||
updatedAt: getBeijingTime()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
inArray(songReplayRequests.songId, updatedSongIds),
|
||||
eq(songReplayRequests.status, 'PENDING')
|
||||
)
|
||||
)
|
||||
.returning({ id: songReplayRequests.id })
|
||||
|
||||
if (updatedRequests.length > 0) {
|
||||
console.log(`将 ${updatedRequests.length} 个重播申请状态更新为 FULFILLED(歌曲已播放)`)
|
||||
}
|
||||
} else {
|
||||
// 撤回已播放:每人仅恢复最新一条,避免违反部分唯一索引
|
||||
const restoredCount = await restoreReplayRequestsToPending({
|
||||
tx,
|
||||
songIds: updatedSongIds,
|
||||
at: getBeijingTime()
|
||||
})
|
||||
|
||||
if (restoredCount > 0) {
|
||||
console.log(`将 ${restoredCount} 个重播申请状态恢复为 PENDING(撤回已播放)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { updatedSongsResult, updatedSongIds }
|
||||
})
|
||||
|
||||
// 异步发送通知
|
||||
if (!isUnmark && updatedSongIds.length > 0) {
|
||||
event.waitUntil(
|
||||
Promise.allSettled(
|
||||
updatedSongIds.map((songId) =>
|
||||
createSongPlayedNotification(songId).catch((err) => {
|
||||
console.error(`发送歌曲(${songId})已播放通知失败:`, err)
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
message: isUnmark ? '歌曲已成功撤回已播放状态' : '歌曲已成功标记为已播放',
|
||||
count: updatedSongsResult.length,
|
||||
updatedSongIds: updatedSongIds
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { db, users } from '~/drizzle/db'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { requestSongForUser } from '~~/server/services/songRequestService'
|
||||
|
||||
const ALLOWED_FIELDS = [
|
||||
'title',
|
||||
'artist',
|
||||
'cover',
|
||||
'musicPlatform',
|
||||
'musicId',
|
||||
'bilibiliCid',
|
||||
'bilibiliPage',
|
||||
'playUrl',
|
||||
'submissionNote',
|
||||
'submissionNotePublic',
|
||||
'preferredPlayTimeId',
|
||||
'cardCode',
|
||||
'collaborators'
|
||||
]
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const apiKey = event.context.apiKey
|
||||
|
||||
if (!apiKey) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'API认证失败'
|
||||
})
|
||||
}
|
||||
|
||||
const userResult = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
role: users.role,
|
||||
status: users.status
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, apiKey.createdByUserId))
|
||||
.limit(1)
|
||||
const user = userResult[0]
|
||||
|
||||
if (!user || user.status !== 'active') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '令牌所属用户不可用'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const payload = ALLOWED_FIELDS.reduce((result: Record<string, any>, field) => {
|
||||
if (Object.prototype.hasOwnProperty.call(body || {}, field)) {
|
||||
result[field] = body[field]
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
|
||||
return await requestSongForUser(event, user, payload)
|
||||
})
|
||||
Reference in New Issue
Block a user