feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
import { and, eq, inArray } from 'drizzle-orm'
import { createError } from 'h3'
import { cardCodeRedeemLogs, cardCodes } from '~/drizzle/schema'
type CardCodeTransitionContext = {
songId: number
cardCodeId: number | null | undefined
operatorId: number
at?: Date
}
const getTransitionTime = (at?: Date) => at || new Date()
const readCardCode = async (tx: any, cardCodeId: number) => {
const rows = await tx.select().from(cardCodes).where(eq(cardCodes.id, cardCodeId)).limit(1)
return rows[0] || null
}
export const redeemCardCodeForSchedule = async (
tx: any,
{ songId, cardCodeId, operatorId, at }: CardCodeTransitionContext
) => {
if (!cardCodeId) return { changed: false, reason: 'NO_CARD_CODE' }
const redeemedAt = getTransitionTime(at)
const redeemResult = await tx
.update(cardCodes)
.set({
status: 'REDEEMED',
redeemedBy: operatorId,
redeemedAt,
lockedBy: null,
lockedAt: null,
updatedAt: redeemedAt
})
.where(and(eq(cardCodes.id, cardCodeId), inArray(cardCodes.status, ['AVAILABLE', 'LOCKED'])))
.returning({ id: cardCodes.id, code: cardCodes.code, redeemedAt: cardCodes.redeemedAt })
if (redeemResult.length > 0) {
const redeemedCardCode = redeemResult[0]
await tx.insert(cardCodeRedeemLogs).values({
cardCodeId: redeemedCardCode.id,
codeSnapshot: redeemedCardCode.code,
redeemedBy: operatorId,
redeemedAt: redeemedCardCode.redeemedAt || redeemedAt,
source: 'SCHEDULE_AUTO',
songId
})
return { changed: true, status: 'REDEEMED' }
}
const current = await readCardCode(tx, cardCodeId)
if (!current) {
throw createError({ statusCode: 400, message: '关联的点歌券不存在,无法发布排期' })
}
if (current.status === 'REDEEMED') {
const existingLogs = await tx
.select({ id: cardCodeRedeemLogs.id })
.from(cardCodeRedeemLogs)
.where(
and(
eq(cardCodeRedeemLogs.cardCodeId, cardCodeId),
eq(cardCodeRedeemLogs.songId, songId),
eq(cardCodeRedeemLogs.source, 'SCHEDULE_AUTO')
)
)
.limit(1)
if (existingLogs.length > 0) {
return { changed: false, status: 'REDEEMED' }
}
throw createError({
statusCode: 409,
message: '该点歌券已被其他歌曲核销,无法用于此歌曲排期'
})
}
// 作废券继续排期会让财务/发放记录失真,因此这里让事务失败。
throw createError({
statusCode: 409,
message: `点歌券当前状态为 ${current.status},无法完成排期核销`
})
}
export const restoreCardCodeAfterScheduleRemoval = async (
tx: any,
{ songId, cardCodeId, operatorId, at }: CardCodeTransitionContext
) => {
if (!cardCodeId) return { changed: false, reason: 'NO_CARD_CODE' }
const restoredAt = getTransitionTime(at)
const current = await readCardCode(tx, cardCodeId)
if (!current) return { changed: false, reason: 'MISSING_CARD_CODE' }
if (current.status !== 'REDEEMED') {
return { changed: false, status: current.status }
}
const restoreResult = await tx
.update(cardCodes)
.set({
status: 'LOCKED',
lockedBy: current.lockedBy || operatorId,
lockedAt: current.lockedAt || restoredAt,
redeemedBy: null,
redeemedAt: null,
updatedAt: restoredAt
})
.where(and(eq(cardCodes.id, cardCodeId), eq(cardCodes.status, 'REDEEMED')))
.returning({ id: cardCodes.id, code: cardCodes.code })
if (restoreResult.length === 0) {
return { changed: false, reason: 'CONCURRENT_CHANGE' }
}
await tx.insert(cardCodeRedeemLogs).values({
cardCodeId,
codeSnapshot: restoreResult[0].code,
redeemedBy: operatorId,
redeemedAt: restoredAt,
source: 'SCHEDULE_REMOVE',
songId
})
return { changed: true, status: 'LOCKED' }
}
export const releaseCardCodeAfterSongWithdrawal = async (
tx: any,
{ songId, cardCodeId, operatorId, at }: CardCodeTransitionContext
) => {
if (!cardCodeId) return { changed: false, reason: 'NO_CARD_CODE' }
const releasedAt = getTransitionTime(at)
const current = await readCardCode(tx, cardCodeId)
if (!current) return { changed: false, reason: 'MISSING_CARD_CODE' }
if (!['LOCKED', 'REDEEMED'].includes(current.status)) {
return { changed: false, status: current.status }
}
const releaseResult = await tx
.update(cardCodes)
.set({
status: 'AVAILABLE',
lockedBy: null,
lockedAt: null,
redeemedBy: null,
redeemedAt: null,
updatedAt: releasedAt
})
.where(and(eq(cardCodes.id, cardCodeId), inArray(cardCodes.status, ['LOCKED', 'REDEEMED'])))
.returning({ id: cardCodes.id, code: cardCodes.code })
if (releaseResult.length === 0) {
return { changed: false, reason: 'CONCURRENT_CHANGE' }
}
await tx.insert(cardCodeRedeemLogs).values({
cardCodeId,
codeSnapshot: releaseResult[0].code,
redeemedBy: operatorId,
redeemedAt: releasedAt,
source: 'WITHDRAW',
songId
})
return { changed: true, status: 'AVAILABLE' }
}