/** * 91 卡券交付前核销: * 在查询接口即将返回 orderStatus=20 + cards 前,同步核销行业电子凭证, * 把「核销时间」提前到链接交付时刻,降低「拿链即退」误退窗口。 * * 语义:核销 = 领取链接已交付(售出闭环),≠ 游戏内道具已到账。 */ import { attachKuaishouIndustryVoucherToTask, } from '../platforms/kuaishou-industry/voucher-binding-service.js' import { consumeKuaishouIndustryVoucher, } from '../platforms/kuaishou-industry/voucher-service.js' import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js' import { logIntegration, logWarn } from '../../utils/logger.js' import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow, } from '../../types/repository/rows.js' export const OPEN_91_DELIVER_CONSUME_SOURCE = 'open91_query_deliver' export type Open91DeliverConsumeResult = | { ok: true consumedTaskIds: number[] alreadyConsumedTaskIds: number[] } | { ok: false errorMessage: string failedTaskIds: number[] consumedTaskIds: number[] alreadyConsumedTaskIds: number[] } /** * 按 task_id / unit_index 解析任务对应的行业电子凭证(与 91 查询就绪判定一致)。 */ export function resolveIndustryVoucherForTask( task: Pick, vouchers: KuaishouIndustryVoucherRow[], ): KuaishouIndustryVoucherRow | null { const taskId = Number(task.id || 0) const unitIndex = Number(task.unit_index || 0) if (taskId > 0) { const byTaskId = vouchers.find((row) => Number(row.task_id || 0) === taskId) if (byTaskId) { return byTaskId } } if (unitIndex > 0) { const byUnit = vouchers.find((row) => Number(row.unit_index || 0) === unitIndex) if (byUnit) { return byUnit } } return null } function isVoucherConsumed(voucher: Pick): boolean { return String(voucher.status || '').trim().toUpperCase() === 'CONSUMED' } /** * 对即将交付给 91 的 ready 任务同步核销行业凭证(幂等)。 * 失败时调用方应返回 orderStatus=10,避免「未核销却已发链」。 */ export async function consumeIndustryVouchersBeforeOpen91Deliver(options: { order: OrderRow tasks: TaskRow[] readyTaskIds: number[] requestId?: string source?: string }): Promise { const source = String(options.source || OPEN_91_DELIVER_CONSUME_SOURCE).trim() || OPEN_91_DELIVER_CONSUME_SOURCE const readySet = new Set( (Array.isArray(options.readyTaskIds) ? options.readyTaskIds : []) .map((id) => Number(id)) .filter((id) => id > 0), ) const readyTasks = (Array.isArray(options.tasks) ? options.tasks : []).filter((task) => readySet.has(Number(task.id || 0)), ) if (readyTasks.length === 0) { return { ok: true, consumedTaskIds: [], alreadyConsumedTaskIds: [], } } const oid = String(options.order.platform_order_id || '').trim() const vouchers = oid ? await listKuaishouIndustryVouchersByOid(oid) : [] const consumedTaskIds: number[] = [] const alreadyConsumedTaskIds: number[] = [] const failed: Array<{ taskId: number; errorMessage: string }> = [] for (const task of readyTasks) { const taskId = Number(task.id || 0) const voucher = resolveIndustryVoucherForTask(task, vouchers) if (!voucher) { failed.push({ taskId, errorMessage: '交付前核销失败:未找到对应电子凭证', }) continue } const alreadyConsumed = isVoucherConsumed(voucher) const result = await consumeKuaishouIndustryVoucher(voucher, { source, task, token: String(voucher.token || '').trim(), consumeTime: Date.now(), }) if (!result.ok || !result.voucher) { failed.push({ taskId, errorMessage: result.errorMessage || '电子凭证核销回调失败', }) continue } await attachKuaishouIndustryVoucherToTask(task, result.voucher, { source, }) if (alreadyConsumed) { alreadyConsumedTaskIds.push(taskId) } else { consumedTaskIds.push(taskId) } } if (failed.length > 0) { const errorMessage = failed[0]?.errorMessage || '电子凭证核销失败,暂缓交付领取链接' logWarn('[open-91/query]', '91交付前核销失败,暂不返回 cards', { requestId: options.requestId || '', orderId: options.order.id, orderNo: oid, failedTaskIds: failed.map((item) => item.taskId), failed, consumedTaskIds, alreadyConsumedTaskIds, errorMessage, }) return { ok: false, errorMessage, failedTaskIds: failed.map((item) => item.taskId), consumedTaskIds, alreadyConsumedTaskIds, } } logIntegration('[open-91/query]', '91交付前核销完成', { requestId: options.requestId || '', orderId: options.order.id, orderNo: oid, readyTaskCount: readyTasks.length, consumedTaskIds, alreadyConsumedTaskIds, source, }) return { ok: true, consumedTaskIds, alreadyConsumedTaskIds, } }