feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -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: '创建点歌券失败' })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user