From f336311ef80ad81fe27988d6e032846897544b60 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 3 Nov 2026 13:27:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(open91):=20=E5=B1=A5=E7=BA=A6=E6=9C=AA?= =?UTF-8?q?=E5=B0=B1=E7=BB=AA=E6=97=B6=E4=BF=9D=E6=8C=81=E8=BD=AE=E8=AF=A2?= =?UTF-8?q?=E7=AD=89=E5=BE=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/open-91/order-create-service.ts | 48 ++++++++----------- .../src/services/open-91/protocol.test.ts | 7 +++ .../platforms/ninetyone/order-service.ts | 26 ++++++++++ 3 files changed, 52 insertions(+), 29 deletions(-) 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 698d69d8..eeb22314 100644 --- a/apps/backend/src/services/open-91/order-create-service.ts +++ b/apps/backend/src/services/open-91/order-create-service.ts @@ -3,14 +3,11 @@ import { logIntegration } from '../../utils/logger.js' import { parseAmountToFen } from '../../utils/money.js' import { buildOpen91SourceEvent, + markOpen91OrderPendingConfig, upsertOpen91PendingOrder, } from '../platforms/ninetyone/order-service.js' import { notifyOpen91PendingConfig } from '../notification/domain-notifications.js' -import { - assertOpen91Config, - OPEN_91_COST_EXCEED_FAIL_CODE, - OPEN_91_DEFAULT_FAIL_CODE, -} from './config.js' +import { assertOpen91Config, OPEN_91_COST_EXCEED_FAIL_CODE } from './config.js' import { assertOpen91CreatePayload, assertOpen91Timestamp, @@ -23,6 +20,11 @@ import { buildOpen91SuccessResponse, } from './response.js' +/** 内部订单已经落库但履约任务尚未生成时,91 必须拿 10 + outTradeNo 继续轮询。 */ +export function shouldWaitForOpen91Fulfillment(tasks: unknown): boolean { + return !Array.isArray(tasks) || tasks.length === 0 +} + export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { const config = assertOpen91Config() const normalized = normalizeOpen91CreatePayload(payload) @@ -86,26 +88,26 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { }) } - if (isWaitingKuaishouIndustrySendCallback(result.ignoreReason) && result.order) { + if (shouldWaitForOpen91Fulfillment(result.tasks)) { + const pendingOrder = await markOpen91OrderPendingConfig( + result.order, + result.ignoreReason || 'fulfillment_not_ready', + ) + logIntegration('[open-91/create]', '91卡券订单等待履约就绪,已进入轮询', { + requestId, + orderNo: normalized.orderNo, + orderId: result.order.id, + reason: result.ignoreReason || 'fulfillment_not_ready', + }) return buildOpen91SuccessResponse({ orderNo: normalized.orderNo, - outTradeNo: buildOpen91OutTradeNo(result.order), + outTradeNo: buildOpen91OutTradeNo(pendingOrder || result.order), orderStatus: 10, orderCost: buildOpen91OrderCost(0), cards: '', }) } - if (!result.order || !Array.isArray(result.tasks) || result.tasks.length === 0) { - return buildOpen91SuccessResponse({ - orderNo: normalized.orderNo, - outTradeNo: '', - orderStatus: 30, - failCode: OPEN_91_DEFAULT_FAIL_CODE, - failReason: resolveOpen91CreateFailReason(result.ignoreReason), - }) - } - return buildOpen91SuccessResponse({ orderNo: normalized.orderNo, outTradeNo: buildOpen91OutTradeNo(result.order), @@ -114,15 +116,3 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { cards: '', }) } - -function resolveOpen91CreateFailReason(ignoreReason: unknown) { - if (isWaitingKuaishouIndustrySendCallback(ignoreReason)) { - return '电子凭证发码回调未成功,订单暂不能履约' - } - - return '商品未配置或订单无法履约' -} - -function isWaitingKuaishouIndustrySendCallback(ignoreReason: unknown) { - return String(ignoreReason || '').trim() === 'kuaishou_industry_send_callback_unconfirmed' -} diff --git a/apps/backend/src/services/open-91/protocol.test.ts b/apps/backend/src/services/open-91/protocol.test.ts index c6394434..ac12abc1 100644 --- a/apps/backend/src/services/open-91/protocol.test.ts +++ b/apps/backend/src/services/open-91/protocol.test.ts @@ -8,6 +8,7 @@ import { verifyOpen91Signature, } from './crypto.js' import { buildOpen91CardItem, formatOpen91ExpireTime, resolveOpen91QueryState } from './response.js' +import { shouldWaitForOpen91Fulfillment } from './order-create-service.js' test('signOpen91Payload signs only request body fields', () => { const config = { @@ -114,6 +115,12 @@ test('resolveOpen91QueryState returns success only when every task is ready', () ) }) +test('内部订单已落库但履约任务未生成时,91 下单响应必须进入轮询', () => { + assert.equal(shouldWaitForOpen91Fulfillment([]), true) + assert.equal(shouldWaitForOpen91Fulfillment(undefined), true) + assert.equal(shouldWaitForOpen91Fulfillment([{ id: 1 }]), false) +}) + test('buildOpen91CardItem mirrors claim link into jumpLink and leaves password empty', () => { assert.deepEqual( buildOpen91CardItem({ diff --git a/apps/backend/src/services/platforms/ninetyone/order-service.ts b/apps/backend/src/services/platforms/ninetyone/order-service.ts index ad9f9693..92793973 100644 --- a/apps/backend/src/services/platforms/ninetyone/order-service.ts +++ b/apps/backend/src/services/platforms/ninetyone/order-service.ts @@ -25,6 +25,32 @@ import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js' export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config' export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed' +export async function markOpen91OrderPendingConfig(order: OrderRow, reason = '') { + const now = nowIso() + const rawPayload = parseJsonObject(order.raw_payload_json) + return updateOrder(order.id, { + provider: order.provider, + platform: order.platform, + shopId: order.shop_id, + shopName: order.shop_name, + platformOrderId: order.platform_order_id, + orderStatus: OPEN_91_PENDING_CONFIG_STATUS, + payStatus: order.pay_status, + buyerId: order.buyer_id, + buyerName: order.buyer_name, + receiverContact: order.receiver_contact, + totalAmount: Number(order.total_amount) || 0, + currency: order.currency, + rawPayloadJson: JSON.stringify({ + ...rawPayload, + pendingReason: String(reason || '').trim() || 'fulfillment_not_ready', + pendingAt: now, + }), + paidAt: order.paid_at, + updatedAt: now, + }) +} + export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObject = {}) { const normalized = normalizeOpen91CreatePayload(payload) const productInfo = parseOpen91ProductNo(normalized.productNo)