169 lines
5.0 KiB
TypeScript
169 lines
5.0 KiB
TypeScript
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' }
|
|
}
|