feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -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' }
|
||||
}
|
||||
Reference in New Issue
Block a user