fix(open91): 履约未就绪时保持轮询等待

This commit is contained in:
yml2213
2026-11-03 13:27:53 +08:00
parent b842d00fc3
commit f336311ef8
3 changed files with 52 additions and 29 deletions
@@ -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'
}
@@ -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({
@@ -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)