feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { createError, defineEventHandler, getRouterParam, readBody } from 'h3'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { cardCodes, cardCodeRedeemLogs } from '~/drizzle/schema'
|
||||
import { CARD_CODE_STATUSES } from '../../../card-codes/statuses'
|
||||
|
||||
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 id = Number(getRouterParam(event, 'id'))
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
throw createError({ statusCode: 400, message: '点歌券ID无效' })
|
||||
}
|
||||
|
||||
const body = await readBody(event) ?? {}
|
||||
const status = typeof body.status === 'string' ? body.status.trim().toUpperCase() : ''
|
||||
const note = typeof body.note === 'string' ? body.note.trim() || null : undefined
|
||||
|
||||
if (!status && note === undefined) {
|
||||
throw createError({ statusCode: 400, message: '没有需要更新的字段' })
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await db.transaction(async (tx) => {
|
||||
// 获取当前状态
|
||||
const rows = await tx.select().from(cardCodes).where(eq(cardCodes.id, id)).limit(1)
|
||||
const current = rows[0]
|
||||
if (!current) throw createError({ statusCode: 404, message: '点歌券未找到' })
|
||||
|
||||
const newValues: any = { updatedAt: new Date() }
|
||||
const now = new Date()
|
||||
|
||||
if (status) {
|
||||
if (!CARD_CODE_STATUSES.includes(status as any)) {
|
||||
throw createError({ statusCode: 400, message: '不支持的状态值' })
|
||||
}
|
||||
if (status === 'REDEEMED') {
|
||||
newValues.status = 'REDEEMED'
|
||||
newValues.redeemedBy = user.id
|
||||
newValues.redeemedAt = now
|
||||
// 清理锁定信息
|
||||
newValues.lockedBy = null
|
||||
newValues.lockedAt = null
|
||||
} else if (status === 'AVAILABLE') {
|
||||
newValues.status = 'AVAILABLE'
|
||||
newValues.lockedBy = null
|
||||
newValues.lockedAt = null
|
||||
newValues.redeemedBy = null
|
||||
newValues.redeemedAt = null
|
||||
} else if (status === 'LOCKED') {
|
||||
newValues.status = 'LOCKED'
|
||||
newValues.lockedBy = user.id
|
||||
newValues.lockedAt = now
|
||||
newValues.redeemedBy = null
|
||||
newValues.redeemedAt = null
|
||||
} else if (status === 'INVALID') {
|
||||
newValues.status = 'INVALID'
|
||||
// 作废时清理锁定与核销信息,与批量更新行为一致
|
||||
newValues.lockedBy = null
|
||||
newValues.lockedAt = null
|
||||
newValues.redeemedBy = null
|
||||
newValues.redeemedAt = null
|
||||
}
|
||||
}
|
||||
|
||||
if (note !== undefined) newValues.note = note
|
||||
|
||||
const res = await tx.update(cardCodes).set(newValues).where(eq(cardCodes.id, id)).returning()
|
||||
const newRow = res[0]
|
||||
|
||||
if (status === 'REDEEMED' && current.status !== status) {
|
||||
await tx.insert(cardCodeRedeemLogs).values({
|
||||
cardCodeId: id,
|
||||
codeSnapshot: newRow.code,
|
||||
redeemedBy: user.id,
|
||||
redeemedAt: newRow.redeemedAt || now,
|
||||
source: 'ADMIN_MANUAL',
|
||||
songId: null
|
||||
})
|
||||
}
|
||||
|
||||
return newRow
|
||||
})
|
||||
|
||||
return { success: true, data: updated }
|
||||
} catch (err: any) {
|
||||
console.error('更新点歌券失败:', err)
|
||||
throw createError({ statusCode: err.statusCode || 500, message: err.message || '更新失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodes } from '~/drizzle/schema'
|
||||
import { inArray } from 'drizzle-orm'
|
||||
|
||||
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 body = await readBody(event) ?? {}
|
||||
|
||||
const rawCodes = Array.isArray(body.codes) ? body.codes : body.codes ? [body.codes] : []
|
||||
const codes = rawCodes.map((c: string) => String(c).trim().toUpperCase()).filter(Boolean)
|
||||
|
||||
const MAX_BATCH_COUNT = 10000
|
||||
const parsedCount = Number(body.count)
|
||||
const requestedBatchCount = Number.isInteger(parsedCount) ? parsedCount : 0
|
||||
if (requestedBatchCount > MAX_BATCH_COUNT) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `单次最多生成 ${MAX_BATCH_COUNT} 个点歌券`
|
||||
})
|
||||
}
|
||||
const batchCount = requestedBatchCount
|
||||
const prefix = typeof body.prefix === 'string' ? body.prefix.trim().toUpperCase() : ''
|
||||
const parsedLength = Number(body.length)
|
||||
const length = Number.isInteger(parsedLength) ? parsedLength : 12
|
||||
if (length > 64) {
|
||||
throw createError({ statusCode: 400, message: '点歌券随机长度不能超过 64 位' })
|
||||
}
|
||||
const charsetInput = typeof body.charset === 'string' && body.charset.trim() ? body.charset.trim().toUpperCase() : 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
const charset = [...new Set(charsetInput.split(''))].join('')
|
||||
if (!charset) {
|
||||
throw createError({ statusCode: 400, message: '字符集不能为空' })
|
||||
}
|
||||
|
||||
const manualCodeSet = new Set(codes)
|
||||
const generatedCodeSet = new Set<string>()
|
||||
if (batchCount > 0) {
|
||||
const actualLength = Math.max(4, length)
|
||||
const maxPossibleCodes = charset.length ** actualLength
|
||||
if (maxPossibleCodes < batchCount) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '可用字符集过小或长度不足,无法生成足够数量的唯一点歌券,请增大字符集或长度'
|
||||
})
|
||||
}
|
||||
|
||||
const makeRandom = () => {
|
||||
let code = prefix
|
||||
for (let i = 0; i < actualLength; i++) {
|
||||
code += charset.charAt(Math.floor(Math.random() * 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} 个点歌券`
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const uniqueCodes = [...new Set(finalCodes)]
|
||||
const existingRows = await db
|
||||
.select({ code: cardCodes.code })
|
||||
.from(cardCodes)
|
||||
.where(inArray(cardCodes.code, uniqueCodes))
|
||||
const existingCodes = new Set(existingRows.map((row) => row.code))
|
||||
const insertCodes = uniqueCodes.filter((code) => !existingCodes.has(code))
|
||||
|
||||
if (!insertCodes.length) {
|
||||
throw createError({ statusCode: 400, message: '这些点歌券已经存在,无需重复创建' })
|
||||
}
|
||||
|
||||
const inserts = insertCodes.map((c: string) => ({
|
||||
code: c,
|
||||
status: 'AVAILABLE',
|
||||
note: typeof body.note === 'string' ? body.note.trim() || null : null
|
||||
}))
|
||||
const res = await db.insert(cardCodes).values(inserts).returning()
|
||||
return {
|
||||
success: true,
|
||||
data: res,
|
||||
skipped: uniqueCodes.length - insertCodes.length
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('创建点歌券失败', err)
|
||||
throw createError({ statusCode: err.statusCode || 500, message: err.message || '创建点歌券失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createError, defineEventHandler, readBody } from 'h3'
|
||||
import { deleteCardCodesByIds } from '~~/server/services/cardCodeDeleteService'
|
||||
|
||||
const parseIds = (body: any): number[] => {
|
||||
const rawIds = Array.isArray(body?.ids) ? body.ids : body?.id !== undefined ? [body.id] : []
|
||||
return Array.from(new Set(
|
||||
rawIds
|
||||
.filter((id: any) => (typeof id === 'number' || typeof id === 'string') && id !== '')
|
||||
.map((id: any) => Number(id))
|
||||
.filter((id: number) => Number.isInteger(id) && id > 0)
|
||||
))
|
||||
}
|
||||
|
||||
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 body = await readBody(event).catch(() => ({})) ?? {}
|
||||
const ids = parseIds(body)
|
||||
if (!ids.length) {
|
||||
throw createError({ statusCode: 400, message: '缺少有效点歌券ID' })
|
||||
}
|
||||
if (ids.length > 500) {
|
||||
throw createError({ statusCode: 400, message: '单次最多删除 500 个点歌券' })
|
||||
}
|
||||
|
||||
try {
|
||||
const deletedRows = await deleteCardCodesByIds(ids)
|
||||
return {
|
||||
success: true,
|
||||
message: '点歌券删除成功',
|
||||
data: {
|
||||
cardCodes: deletedRows,
|
||||
deleted: deletedRows.length,
|
||||
requested: ids.length
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.statusCode) throw err
|
||||
console.error('删除点歌券失败', err)
|
||||
throw createError({ statusCode: 500, message: '删除点歌券失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodes } from '~/drizzle/schema'
|
||||
import { and, desc, eq, ilike, or, inArray } from 'drizzle-orm'
|
||||
import { setHeader } from 'h3'
|
||||
|
||||
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: '权限不足' })
|
||||
}
|
||||
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const idsParam = typeof query.ids === 'string' ? query.ids.trim() : ''
|
||||
const status = typeof query.status === 'string' ? query.status.trim().toUpperCase() : ''
|
||||
const keyword = typeof query.q === 'string' ? query.q.trim() : ''
|
||||
|
||||
const conditions: 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 rows
|
||||
if (idsParam) {
|
||||
const ids = idsParam.split(',').map((s) => Number(s)).filter((id) => Number.isInteger(id) && id > 0)
|
||||
if (ids.length === 0) {
|
||||
throw createError({ statusCode: 400, message: '无效的点歌券ID' })
|
||||
}
|
||||
rows = await db.select().from(cardCodes).where(inArray(cardCodes.id, ids)).orderBy(desc(cardCodes.createdAt))
|
||||
} else {
|
||||
let qb = db.select().from(cardCodes)
|
||||
if (conditions.length) qb = qb.where(and(...conditions))
|
||||
rows = await qb.orderBy(desc(cardCodes.createdAt))
|
||||
}
|
||||
|
||||
const statusLabelMap: Record<string, string> = {
|
||||
AVAILABLE: '可用',
|
||||
LOCKED: '已锁定',
|
||||
REDEEMED: '已核销',
|
||||
INVALID: '已作废'
|
||||
}
|
||||
const header = ['ID', '点歌券', '状态', '备注', '创建时间', '更新时间', '锁定用户ID', '锁定时间', '核销用户ID', '核销时间']
|
||||
const csvRows = [header.join(',')]
|
||||
for (const r of rows) {
|
||||
const esc = (v: any) => {
|
||||
if (v === null || typeof v === 'undefined') return ''
|
||||
let s = v instanceof Date ? v.toISOString() : String(v)
|
||||
if (/^[=+\-@]/.test(s)) {
|
||||
s = `\t${s}`
|
||||
}
|
||||
return s.includes(',') || s.includes('\n') || s.includes('"') ? '"' + s.replace(/"/g, '""') + '"' : s
|
||||
}
|
||||
csvRows.push([
|
||||
esc(r.id),
|
||||
esc(r.code),
|
||||
esc(statusLabelMap[r.status] || r.status),
|
||||
esc(r.note),
|
||||
esc(r.createdAt),
|
||||
esc(r.updatedAt),
|
||||
esc(r.lockedBy),
|
||||
esc(r.lockedAt),
|
||||
esc(r.redeemedBy),
|
||||
esc(r.redeemedAt)
|
||||
].join(','))
|
||||
}
|
||||
|
||||
const csv = '\ufeff' + csvRows.join('\n')
|
||||
|
||||
setHeader(event, 'Content-Type', 'text/csv; charset=utf-8')
|
||||
setHeader(event, 'Content-Disposition', 'attachment; filename="card-codes.csv"')
|
||||
return csv
|
||||
} catch (err: any) {
|
||||
console.error('导出点歌券失败', err)
|
||||
throw createError({ statusCode: err.statusCode || 500, message: err.message || '导出点歌券失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodes } from '~/drizzle/schema'
|
||||
import { and, count, desc, eq, ilike, or } from 'drizzle-orm'
|
||||
|
||||
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: '权限不足' })
|
||||
}
|
||||
|
||||
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 page = Math.max(1, Number.parseInt(String(query.page || '1'), 10) || 1)
|
||||
const limitInput = Number.parseInt(String(query.limit || '10'), 10) || 10
|
||||
const limit = Math.min(100, Math.max(1, limitInput))
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
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 [result, totalResult, statsRows] = await Promise.all([
|
||||
queryBuilder.orderBy(desc(cardCodes.createdAt)).limit(limit).offset(offset),
|
||||
countQueryBuilder,
|
||||
db.select({ status: cardCodes.status, count: count() }).from(cardCodes).groupBy(cardCodes.status)
|
||||
])
|
||||
|
||||
const stats = { total: 0, available: 0, locked: 0, redeemed: 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
|
||||
}
|
||||
|
||||
const total = Number(totalResult[0]?.count || 0)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.max(1, Math.ceil(total / limit))
|
||||
},
|
||||
stats
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取点歌券列表失败', err)
|
||||
throw createError({ statusCode: 500, message: '获取点歌券列表失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodeRedeemLogs, cardCodes, songs, users } from '~/drizzle/schema'
|
||||
import { and, desc, eq, gte, ilike, lte, or } from 'drizzle-orm'
|
||||
|
||||
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: '权限不足' })
|
||||
}
|
||||
|
||||
try {
|
||||
const query = getQuery(event)
|
||||
const limitParam = Number(query.limit)
|
||||
const limit = Number.isInteger(limitParam) ? Math.min(Math.max(limitParam, 1), 200) : 50
|
||||
|
||||
const keyword = typeof query.q === 'string' ? query.q.trim() : ''
|
||||
const source = typeof query.source === 'string' ? query.source.trim().toUpperCase() : ''
|
||||
const redeemer = typeof query.redeemer === 'string' ? query.redeemer.trim() : ''
|
||||
const cardCode = typeof query.cardCode === 'string' ? query.cardCode.trim() : ''
|
||||
const songKeyword = typeof query.song === 'string' ? query.song.trim() : ''
|
||||
const startDate = typeof query.startDate === 'string' ? query.startDate.trim() : ''
|
||||
const endDate = typeof query.endDate === 'string' ? query.endDate.trim() : ''
|
||||
|
||||
const conditions: any[] = []
|
||||
|
||||
if (source) {
|
||||
conditions.push(eq(cardCodeRedeemLogs.source, source as any))
|
||||
}
|
||||
|
||||
if (cardCode) {
|
||||
conditions.push(or(ilike(cardCodes.code, `%${cardCode}%`), ilike(cardCodeRedeemLogs.codeSnapshot, `%${cardCode}%`)))
|
||||
}
|
||||
|
||||
if (redeemer) {
|
||||
conditions.push(or(ilike(users.name, `%${redeemer}%`), ilike(users.username, `%${redeemer}%`)))
|
||||
}
|
||||
|
||||
if (songKeyword) {
|
||||
conditions.push(or(ilike(songs.title, `%${songKeyword}%`), ilike(songs.artist, `%${songKeyword}%`)))
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(cardCodes.code, `%${keyword}%`),
|
||||
ilike(cardCodeRedeemLogs.codeSnapshot, `%${keyword}%`),
|
||||
ilike(users.name, `%${keyword}%`),
|
||||
ilike(users.username, `%${keyword}%`),
|
||||
ilike(songs.title, `%${keyword}%`),
|
||||
ilike(songs.artist, `%${keyword}%`),
|
||||
ilike(cardCodeRedeemLogs.source, `%${keyword}%`)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const startDateValue = startDate ? new Date(startDate) : null
|
||||
if (startDateValue && !Number.isNaN(startDateValue.getTime())) {
|
||||
conditions.push(gte(cardCodeRedeemLogs.redeemedAt, startDateValue))
|
||||
}
|
||||
|
||||
const endDateValue = endDate ? new Date(endDate) : null
|
||||
if (endDateValue && !Number.isNaN(endDateValue.getTime())) {
|
||||
const adjustedEndDate = new Date(endDateValue)
|
||||
adjustedEndDate.setHours(23, 59, 59, 999)
|
||||
conditions.push(lte(cardCodeRedeemLogs.redeemedAt, adjustedEndDate))
|
||||
}
|
||||
|
||||
let queryBuilder = db
|
||||
.select({
|
||||
id: cardCodeRedeemLogs.id,
|
||||
cardCodeId: cardCodeRedeemLogs.cardCodeId,
|
||||
codeSnapshot: cardCodeRedeemLogs.codeSnapshot,
|
||||
redeemedAt: cardCodeRedeemLogs.redeemedAt,
|
||||
source: cardCodeRedeemLogs.source,
|
||||
songId: cardCodeRedeemLogs.songId,
|
||||
currentCode: cardCodes.code,
|
||||
songTitle: songs.title,
|
||||
songArtist: songs.artist,
|
||||
redeemerId: users.id,
|
||||
redeemerName: users.name,
|
||||
redeemerUsername: users.username
|
||||
})
|
||||
.from(cardCodeRedeemLogs)
|
||||
.leftJoin(users, eq(cardCodeRedeemLogs.redeemedBy, users.id))
|
||||
.leftJoin(cardCodes, eq(cardCodeRedeemLogs.cardCodeId, cardCodes.id))
|
||||
.leftJoin(songs, eq(cardCodeRedeemLogs.songId, songs.id))
|
||||
|
||||
if (conditions.length > 0) {
|
||||
queryBuilder = queryBuilder.where(and(...conditions))
|
||||
}
|
||||
|
||||
const result = await queryBuilder
|
||||
.orderBy(desc(cardCodeRedeemLogs.redeemedAt), desc(cardCodeRedeemLogs.id))
|
||||
.limit(limit)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.map((item) => ({
|
||||
id: item.id,
|
||||
cardCodeId: item.cardCodeId,
|
||||
code: item.currentCode || item.codeSnapshot,
|
||||
codeSnapshot: item.codeSnapshot,
|
||||
redeemedAt: item.redeemedAt,
|
||||
source: item.source,
|
||||
song: item.songId
|
||||
? {
|
||||
id: item.songId,
|
||||
title: item.songTitle || '未知歌曲',
|
||||
artist: item.songArtist || ''
|
||||
}
|
||||
: null,
|
||||
redeemer: {
|
||||
id: item.redeemerId,
|
||||
name: item.redeemerName || '未知用户',
|
||||
username: item.redeemerUsername || 'unknown'
|
||||
}
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取点歌券兑换日志失败', err)
|
||||
throw createError({ statusCode: 500, message: '获取点歌券兑换日志失败' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import { db } from '~/drizzle/db'
|
||||
import { cardCodes, cardCodeRedeemLogs } from '~/drizzle/schema'
|
||||
import { inArray, eq } from 'drizzle-orm'
|
||||
import { CARD_CODE_STATUSES } from '../../../card-codes/statuses'
|
||||
|
||||
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 body = await readBody(event) ?? {}
|
||||
const ids = Array.isArray(body.ids) ? body.ids : body.id ? [body.id] : []
|
||||
const status = typeof body.status === 'string' ? body.status.trim().toUpperCase() : ''
|
||||
if (!ids.length) {
|
||||
throw createError({ statusCode: 400, message: '缺少点歌券ID' })
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedIds = ids.map((id: any) => Number(id)).filter((id: number) => Number.isInteger(id) && id > 0)
|
||||
if (!normalizedIds.length) {
|
||||
throw createError({ statusCode: 400, message: '点歌券ID无效' })
|
||||
}
|
||||
|
||||
const beforeRows = await db
|
||||
.select({
|
||||
id: cardCodes.id,
|
||||
code: cardCodes.code,
|
||||
status: cardCodes.status
|
||||
})
|
||||
.from(cardCodes)
|
||||
.where(inArray(cardCodes.id, normalizedIds))
|
||||
|
||||
const beforeMap = new Map(beforeRows.map((row) => [row.id, row]))
|
||||
|
||||
const updateObj: any = {}
|
||||
if (status) {
|
||||
if (!CARD_CODE_STATUSES.includes(status as any)) {
|
||||
throw createError({ statusCode: 400, message: '不支持的状态值' })
|
||||
}
|
||||
updateObj.status = status
|
||||
}
|
||||
if (status === 'REDEEMED') {
|
||||
updateObj.redeemedBy = user.id
|
||||
updateObj.redeemedAt = new Date()
|
||||
updateObj.lockedBy = null
|
||||
updateObj.lockedAt = null
|
||||
}
|
||||
if (status === 'LOCKED') {
|
||||
updateObj.lockedBy = user.id
|
||||
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 (status === 'INVALID') {
|
||||
updateObj.lockedBy = null
|
||||
updateObj.lockedAt = null
|
||||
updateObj.redeemedBy = null
|
||||
updateObj.redeemedAt = null
|
||||
}
|
||||
if (typeof body.note !== 'undefined') {
|
||||
updateObj.note = typeof body.note === 'string' ? body.note.trim() || null : body.note
|
||||
}
|
||||
if (Object.keys(updateObj).length === 0) {
|
||||
throw createError({ statusCode: 400, message: '没有需要更新的字段' })
|
||||
}
|
||||
updateObj.updatedAt = new Date()
|
||||
|
||||
const res = await db.transaction(async (tx) => {
|
||||
const updatedRows = await tx.update(cardCodes).set(updateObj).where(inArray(cardCodes.id, normalizedIds)).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: user.id,
|
||||
redeemedAt: row.redeemedAt || new Date(),
|
||||
source: 'ADMIN_MANUAL',
|
||||
songId: null
|
||||
}))
|
||||
|
||||
if (logsToInsert.length > 0) {
|
||||
await tx.insert(cardCodeRedeemLogs).values(logsToInsert)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedRows
|
||||
})
|
||||
|
||||
return { success: true, data: res }
|
||||
} catch (err: any) {
|
||||
console.error('更新点歌券失败', err)
|
||||
throw createError({ statusCode: err.statusCode || 500, message: err.message || '更新点歌券失败' })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user