修复错误
This commit is contained in:
@@ -110,10 +110,31 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
task_id = COALESCE(kuaishou_industry_vouchers.task_id, EXCLUDED.task_id),
|
task_id = COALESCE(kuaishou_industry_vouchers.task_id, EXCLUDED.task_id),
|
||||||
valid_start_time = EXCLUDED.valid_start_time,
|
valid_start_time = EXCLUDED.valid_start_time,
|
||||||
valid_end_time = EXCLUDED.valid_end_time,
|
valid_end_time = EXCLUDED.valid_end_time,
|
||||||
send_callback_status = EXCLUDED.send_callback_status,
|
send_callback_status = CASE
|
||||||
send_callback_last_error = EXCLUDED.send_callback_last_error,
|
WHEN kuaishou_industry_vouchers.send_callback_status = 'success'
|
||||||
send_callback_response_json = EXCLUDED.send_callback_response_json,
|
THEN kuaishou_industry_vouchers.send_callback_status
|
||||||
send_callback_sent_at = EXCLUDED.send_callback_sent_at,
|
ELSE EXCLUDED.send_callback_status
|
||||||
|
END,
|
||||||
|
send_callback_attempt_count = CASE
|
||||||
|
WHEN kuaishou_industry_vouchers.send_callback_status = 'success'
|
||||||
|
THEN kuaishou_industry_vouchers.send_callback_attempt_count
|
||||||
|
ELSE EXCLUDED.send_callback_attempt_count
|
||||||
|
END,
|
||||||
|
send_callback_last_error = CASE
|
||||||
|
WHEN kuaishou_industry_vouchers.send_callback_status = 'success'
|
||||||
|
THEN kuaishou_industry_vouchers.send_callback_last_error
|
||||||
|
ELSE EXCLUDED.send_callback_last_error
|
||||||
|
END,
|
||||||
|
send_callback_response_json = CASE
|
||||||
|
WHEN kuaishou_industry_vouchers.send_callback_status = 'success'
|
||||||
|
THEN kuaishou_industry_vouchers.send_callback_response_json
|
||||||
|
ELSE EXCLUDED.send_callback_response_json
|
||||||
|
END,
|
||||||
|
send_callback_sent_at = CASE
|
||||||
|
WHEN kuaishou_industry_vouchers.send_callback_status = 'success'
|
||||||
|
THEN kuaishou_industry_vouchers.send_callback_sent_at
|
||||||
|
ELSE EXCLUDED.send_callback_sent_at
|
||||||
|
END,
|
||||||
raw_payload_json = EXCLUDED.raw_payload_json,
|
raw_payload_json = EXCLUDED.raw_payload_json,
|
||||||
updated_at = EXCLUDED.updated_at
|
updated_at = EXCLUDED.updated_at
|
||||||
RETURNING *
|
RETURNING *
|
||||||
@@ -163,6 +184,23 @@ export async function listKuaishouIndustryVouchersByOid(
|
|||||||
return result.rows
|
return result.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listKuaishouIndustryVouchersForSendCallbackRetry(
|
||||||
|
limit = 100,
|
||||||
|
): Promise<KuaishouIndustryVoucherRow[]> {
|
||||||
|
const result = await query<KuaishouIndustryVoucherRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM kuaishou_industry_vouchers
|
||||||
|
WHERE send_callback_status <> 'success'
|
||||||
|
ORDER BY COALESCE(send_callback_sent_at, created_at) ASC, id ASC
|
||||||
|
LIMIT $1
|
||||||
|
`,
|
||||||
|
[normalizePositiveLimit(limit, 100)],
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
export async function listKuaishouIndustryVouchersByTaskId(
|
export async function listKuaishouIndustryVouchersByTaskId(
|
||||||
taskId: number | string,
|
taskId: number | string,
|
||||||
): Promise<KuaishouIndustryVoucherRow[]> {
|
): Promise<KuaishouIndustryVoucherRow[]> {
|
||||||
@@ -388,6 +426,11 @@ function normalizeNonNegativeInteger(value: unknown, fallback: number) {
|
|||||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePositiveLimit(value: unknown, fallback: number): number {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : fallback
|
||||||
|
}
|
||||||
|
|
||||||
function stringifyJson(value: unknown): string {
|
function stringifyJson(value: unknown): string {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -86,6 +86,16 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isWaitingKuaishouIndustrySendCallback(result.ignoreReason) && result.order) {
|
||||||
|
return buildOpen91SuccessResponse({
|
||||||
|
orderNo: normalized.orderNo,
|
||||||
|
outTradeNo: buildOpen91OutTradeNo(result.order),
|
||||||
|
orderStatus: 10,
|
||||||
|
orderCost: buildOpen91OrderCost(0),
|
||||||
|
cards: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.order || !Array.isArray(result.tasks) || result.tasks.length === 0) {
|
if (!result.order || !Array.isArray(result.tasks) || result.tasks.length === 0) {
|
||||||
return buildOpen91SuccessResponse({
|
return buildOpen91SuccessResponse({
|
||||||
orderNo: normalized.orderNo,
|
orderNo: normalized.orderNo,
|
||||||
@@ -106,9 +116,13 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveOpen91CreateFailReason(ignoreReason: unknown) {
|
function resolveOpen91CreateFailReason(ignoreReason: unknown) {
|
||||||
if (String(ignoreReason || '').trim() === 'kuaishou_industry_send_callback_unconfirmed') {
|
if (isWaitingKuaishouIndustrySendCallback(ignoreReason)) {
|
||||||
return '电子凭证发码回调未成功,订单暂不能履约'
|
return '电子凭证发码回调未成功,订单暂不能履约'
|
||||||
}
|
}
|
||||||
|
|
||||||
return '商品未配置或订单无法履约'
|
return '商品未配置或订单无法履约'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWaitingKuaishouIndustrySendCallback(ignoreReason: unknown) {
|
||||||
|
return String(ignoreReason || '').trim() === 'kuaishou_industry_send_callback_unconfirmed'
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { findLatestOrderByPlatformOrderId } from '../../repositories/order-repo.js'
|
import { findLatestOrderByPlatformOrderId } from '../../repositories/order-repo.js'
|
||||||
|
import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
import { listTasksByOrderId } from '../../repositories/task-repo.js'
|
||||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
import { buildClaimUrl } from '../claim/claim-service.js'
|
||||||
import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud/index.js'
|
import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud/index.js'
|
||||||
@@ -11,6 +12,10 @@ import {
|
|||||||
OPEN_91_MANUAL_FAILED_STATUS,
|
OPEN_91_MANUAL_FAILED_STATUS,
|
||||||
OPEN_91_PENDING_CONFIG_STATUS,
|
OPEN_91_PENDING_CONFIG_STATUS,
|
||||||
} from '../platforms/ninetyone/order-service.js'
|
} from '../platforms/ninetyone/order-service.js'
|
||||||
|
import {
|
||||||
|
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||||
|
resolveKuaishouIndustryVoucherSendCallbackMessage,
|
||||||
|
} from '../platforms/kuaishou-industry/voucher-service.js'
|
||||||
import {
|
import {
|
||||||
assertOpen91Config,
|
assertOpen91Config,
|
||||||
OPEN_91_DEFAULT_FAIL_CODE,
|
OPEN_91_DEFAULT_FAIL_CODE,
|
||||||
@@ -30,7 +35,7 @@ import {
|
|||||||
buildOpen91SuccessResponse,
|
buildOpen91SuccessResponse,
|
||||||
resolveOpen91QueryState,
|
resolveOpen91QueryState,
|
||||||
} from './response.js'
|
} from './response.js'
|
||||||
import type { OrderRow } from '../../types/repository/rows.js'
|
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
@@ -59,6 +64,7 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tasks = await listTasksByOrderId(order.id)
|
const tasks = await listTasksByOrderId(order.id)
|
||||||
|
const industryVoucherState = await resolveOpen91IndustryVoucherState(order, tasks)
|
||||||
|
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
if (String(order.order_status || '').trim() === OPEN_91_PENDING_CONFIG_STATUS) {
|
if (String(order.order_status || '').trim() === OPEN_91_PENDING_CONFIG_STATUS) {
|
||||||
@@ -82,10 +88,23 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
|||||||
failReason: resolveOpen91OrderFailReason(order) || '商家手动标记无法履约',
|
failReason: resolveOpen91OrderFailReason(order) || '商家手动标记无法履约',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (industryVoucherState.voucherCount > 0) {
|
||||||
|
return buildOpen91SuccessResponse({
|
||||||
|
orderNo: normalized.orderNo,
|
||||||
|
outTradeNo: buildOpen91OutTradeNo(order),
|
||||||
|
orderStatus: 10,
|
||||||
|
failCode: 0,
|
||||||
|
failReason: '',
|
||||||
|
orderCost: buildOpen91OrderCost(0),
|
||||||
|
cards: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardItems = []
|
const cardItems = []
|
||||||
const readyTaskIds = []
|
const readyTaskIds = []
|
||||||
|
const industryReadyTaskIds = new Set(industryVoucherState.readyTaskIds)
|
||||||
|
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
let claimUrl = ''
|
let claimUrl = ''
|
||||||
@@ -115,6 +134,10 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!industryReadyTaskIds.has(Number(task.id))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
readyTaskIds.push(Number(task.id))
|
readyTaskIds.push(Number(task.id))
|
||||||
cardItems.push(buildOpen91CardItem({
|
cardItems.push(buildOpen91CardItem({
|
||||||
claimUrl,
|
claimUrl,
|
||||||
@@ -133,6 +156,9 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '
|
|||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
taskCount: tasks.length,
|
taskCount: tasks.length,
|
||||||
readyTaskCount: readyTaskIds.length,
|
readyTaskCount: readyTaskIds.length,
|
||||||
|
voucherCount: industryVoucherState.voucherCount,
|
||||||
|
voucherReadyTaskCount: industryVoucherState.readyTaskIds.length,
|
||||||
|
voucherPendingReason: industryVoucherState.pendingReason,
|
||||||
orderStatus: queryState.orderStatus,
|
orderStatus: queryState.orderStatus,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -189,6 +215,50 @@ function resolveOpen91OrderFailReason(order: OrderRow) {
|
|||||||
return String(payload.manualFailedReason || '').trim()
|
return String(payload.manualFailedReason || '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveOpen91IndustryVoucherState(order: OrderRow, tasks: TaskRow[]) {
|
||||||
|
const vouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id)
|
||||||
|
const voucherByTaskId = new Map<number, KuaishouIndustryVoucherRow>()
|
||||||
|
const voucherByUnitIndex = new Map<number, KuaishouIndustryVoucherRow>()
|
||||||
|
|
||||||
|
for (const voucher of vouchers) {
|
||||||
|
const taskId = Number(voucher.task_id || 0)
|
||||||
|
const unitIndex = Number(voucher.unit_index || 0)
|
||||||
|
if (taskId > 0 && !voucherByTaskId.has(taskId)) {
|
||||||
|
voucherByTaskId.set(taskId, voucher)
|
||||||
|
}
|
||||||
|
if (unitIndex > 0 && !voucherByUnitIndex.has(unitIndex)) {
|
||||||
|
voucherByUnitIndex.set(unitIndex, voucher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readyTaskIds: number[] = []
|
||||||
|
let pendingReason = ''
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
const taskId = Number(task.id || 0)
|
||||||
|
const unitIndex = Number(task.unit_index || 0)
|
||||||
|
const voucher = voucherByTaskId.get(taskId) || voucherByUnitIndex.get(unitIndex) || null
|
||||||
|
|
||||||
|
if (!voucher) {
|
||||||
|
pendingReason = pendingReason || '电子凭证发码通知尚未完成'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) {
|
||||||
|
pendingReason = pendingReason || resolveKuaishouIndustryVoucherSendCallbackMessage(voucher)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
readyTaskIds.push(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
voucherCount: vouchers.length,
|
||||||
|
readyTaskIds,
|
||||||
|
pendingReason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseJsonObject(value: unknown): JsonObject {
|
function parseJsonObject(value: unknown): JsonObject {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-re
|
|||||||
import { listTasksByOrderId } from '../../../repositories/task-repo.js'
|
import { listTasksByOrderId } from '../../../repositories/task-repo.js'
|
||||||
import {
|
import {
|
||||||
findKuaishouIndustryVoucherByCode,
|
findKuaishouIndustryVoucherByCode,
|
||||||
|
listKuaishouIndustryVouchersByOid,
|
||||||
|
listKuaishouIndustryVouchersForSendCallbackRetry,
|
||||||
updateKuaishouIndustryVoucherByCode,
|
updateKuaishouIndustryVoucherByCode,
|
||||||
upsertKuaishouIndustryVoucher,
|
upsertKuaishouIndustryVoucher,
|
||||||
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../../open-91/config.js'
|
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../../open-91/config.js'
|
||||||
|
import { retryOpen91Order } from '../ninetyone/order-service.js'
|
||||||
import { normalizeTimestampIso } from '../../../utils/time.js'
|
import { normalizeTimestampIso } from '../../../utils/time.js'
|
||||||
import { logIntegration } from '../../../utils/logger.js'
|
import { logIntegration } from '../../../utils/logger.js'
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +18,6 @@ import {
|
|||||||
import { normalizeSendCodePayload, assertSendCodePayload } from './payload.js'
|
import { normalizeSendCodePayload, assertSendCodePayload } from './payload.js'
|
||||||
import { assertKuaishouIndustrySignature } from './crypto.js'
|
import { assertKuaishouIndustrySignature } from './crypto.js'
|
||||||
import {
|
import {
|
||||||
buildIndustryErrorResponse,
|
|
||||||
buildIndustrySuccessResponse,
|
buildIndustrySuccessResponse,
|
||||||
buildIndustryEticketItem,
|
buildIndustryEticketItem,
|
||||||
buildIndustrySendCodeData,
|
buildIndustrySendCodeData,
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
import { sendCallback } from './send-callback-service.js'
|
import { sendCallback } from './send-callback-service.js'
|
||||||
import {
|
import {
|
||||||
buildKuaishouIndustryEticketFromVoucher,
|
buildKuaishouIndustryEticketFromVoucher,
|
||||||
|
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||||
KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS,
|
KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS,
|
||||||
resolveKuaishouIndustryVoucherValidity,
|
resolveKuaishouIndustryVoucherValidity,
|
||||||
} from './voucher-service.js'
|
} from './voucher-service.js'
|
||||||
@@ -41,6 +44,15 @@ type SendCodeCallbackParams = {
|
|||||||
ext?: string
|
ext?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SEND_CALLBACK_INITIAL_DELAY_MS = 3_000
|
||||||
|
const SEND_CALLBACK_SCAN_INTERVAL_MS = 15_000
|
||||||
|
const SEND_CALLBACK_MAX_ATTEMPTS = 8
|
||||||
|
const SEND_CALLBACK_BACKOFF_MS = [3_000, 10_000, 30_000, 60_000, 120_000, 300_000]
|
||||||
|
|
||||||
|
const sendCallbackRetryTimers = new Map<string, NodeJS.Timeout>()
|
||||||
|
let sendCallbackWorkerTimer: NodeJS.Timeout | null = null
|
||||||
|
let sendCallbackWorkerRunning = false
|
||||||
|
|
||||||
export async function handleSendCode(rawBody: JsonObject = {}) {
|
export async function handleSendCode(rawBody: JsonObject = {}) {
|
||||||
const config = getKuaishouIndustryConfig()
|
const config = getKuaishouIndustryConfig()
|
||||||
|
|
||||||
@@ -99,25 +111,6 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
|||||||
buildKuaishouIndustryEticketFromVoucher(voucher, params.eticketType),
|
buildKuaishouIndustryEticketFromVoucher(voucher, params.eticketType),
|
||||||
)
|
)
|
||||||
|
|
||||||
const callbackResult = await sendAndRecordCallback({
|
|
||||||
oid: normalizedOid,
|
|
||||||
sendType: params.sendType,
|
|
||||||
etickets,
|
|
||||||
vouchers,
|
|
||||||
sellerId: params.sellerId,
|
|
||||||
token: params.token,
|
|
||||||
eticketType: params.eticketType,
|
|
||||||
ext: params.ext,
|
|
||||||
preferredTotalGoodsValue: resolveSendCallbackPreferredTotalGoodsValue(order),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!callbackResult.success) {
|
|
||||||
return buildIndustryErrorResponse(
|
|
||||||
4010003,
|
|
||||||
resolveSendCallbackFailureMessage(callbackResult),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (order) {
|
if (order) {
|
||||||
await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, {
|
await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, {
|
||||||
source: 'kuaishou_industry_send_code',
|
source: 'kuaishou_industry_send_code',
|
||||||
@@ -125,6 +118,10 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleKuaishouIndustrySendCallbackRetry(normalizedOid, SEND_CALLBACK_INITIAL_DELAY_MS, {
|
||||||
|
source: 'send_code_accepted',
|
||||||
|
})
|
||||||
|
|
||||||
return buildIndustrySuccessResponse(
|
return buildIndustrySuccessResponse(
|
||||||
buildIndustrySendCodeData({
|
buildIndustrySendCodeData({
|
||||||
oid: normalizedOid,
|
oid: normalizedOid,
|
||||||
@@ -135,6 +132,26 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function startKuaishouIndustrySendCallbackRetryWorker() {
|
||||||
|
if (sendCallbackWorkerTimer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleSendCallbackWorkerScan(5_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopKuaishouIndustrySendCallbackRetryWorker() {
|
||||||
|
if (sendCallbackWorkerTimer) {
|
||||||
|
clearTimeout(sendCallbackWorkerTimer)
|
||||||
|
sendCallbackWorkerTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const timer of sendCallbackRetryTimers.values()) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
sendCallbackRetryTimers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
export async function resendKuaishouIndustryVoucherSendCallback(input: {
|
export async function resendKuaishouIndustryVoucherSendCallback(input: {
|
||||||
voucherCode: string
|
voucherCode: string
|
||||||
oid?: string
|
oid?: string
|
||||||
@@ -170,12 +187,273 @@ export async function resendKuaishouIndustryVoucherSendCallback(input: {
|
|||||||
})
|
})
|
||||||
const updatedVoucher = await findKuaishouIndustryVoucherByCode(voucher.voucher_code, voucher.oid)
|
const updatedVoucher = await findKuaishouIndustryVoucherByCode(voucher.voucher_code, voucher.oid)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
await syncOpen91OrderAfterSendCallbackSuccess(params.oid, {
|
||||||
|
source: 'admin_resend',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
voucher: updatedVoucher || voucher,
|
voucher: updatedVoucher || voucher,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runKuaishouIndustrySendCallbackForOid(
|
||||||
|
oid: string,
|
||||||
|
{ source = 'async_retry' }: { source?: string } = {},
|
||||||
|
) {
|
||||||
|
const normalizedOid = String(oid || '').trim()
|
||||||
|
if (!normalizedOid) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '缺少订单号,无法发起电子凭证发货回调',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vouchers = await listKuaishouIndustryVouchersByOid(normalizedOid)
|
||||||
|
const pendingVouchers = vouchers
|
||||||
|
.filter((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher))
|
||||||
|
.filter((voucher) =>
|
||||||
|
normalizePositiveInteger(voucher.send_callback_attempt_count) < SEND_CALLBACK_MAX_ATTEMPTS,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pendingVouchers.length === 0) {
|
||||||
|
return {
|
||||||
|
success: vouchers.length > 0,
|
||||||
|
skipped: true,
|
||||||
|
reason: vouchers.length > 0 ? 'send_callback_already_success_or_max_attempts' : 'voucher_missing',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPayload = parseJsonObject(pendingVouchers[0]?.raw_payload_json)
|
||||||
|
const body = parseJsonObject(rawPayload.body)
|
||||||
|
const firstVoucher = pendingVouchers[0]
|
||||||
|
if (!firstVoucher) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '缺少待回调电子凭证',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = resolveSendCodeCallbackParams(firstVoucher, body)
|
||||||
|
const eticketType = String(params.eticketType || '').trim()
|
||||||
|
const etickets = pendingVouchers.map((voucher) =>
|
||||||
|
buildKuaishouIndustryEticketFromVoucher(voucher, eticketType),
|
||||||
|
)
|
||||||
|
const order = await findLatestOrderByPlatformOrderId({
|
||||||
|
provider: OPEN_91_PROVIDER,
|
||||||
|
platform: OPEN_91_PLATFORM,
|
||||||
|
platformOrderId: normalizedOid,
|
||||||
|
})
|
||||||
|
const result = await sendAndRecordCallback({
|
||||||
|
oid: params.oid,
|
||||||
|
sendType: params.sendType,
|
||||||
|
etickets,
|
||||||
|
vouchers: pendingVouchers,
|
||||||
|
sellerId: params.sellerId,
|
||||||
|
token: params.token,
|
||||||
|
eticketType,
|
||||||
|
source,
|
||||||
|
preferredTotalGoodsValue: resolveSendCallbackPreferredTotalGoodsValue(order),
|
||||||
|
...(params.ext ? { ext: params.ext } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
await syncOpen91OrderAfterSendCallbackSuccess(normalizedOid, { source })
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAttemptCount =
|
||||||
|
Math.max(...pendingVouchers.map((voucher) =>
|
||||||
|
normalizePositiveInteger(voucher.send_callback_attempt_count),
|
||||||
|
)) + 1
|
||||||
|
if (
|
||||||
|
nextAttemptCount < SEND_CALLBACK_MAX_ATTEMPTS &&
|
||||||
|
isRetriableSendCallbackFailure(result)
|
||||||
|
) {
|
||||||
|
scheduleKuaishouIndustrySendCallbackRetry(normalizedOid, resolveSendCallbackBackoffMs(nextAttemptCount), {
|
||||||
|
source: `${source}_retry`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleKuaishouIndustrySendCallbackRetry(
|
||||||
|
oid: string,
|
||||||
|
delayMs: number,
|
||||||
|
{ source = 'async_retry' }: { source?: string } = {},
|
||||||
|
) {
|
||||||
|
const normalizedOid = String(oid || '').trim()
|
||||||
|
if (!normalizedOid || sendCallbackRetryTimers.has(normalizedOid)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedDelayMs = Math.max(0, Number(delayMs) || 0)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
sendCallbackRetryTimers.delete(normalizedOid)
|
||||||
|
void runKuaishouIndustrySendCallbackForOid(normalizedOid, { source }).catch((error) => {
|
||||||
|
logIntegration('[kuaishou-industry/send-code]', '异步电子凭证发货回调执行异常', {
|
||||||
|
source,
|
||||||
|
oid: normalizedOid,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}, { level: 'warn' })
|
||||||
|
scheduleKuaishouIndustrySendCallbackRetry(normalizedOid, resolveSendCallbackBackoffMs(1), {
|
||||||
|
source: `${source}_exception_retry`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, normalizedDelayMs)
|
||||||
|
|
||||||
|
sendCallbackRetryTimers.set(normalizedOid, timer)
|
||||||
|
logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调已进入异步队列', {
|
||||||
|
source,
|
||||||
|
oid: normalizedOid,
|
||||||
|
delayMs: normalizedDelayMs,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSendCallbackWorkerScan(delayMs: number) {
|
||||||
|
if (sendCallbackWorkerTimer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCallbackWorkerTimer = setTimeout(() => {
|
||||||
|
sendCallbackWorkerTimer = null
|
||||||
|
void runSendCallbackWorkerScan().finally(() => {
|
||||||
|
scheduleSendCallbackWorkerScan(SEND_CALLBACK_SCAN_INTERVAL_MS)
|
||||||
|
})
|
||||||
|
}, Math.max(0, Number(delayMs) || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSendCallbackWorkerScan() {
|
||||||
|
if (sendCallbackWorkerRunning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCallbackWorkerRunning = true
|
||||||
|
try {
|
||||||
|
const vouchers = await listKuaishouIndustryVouchersForSendCallbackRetry(200)
|
||||||
|
const dueOids = new Set<string>()
|
||||||
|
for (const voucher of vouchers) {
|
||||||
|
if (isVoucherDueForSendCallbackRetry(voucher)) {
|
||||||
|
dueOids.add(String(voucher.oid || '').trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const oid of dueOids) {
|
||||||
|
scheduleKuaishouIndustrySendCallbackRetry(oid, 0, {
|
||||||
|
source: 'worker_scan',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logIntegration('[kuaishou-industry/send-code]', '扫描待重试电子凭证发货回调失败', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}, { level: 'warn' })
|
||||||
|
} finally {
|
||||||
|
sendCallbackWorkerRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVoucherDueForSendCallbackRetry(voucher: KuaishouIndustryVoucherRow) {
|
||||||
|
if (isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptCount = normalizePositiveInteger(voucher.send_callback_attempt_count)
|
||||||
|
if (attemptCount >= SEND_CALLBACK_MAX_ATTEMPTS) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseAt = voucher.send_callback_sent_at || voucher.created_at
|
||||||
|
const baseTime = Date.parse(String(baseAt || ''))
|
||||||
|
const elapsedMs = Date.now() - (Number.isFinite(baseTime) ? baseTime : 0)
|
||||||
|
const requiredDelayMs = attemptCount === 0
|
||||||
|
? SEND_CALLBACK_INITIAL_DELAY_MS
|
||||||
|
: resolveSendCallbackBackoffMs(attemptCount)
|
||||||
|
|
||||||
|
return elapsedMs >= requiredDelayMs
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSendCallbackBackoffMs(attemptCount: unknown) {
|
||||||
|
const attempt = Math.max(1, normalizePositiveInteger(attemptCount))
|
||||||
|
return SEND_CALLBACK_BACKOFF_MS[Math.min(attempt, SEND_CALLBACK_BACKOFF_MS.length - 1)] ||
|
||||||
|
SEND_CALLBACK_BACKOFF_MS[SEND_CALLBACK_BACKOFF_MS.length - 1] ||
|
||||||
|
SEND_CALLBACK_INITIAL_DELAY_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncOpen91OrderAfterSendCallbackSuccess(
|
||||||
|
oid: string,
|
||||||
|
{ source = 'send_callback_success' }: { source?: string } = {},
|
||||||
|
) {
|
||||||
|
const normalizedOid = String(oid || '').trim()
|
||||||
|
if (!normalizedOid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = await findLatestOrderByPlatformOrderId({
|
||||||
|
provider: OPEN_91_PROVIDER,
|
||||||
|
platform: OPEN_91_PLATFORM,
|
||||||
|
platformOrderId: normalizedOid,
|
||||||
|
})
|
||||||
|
if (!order) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = await listTasksByOrderId(order.id)
|
||||||
|
if (tasks.length > 0) {
|
||||||
|
await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, {
|
||||||
|
source,
|
||||||
|
now: normalizeTimestampIso(new Date().toISOString()),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await retryOpen91Order(order.id)
|
||||||
|
logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调成功后已触发 91 订单重试', {
|
||||||
|
source,
|
||||||
|
oid: normalizedOid,
|
||||||
|
orderId: order.id,
|
||||||
|
taskCount: result.tasks.length,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调成功后重试 91 订单失败', {
|
||||||
|
source,
|
||||||
|
oid: normalizedOid,
|
||||||
|
orderId: order.id,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}, { level: 'warn' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRetriableSendCallbackFailure(result: Awaited<ReturnType<typeof sendCallback>>) {
|
||||||
|
if (result.success) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(result.error || '').trim()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = parseJsonObject(result.response)
|
||||||
|
const marker = [
|
||||||
|
response.result,
|
||||||
|
response.code,
|
||||||
|
response.sub_code,
|
||||||
|
response.error_msg,
|
||||||
|
response.sub_msg,
|
||||||
|
response.msg,
|
||||||
|
].map((item) => String(item || '').trim()).join(' ')
|
||||||
|
|
||||||
|
return (
|
||||||
|
marker.includes('9994') ||
|
||||||
|
marker.includes('807000') ||
|
||||||
|
marker.includes('并发冲突')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function sendAndRecordCallback(input: {
|
async function sendAndRecordCallback(input: {
|
||||||
oid: string
|
oid: string
|
||||||
sendType: string
|
sendType: string
|
||||||
@@ -254,7 +532,7 @@ async function sendAndRecordCallback(input: {
|
|||||||
))
|
))
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调执行失败,发码请求已拒绝继续', {
|
logIntegration('[kuaishou-industry/send-code]', '电子凭证发货回调执行失败,等待异步重试', {
|
||||||
source: input.source || 'send_code',
|
source: input.source || 'send_code',
|
||||||
oid: input.oid,
|
oid: input.oid,
|
||||||
error: result.error || '',
|
error: result.error || '',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { runDatabaseMigrations } from "../db/migrate.js";
|
import { runDatabaseMigrations } from "../db/migrate.js";
|
||||||
import { ensureAdminUsersBootstrapped } from "../services/admin/admin-auth-service.js";
|
import { ensureAdminUsersBootstrapped } from "../services/admin/admin-auth-service.js";
|
||||||
import { ensureFulfillmentCatalogBootstrapped } from "../services/bootstrap/fulfillment-bootstrap-service.js";
|
import { ensureFulfillmentCatalogBootstrapped } from "../services/bootstrap/fulfillment-bootstrap-service.js";
|
||||||
|
import { startKuaishouIndustrySendCallbackRetryWorker } from "../services/platforms/kuaishou-industry/send-code-service.js";
|
||||||
import { startScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
import { startScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
||||||
import { logError, logInfo } from "../utils/logger.js";
|
import { logError, logInfo } from "../utils/logger.js";
|
||||||
import { formatStartupError, type StartupState } from "./state.js";
|
import { formatStartupError, type StartupState } from "./state.js";
|
||||||
@@ -42,6 +43,7 @@ export async function bootstrapCoreServices(
|
|||||||
});
|
});
|
||||||
|
|
||||||
startScheduledJobs();
|
startScheduledJobs();
|
||||||
|
startKuaishouIndustrySendCallbackRetryWorker();
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = formatStartupError(error);
|
const message = formatStartupError(error);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Server } from "node:http";
|
import type { Server } from "node:http";
|
||||||
|
|
||||||
|
import { stopKuaishouIndustrySendCallbackRetryWorker } from "../services/platforms/kuaishou-industry/send-code-service.js";
|
||||||
import { stopScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
import { stopScheduledJobs } from "../services/scheduler/scheduler-service.js";
|
||||||
import { logError, logInfo } from "../utils/logger.js";
|
import { logError, logInfo } from "../utils/logger.js";
|
||||||
import type { StartupState } from "./state.js";
|
import type { StartupState } from "./state.js";
|
||||||
@@ -20,6 +21,7 @@ export function createShutdownController(
|
|||||||
logInfo("[shutdown]", `received ${signal}, closing HTTP server`);
|
logInfo("[shutdown]", `received ${signal}, closing HTTP server`);
|
||||||
|
|
||||||
stopScheduledJobs();
|
stopScheduledJobs();
|
||||||
|
stopKuaishouIndustrySendCallbackRetryWorker();
|
||||||
|
|
||||||
if (!server.listening) {
|
if (!server.listening) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user