diff --git a/apps/backend/src/db/migrations/005_kuaishou_industry_voucher_send_callback.sql b/apps/backend/src/db/migrations/005_kuaishou_industry_voucher_send_callback.sql new file mode 100644 index 00000000..f5865d8d --- /dev/null +++ b/apps/backend/src/db/migrations/005_kuaishou_industry_voucher_send_callback.sql @@ -0,0 +1,9 @@ +ALTER TABLE kuaishou_industry_vouchers + ADD COLUMN IF NOT EXISTS send_callback_status TEXT NOT NULL DEFAULT 'success', + ADD COLUMN IF NOT EXISTS send_callback_attempt_count INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS send_callback_last_error TEXT NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS send_callback_response_json JSONB NOT NULL DEFAULT '{}'::jsonb, + ADD COLUMN IF NOT EXISTS send_callback_sent_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS idx_kuaishou_industry_vouchers_send_callback_status + ON kuaishou_industry_vouchers(send_callback_status); diff --git a/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts b/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts index 6c839175..cf4bbc66 100644 --- a/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts +++ b/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts @@ -63,6 +63,11 @@ async function insertKuaishouIndustryVoucher( consume_details_json, consumed_at, destroyed_at, + send_callback_status, + send_callback_attempt_count, + send_callback_last_error, + send_callback_response_json, + send_callback_sent_at, raw_payload_json, created_at, updated_at @@ -82,9 +87,14 @@ async function insertKuaishouIndustryVoucher( $12::jsonb, $13, $14, - $15::jsonb, + $15, $16, - $17 + $17, + $18::jsonb, + $19, + $20::jsonb, + $21, + $22 ) ON CONFLICT (oid, unit_index) DO UPDATE SET @@ -100,6 +110,10 @@ async function insertKuaishouIndustryVoucher( task_id = COALESCE(kuaishou_industry_vouchers.task_id, EXCLUDED.task_id), valid_start_time = EXCLUDED.valid_start_time, valid_end_time = EXCLUDED.valid_end_time, + send_callback_status = EXCLUDED.send_callback_status, + send_callback_last_error = EXCLUDED.send_callback_last_error, + send_callback_response_json = EXCLUDED.send_callback_response_json, + send_callback_sent_at = EXCLUDED.send_callback_sent_at, raw_payload_json = EXCLUDED.raw_payload_json, updated_at = EXCLUDED.updated_at RETURNING * @@ -119,6 +133,11 @@ async function insertKuaishouIndustryVoucher( stringifyJson(input.consumeDetailsJson ?? []), input.consumedAt || null, input.destroyedAt || null, + input.sendCallbackStatus || 'success', + normalizeNonNegativeInteger(input.sendCallbackAttemptCount, 0), + input.sendCallbackLastError || '', + stringifyJson(input.sendCallbackResponseJson ?? {}), + input.sendCallbackSentAt || null, stringifyJson(input.rawPayloadJson ?? {}), input.createdAt, input.updatedAt, @@ -277,6 +296,33 @@ function normalizeVoucherPatchColumns( columns.push({ column: 'destroyed_at', value: patch.destroyedAt || null }) } + if (patch.sendCallbackStatus !== undefined) { + columns.push({ column: 'send_callback_status', value: patch.sendCallbackStatus || 'success' }) + } + + if (patch.sendCallbackAttemptCount !== undefined) { + columns.push({ + column: 'send_callback_attempt_count', + value: normalizeNonNegativeInteger(patch.sendCallbackAttemptCount, 0), + }) + } + + if (patch.sendCallbackLastError !== undefined) { + columns.push({ column: 'send_callback_last_error', value: patch.sendCallbackLastError || '' }) + } + + if (patch.sendCallbackResponseJson !== undefined) { + columns.push({ + column: 'send_callback_response_json', + value: stringifyJson(patch.sendCallbackResponseJson ?? {}), + cast: '::jsonb', + }) + } + + if (patch.sendCallbackSentAt !== undefined) { + columns.push({ column: 'send_callback_sent_at', value: patch.sendCallbackSentAt || null }) + } + if (patch.rawPayloadJson !== undefined) { columns.push({ column: 'raw_payload_json', @@ -337,6 +383,11 @@ function normalizeNullableId(value: unknown): number | null { return Number.isFinite(parsed) && parsed > 0 ? parsed : null } +function normalizeNonNegativeInteger(value: unknown, fallback: number) { + const parsed = Number(value) + return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback +} + function stringifyJson(value: unknown): string { if (typeof value === 'string') { return value diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts index 17537869..056e0968 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.ts @@ -638,7 +638,8 @@ function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean { : {}; const voucherCode = String(voucher.voucherCode || voucher.eticketId || "").trim(); const status = String(voucher.status || "UNUSED").trim().toUpperCase(); - return Boolean(voucherCode && status !== "DESTROYED"); + const sendCallbackStatus = String(voucher.sendCallbackStatus || "success").trim().toLowerCase(); + return Boolean(voucherCode && status !== "DESTROYED" && sendCallbackStatus === "success"); } export function buildDispatchStockItems( diff --git a/apps/backend/src/services/open-91/order-create-service.ts b/apps/backend/src/services/open-91/order-create-service.ts index 03547abc..4796f752 100644 --- a/apps/backend/src/services/open-91/order-create-service.ts +++ b/apps/backend/src/services/open-91/order-create-service.ts @@ -92,7 +92,7 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { outTradeNo: '', orderStatus: 30, failCode: OPEN_91_DEFAULT_FAIL_CODE, - failReason: '商品未配置或订单无法履约', + failReason: resolveOpen91CreateFailReason(result.ignoreReason), }) } @@ -104,3 +104,11 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { cards: '', }) } + +function resolveOpen91CreateFailReason(ignoreReason: unknown) { + if (String(ignoreReason || '').trim() === 'kuaishou_industry_send_callback_unconfirmed') { + return '电子凭证发码回调未成功,订单暂不能履约' + } + + return '商品未配置或订单无法履约' +} diff --git a/apps/backend/src/services/order/order-service.ts b/apps/backend/src/services/order/order-service.ts index d6ef89cb..5f67196c 100644 --- a/apps/backend/src/services/order/order-service.ts +++ b/apps/backend/src/services/order/order-service.ts @@ -4,9 +4,14 @@ import { updateOrder, } from '../../repositories/order-repo.js' import { replaceOrderItems } from '../../repositories/order-item-repo.js' +import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js' import { syncDeliveryTasksForOrder } from './delivery-task-service.js' import { resolveOrderItemForFulfillment } from './product-match-service.js' import { bindKuaishouIndustryVouchersToOrderTasks } from '../platforms/kuaishou-industry/voucher-binding-service.js' +import { + isKuaishouIndustryVoucherSendCallbackSuccess, + resolveKuaishouIndustryVoucherSendCallbackMessage, +} from '../platforms/kuaishou-industry/voucher-service.js' import { nowIso } from '../../utils/time.js' import { logIntegration } from '../../utils/logger.js' import { createHttpError } from '../../utils/http.js' @@ -184,6 +189,25 @@ export async function upsertOrderFromSource( })), ) + const industryVoucherGate = await resolveKuaishouIndustryVoucherGate(order.platform_order_id) + if (!industryVoucherGate.allowed) { + logIntegration('[order-service]', `${sourceLabel} 订单暂停履约:电子凭证发码回调未确认`, { + orderId: order.id, + provider: order.provider, + platform: order.platform, + platformOrderId: order.platform_order_id, + voucherCount: industryVoucherGate.voucherCount, + reason: industryVoucherGate.reason, + }, { level: 'warn' }) + + return { + ignoreReason: 'kuaishou_industry_send_callback_unconfirmed', + order, + orderItems, + tasks: [], + } + } + const tasks = await syncDeliveryTasksForOrder(order, orderItems) await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, { source: `${sourceLabel}_order_upsert`, @@ -209,6 +233,36 @@ export async function upsertOrderFromSource( } } +async function resolveKuaishouIndustryVoucherGate(platformOrderId: unknown) { + const oid = String(platformOrderId || '').trim() + if (!oid) { + return { + allowed: true, + voucherCount: 0, + reason: '', + } + } + + const vouchers = await listKuaishouIndustryVouchersByOid(oid) + if (vouchers.length === 0 || vouchers.every(isKuaishouIndustryVoucherSendCallbackSuccess)) { + return { + allowed: true, + voucherCount: vouchers.length, + reason: '', + } + } + + const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) + + return { + allowed: false, + voucherCount: vouchers.length, + reason: blockedVoucher + ? resolveKuaishouIndustryVoucherSendCallbackMessage(blockedVoucher) + : '电子凭证发码回调未确认', + } +} + const ORDER_STATUS_PRIORITY = { created: 0, paid: 1, diff --git a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts index 5b5fc443..f920c2e2 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts @@ -13,7 +13,10 @@ import { buildIndustryErrorResponse, buildIndustryQueryCodeData, } from './response.js' -import { buildKuaishouIndustryEticketFromVoucher } from './voucher-service.js' +import { + buildKuaishouIndustryEticketFromVoucher, + isKuaishouIndustryVoucherSendCallbackSuccess, +} from './voucher-service.js' type JsonObject = Record @@ -33,6 +36,9 @@ export async function handleQueryCode(rawBody: JsonObject = {}) { if (!matched) { return buildIndustryErrorResponse(4012005, `卡券不存在: ${params.eticketId}`) } + if (!isKuaishouIndustryVoucherSendCallbackSuccess(matched)) { + return buildIndustryErrorResponse(4012005, `卡券尚未完成发码: ${params.eticketId}`) + } const eticket = buildKuaishouIndustryEticketFromVoucher(matched, params.eticketType) return buildIndustrySuccessResponse( @@ -45,7 +51,8 @@ export async function handleQueryCode(rawBody: JsonObject = {}) { ) } - const vouchers = await listKuaishouIndustryVouchersByOid(normalizedOid) + const vouchers = (await listKuaishouIndustryVouchersByOid(normalizedOid)) + .filter(isKuaishouIndustryVoucherSendCallbackSuccess) if (vouchers.length === 0) { return buildIndustryErrorResponse(4012002, `订单不存在: ${normalizedOid}`) } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts index bd4877a8..744aa4a8 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts @@ -1,6 +1,9 @@ import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js' import { listTasksByOrderId } from '../../../repositories/task-repo.js' -import { upsertKuaishouIndustryVoucher } from '../../../repositories/kuaishou-industry-voucher-repo.js' +import { + updateKuaishouIndustryVoucherByCode, + upsertKuaishouIndustryVoucher, +} from '../../../repositories/kuaishou-industry-voucher-repo.js' import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../../open-91/config.js' import { normalizeTimestampIso } from '../../../utils/time.js' import { logIntegration } from '../../../utils/logger.js' @@ -11,6 +14,7 @@ import { import { normalizeSendCodePayload, assertSendCodePayload } from './payload.js' import { assertKuaishouIndustrySignature } from './crypto.js' import { + buildIndustryErrorResponse, buildIndustrySuccessResponse, buildIndustryEticketItem, buildIndustrySendCodeData, @@ -18,11 +22,12 @@ import { import { sendCallback } from './send-callback-service.js' import { buildKuaishouIndustryEticketFromVoucher, + KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS, resolveKuaishouIndustryVoucherValidity, } from './voucher-service.js' import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-service.js' import { parseAmountToFen } from '../../../utils/money.js' -import type { OrderRow } from '../../../types/repository/rows.js' +import type { KuaishouIndustryVoucherRow, OrderRow } from '../../../types/repository/rows.js' type JsonObject = Record @@ -47,7 +52,7 @@ export async function handleSendCode(rawBody: JsonObject = {}) { const targetTaskCount = params.num > 0 ? params.num : 1 const tasks = order ? await listTasksByOrderId(order.id) : [] const validity = resolveKuaishouIndustryVoucherValidity(params, nowMs) - const vouchers = [] + const vouchers: KuaishouIndustryVoucherRow[] = [] for (let index = 0; index < targetTaskCount; index += 1) { const unitIndex = index + 1 @@ -60,6 +65,10 @@ export async function handleSendCode(rawBody: JsonObject = {}) { orderId: order?.id || null, taskId: task?.id || null, status: 'UNUSED', + sendCallbackStatus: KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.PENDING, + sendCallbackLastError: '', + sendCallbackResponseJson: {}, + sendCallbackSentAt: null, validStartTime: validity.validStartTime, validEndTime: validity.validEndTime, rawPayloadJson: { @@ -76,30 +85,15 @@ export async function handleSendCode(rawBody: JsonObject = {}) { } } - if (order) { - await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, { - source: 'kuaishou_industry_send_code', - now, - }) - } - const etickets = vouchers.map((voucher) => buildKuaishouIndustryEticketFromVoucher(voucher, params.eticketType), ) - const response = buildIndustrySuccessResponse( - buildIndustrySendCodeData({ - oid: normalizedOid, - sendType: params.sendType, - sendNum: etickets.length, - etickets, - }), - ) - - fireSendCallback({ + const callbackResult = await sendAndRecordCallback({ oid: normalizedOid, sendType: params.sendType, etickets, + vouchers, sellerId: params.sellerId, token: params.token, eticketType: params.eticketType, @@ -107,13 +101,35 @@ export async function handleSendCode(rawBody: JsonObject = {}) { preferredTotalGoodsValue: resolveSendCallbackPreferredTotalGoodsValue(order), }) - return response + if (!callbackResult.success) { + return buildIndustryErrorResponse( + 4010003, + resolveSendCallbackFailureMessage(callbackResult), + ) + } + + if (order) { + await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, { + source: 'kuaishou_industry_send_code', + now, + }) + } + + return buildIndustrySuccessResponse( + buildIndustrySendCodeData({ + oid: normalizedOid, + sendType: params.sendType, + sendNum: etickets.length, + etickets, + }), + ) } -function fireSendCallback(input: { +async function sendAndRecordCallback(input: { oid: string sendType: string etickets: ReturnType[] + vouchers: KuaishouIndustryVoucherRow[] sellerId: string token: string eticketType?: string @@ -139,7 +155,7 @@ function fireSendCallback(input: { const sendNum = eticketItems.reduce((sum, e) => sum + e.num, 0) const totalGoodsValue = goodsValuePlan.totalGoodsValue - logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调已调度', { + logIntegration('[kuaishou-industry/send-code]', '准备发起电子凭证发货回调', { oid: input.oid, sellerId: input.sellerId, sendType: input.sendType, @@ -151,35 +167,74 @@ function fireSendCallback(input: { eticketType: input.eticketType || '', }) - sendCallback({ - oid: input.oid, - sendType: input.sendType, - etickets: eticketItemsWithGoodsValue, - sellerId: input.sellerId, - sendNum, - totalGoodsValue, - token: input.token, - ...(input.eticketType ? { eticketType: input.eticketType } : {}), - }).then((result) => { - if (!result.success) { - logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调异步执行失败', { - oid: input.oid, - error: result.error || '', - response: result.response || null, - }, { level: 'warn' }) - return - } - - logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调异步执行完成', { + let result: Awaited> + try { + result = await sendCallback({ oid: input.oid, - response: result.response || null, + sendType: input.sendType, + etickets: eticketItemsWithGoodsValue, + sellerId: input.sellerId, + sendNum, + totalGoodsValue, + token: input.token, + ...(input.eticketType ? { eticketType: input.eticketType } : {}), }) - }).catch((err) => { - logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调异步执行异常', { + } catch (error) { + result = { + success: false, + error: error instanceof Error ? error.message : String(error), + } + } + + const sentAt = normalizeTimestampIso(new Date().toISOString()) + await Promise.all(input.vouchers.map((voucher) => + updateKuaishouIndustryVoucherByCode(voucher.voucher_code, { + sendCallbackStatus: result.success + ? KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.SUCCESS + : KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.FAILED, + sendCallbackAttemptCount: normalizePositiveInteger(voucher.send_callback_attempt_count) + 1, + sendCallbackLastError: result.success ? '' : resolveSendCallbackFailureMessage(result), + sendCallbackResponseJson: result.response || {}, + sendCallbackSentAt: sentAt, + updatedAt: sentAt, + }), + )) + + if (!result.success) { + logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调执行失败,发码请求已拒绝继续', { oid: input.oid, - error: err instanceof Error ? err.message : String(err), + error: result.error || '', + response: result.response || null, }, { level: 'warn' }) + return result + } + + logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调执行完成', { + oid: input.oid, + response: result.response || null, }) + + return result +} + +function resolveSendCallbackFailureMessage(result: Awaited>) { + const error = String(result.error || '').trim() + if (error) { + return error + } + + const response = parseJsonObject(result.response) + const errorMsg = String(response.error_msg || response.message || response.msg || '').trim() + if (errorMsg) { + return errorMsg + } + + const resultCode = String(response.result || response.code || '').trim() + if (resultCode) { + return `电子凭证发货回调失败,平台返回码:${resultCode}` + } + + return '电子凭证发货回调失败' } export function resolveSendCallbackGoodsValuePlan( diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts index ba6e7913..52b12a6f 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts @@ -6,6 +6,11 @@ import { import { normalizeTimestampIso } from '../../../utils/time.js' import { parseTaskContext } from '../../../utils/task-json.js' import { normalizeKuaishouCloudFlow } from '../../fulfillment/kuaishou-cloud/domain.js' +import { + isKuaishouIndustryVoucherSendCallbackSuccess, + normalizeKuaishouIndustrySendCallbackStatus, + resolveKuaishouIndustryVoucherSendCallbackMessage, +} from './voucher-service.js' import type { KuaishouIndustryVoucherRow, OrderRow, @@ -41,7 +46,7 @@ export async function bindKuaishouIndustryVouchersToOrderTasks( const currentVoucher = nextVoucher || voucher bound.push(currentVoucher) - if (task) { + if (task && isKuaishouIndustryVoucherSendCallbackSuccess(currentVoucher)) { await attachKuaishouIndustryVoucherToTask(task, currentVoucher, { source: options.source || 'voucher_bind', now, @@ -67,12 +72,16 @@ export async function attachKuaishouIndustryVoucherToTask( context.kuaishouIndustryVoucher, now, ) + const sendCallbackConfirmed = isKuaishouIndustryVoucherSendCallbackSuccess(voucher) const nextContext: JsonObject = { ...context, kuaishouIndustryVoucher: nextVoucherContext, } - if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' || context.kuaishouCloudFulfillment) { + if ( + sendCallbackConfirmed && + (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' || context.kuaishouCloudFulfillment) + ) { const flow = normalizeKuaishouCloudFlow(context.kuaishouCloudFulfillment) const consumedAt = voucher.consumed_at || nextVoucherContext.consumedAt || null const status = String(voucher.status || 'UNUSED').trim().toUpperCase() @@ -143,6 +152,10 @@ export function buildKuaishouIndustryVoucherContext( voucherCode: String(voucher.voucher_code || '').trim(), unitIndex: Number(voucher.unit_index || 0) || 0, status, + sendCallbackStatus: normalizeKuaishouIndustrySendCallbackStatus(voucher.send_callback_status), + sendCallbackLastError: resolveKuaishouIndustryVoucherSendCallbackMessage(voucher), + sendCallbackAttemptCount: Number(voucher.send_callback_attempt_count || 0) || 0, + sendCallbackSentAt: voucher.send_callback_sent_at || existing.sendCallbackSentAt || null, validStartTime: Number(voucher.valid_start_time || 0) || 0, validEndTime: Number(voucher.valid_end_time || 0) || 0, verifiedAt: existing.verifiedAt || now, diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts index 13536285..e5ebd42f 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts @@ -12,6 +12,11 @@ import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/reposit type JsonObject = Record export const KUAISHOU_INDUSTRY_DEFAULT_VALID_DAYS = 3 +export const KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS = { + PENDING: 'pending', + SUCCESS: 'success', + FAILED: 'failed', +} as const export function resolveKuaishouIndustryVoucherValidity( input: { @@ -61,6 +66,43 @@ export function buildKuaishouIndustryEticketFromVoucher( }) } +export function normalizeKuaishouIndustrySendCallbackStatus(value: unknown) { + const normalized = String(value || '').trim().toLowerCase() + if ( + normalized === KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.PENDING || + normalized === KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.FAILED + ) { + return normalized + } + + return KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.SUCCESS +} + +export function isKuaishouIndustryVoucherSendCallbackSuccess( + voucher: Pick, +) { + return normalizeKuaishouIndustrySendCallbackStatus(voucher.send_callback_status) === + KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.SUCCESS +} + +export function resolveKuaishouIndustryVoucherSendCallbackMessage( + voucher: Pick, +) { + const status = normalizeKuaishouIndustrySendCallbackStatus(voucher.send_callback_status) + if (status === KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.SUCCESS) { + return '' + } + + const errorMessage = String(voucher.send_callback_last_error || '').trim() + if (errorMessage) { + return errorMessage + } + + return status === KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.PENDING + ? '电子凭证发码回调尚未成功,暂不能继续履约' + : '电子凭证发码回调失败,暂不能继续履约' +} + export async function consumeKuaishouIndustryVouchersForTask( task: TaskRow, input: { @@ -149,6 +191,15 @@ export async function consumeKuaishouIndustryVoucher( } } + if (!isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) { + return { + ok: false, + voucher, + callbackSuccess: false, + errorMessage: resolveKuaishouIndustryVoucherSendCallbackMessage(voucher), + } + } + const now = new Date().toISOString() const consumeTime = Number(input.consumeTime || Date.now()) || Date.now() const consumeType = String(input.consumeType || 'delivery').trim() || 'delivery' diff --git a/apps/backend/src/types/repository/inputs.ts b/apps/backend/src/types/repository/inputs.ts index df2707e9..1ba2c5f4 100644 --- a/apps/backend/src/types/repository/inputs.ts +++ b/apps/backend/src/types/repository/inputs.ts @@ -142,6 +142,11 @@ export type KuaishouIndustryVoucherUpsertInput = { consumeDetailsJson?: string | Record | unknown[] consumedAt?: string | null destroyedAt?: string | null + sendCallbackStatus?: string + sendCallbackAttemptCount?: number + sendCallbackLastError?: string + sendCallbackResponseJson?: string | Record + sendCallbackSentAt?: string | null rawPayloadJson?: string | Record createdAt: string updatedAt: string diff --git a/apps/backend/src/types/repository/rows.ts b/apps/backend/src/types/repository/rows.ts index 567a1d1f..782d0a7a 100644 --- a/apps/backend/src/types/repository/rows.ts +++ b/apps/backend/src/types/repository/rows.ts @@ -120,6 +120,11 @@ export type KuaishouIndustryVoucherRow = { consume_details_json: string | Record | unknown[] consumed_at: string | null destroyed_at: string | null + send_callback_status: string + send_callback_attempt_count: number + send_callback_last_error: string + send_callback_response_json: string | Record + send_callback_sent_at: string | null raw_payload_json: string | Record created_at: string updated_at: string