diff --git a/apps/backend/src/repositories/webhook-event-repo.js b/apps/backend/src/repositories/webhook-event-repo.js index 28f20710..4aa698e2 100644 --- a/apps/backend/src/repositories/webhook-event-repo.js +++ b/apps/backend/src/repositories/webhook-event-repo.js @@ -75,6 +75,7 @@ export async function listWebhookEvents({ pageSize = 20, provider = '', platform = '', + platformOrderId = '', processed = '', relatedOrderId = '', dateFrom = '', @@ -94,6 +95,11 @@ export async function listWebhookEvents({ filters.push(`platform = $${params.length}`) } + if (platformOrderId) { + params.push(`%${platformOrderId}%`) + filters.push(`(event_key ILIKE $${params.length} OR body_json::text ILIKE $${params.length})`) + } + if (processed === '0' || processed === '1') { params.push(processed === '1') filters.push(`processed = $${params.length}`) diff --git a/apps/backend/src/services/admin/admin-service.js b/apps/backend/src/services/admin/admin-service.js index 6cac2a45..a8a42f67 100644 --- a/apps/backend/src/services/admin/admin-service.js +++ b/apps/backend/src/services/admin/admin-service.js @@ -41,6 +41,7 @@ import { nowIso } from '../../utils/time.js' import { reserveInventoryForTask } from '../order/inventory-service.js' import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js' import { syncConfiguredFulfillmentBindings } from '../bootstrap/fulfillment-bootstrap-service.js' +import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from '../order/agiso-trade-parsing.js' import { normalizeProductName } from '../order/product-match-service.js' import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js' import { normalizeAdminRole } from './admin-auth-service.js' @@ -493,6 +494,7 @@ export async function getAdminWebhookEvents(query = {}) { pageSize, provider: String(query.provider || '').trim(), platform: String(query.platform || '').trim(), + platformOrderId: String(query.platformOrderId || '').trim(), processed: String(query.processed || '').trim(), relatedOrderId: String(query.relatedOrderId || '').trim(), dateFrom: normalizeDateQuery(query.dateFrom), @@ -1404,7 +1406,6 @@ async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) { const body = normalizeRecord(safeParseJson(item.body_json)) const payload = extractWebhookPayload(body) const itemSources = extractWebhookItemSources(payload) - const firstItem = itemSources[0] || {} const rawAmount = pickFirstNonEmpty([ payload.total_fee, payload.totalFee, @@ -1473,18 +1474,7 @@ async function mapAdminWebhookEvent(item, { includeRaw = false } = {}) { processError: item.process_error, relatedOrderId: item.related_order_id, createdAt: item.created_at, - platformOrderId: pickFirstNonEmpty([ - payload.biz_order_id, - payload.bizOrderId, - payload.Tid, - payload.tid, - payload.Oid, - payload.oid, - payload.order_id, - payload.orderId, - firstItem.Oid, - firstItem.oid, - ]), + platformOrderId: resolveAgisoTradePlatformOrderId(payload), buyerId: pickFirstNonEmpty([ payload.buyer_id, payload.buyerId, @@ -2238,14 +2228,7 @@ function isPlainObject(value) { } function extractWebhookPayload(body) { - const normalizedBody = normalizeRecord(body) - const rawJson = String(normalizedBody.json || normalizedBody.JSON || '').trim() - - if (rawJson) { - return normalizeRecord(safeParseJson(rawJson)) - } - - return normalizedBody + return extractAgisoTradePayload(body) } function extractWebhookItemSources(payload) { diff --git a/apps/backend/src/services/order/agiso-trade-parsing.js b/apps/backend/src/services/order/agiso-trade-parsing.js new file mode 100644 index 00000000..96b5051d --- /dev/null +++ b/apps/backend/src/services/order/agiso-trade-parsing.js @@ -0,0 +1,71 @@ +import { parseJsonObject } from '../../utils/json.js' + +export function extractAgisoTradePayload(body) { + const normalizedBody = normalizeRecord(body) + const rawJson = String(normalizedBody.json || normalizedBody.JSON || '').trim() + + if (rawJson) { + return normalizeRecord(parseJsonObject(rawJson, { preserveLargeIntegers: true })) + } + + return normalizedBody +} + +export function resolveAgisoTradePlatformOrderId(payload) { + const normalizedPayload = normalizeRecord(payload) + const firstItem = extractAgisoTradeOrderItemSources(normalizedPayload)[0] || {} + + return pickFirstNonEmpty([ + normalizedPayload.biz_order_id, + normalizedPayload.Tid, + normalizedPayload.tid, + normalizedPayload.Oid, + normalizedPayload.oid, + normalizedPayload.order_id, + normalizedPayload.orderId, + firstItem.Oid, + firstItem.oid, + ]) +} + +export function extractAgisoTradeOrderItemSources(payload) { + const normalizedPayload = normalizeRecord(payload) + const candidates = [ + normalizedPayload.items, + normalizedPayload.Items, + normalizedPayload.orders, + normalizedPayload.Orders, + normalizedPayload.order_list, + normalizedPayload.OrderList, + ] + + for (const current of candidates) { + if (Array.isArray(current) && current.length > 0) { + return current.map((item) => normalizeRecord(item)).filter((item) => Object.keys(item).length > 0) + } + } + + if (Object.keys(normalizedPayload).length > 0) { + return [normalizedPayload] + } + + return [] +} + +function normalizeRecord(value) { + return value && typeof value === 'object' && !Array.isArray(value) ? value : {} +} + +function pickFirstNonEmpty(values) { + for (const value of values) { + if (typeof value === 'string' && value.trim()) { + return value.trim() + } + + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value) + } + } + + return '' +} diff --git a/apps/backend/src/services/order/agiso-trade-parsing.test.js b/apps/backend/src/services/order/agiso-trade-parsing.test.js new file mode 100644 index 00000000..4f85b9d5 --- /dev/null +++ b/apps/backend/src/services/order/agiso-trade-parsing.test.js @@ -0,0 +1,31 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from './agiso-trade-parsing.js' + +test('extractAgisoTradePayload preserves large integer order ids from raw json', () => { + const payload = extractAgisoTradePayload({ + json: '{"biz_order_id":4502259793206016214,"item_id":978102431355,"order_status":4}', + }) + + assert.equal(payload.biz_order_id, '4502259793206016214') + assert.equal(typeof payload.biz_order_id, 'string') +}) + +test('resolveAgisoTradePlatformOrderId keeps exact webhook order id', () => { + const payload = extractAgisoTradePayload({ + json: '{"biz_order_id":4502259793206016214,"item_id":978102431355,"order_status":4}', + }) + + assert.equal(resolveAgisoTradePlatformOrderId(payload), '4502259793206016214') +}) + +test('resolveAgisoTradePlatformOrderId falls back to tid when biz_order_id is absent', () => { + assert.equal( + resolveAgisoTradePlatformOrderId({ + tid: '4502251008019011224', + orders: [{ oid: '4502251008019011888' }], + }), + '4502251008019011224', + ) +}) diff --git a/apps/backend/src/services/order/webhook-service.js b/apps/backend/src/services/order/webhook-service.js index a2ce1f78..d2951a91 100644 --- a/apps/backend/src/services/order/webhook-service.js +++ b/apps/backend/src/services/order/webhook-service.js @@ -3,6 +3,10 @@ import crypto from 'node:crypto' import { runtimeConfig } from '../../config/runtime.js' import { createWebhookEvent, updateWebhookEvent } from '../../repositories/webhook-event-repo.js' import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js' +import { + extractAgisoTradePayload, + resolveAgisoTradePlatformOrderId, +} from './agiso-trade-parsing.js' import { upsertOrderFromWebhook } from './order-service.js' import { hasConfiguredOrderItems } from './product-match-service.js' import { createHttpError } from '../../utils/http.js' @@ -305,7 +309,7 @@ function parseAgisoTradeRequest(requestLike) { const query = normalizeRecord(requestLike.query) const body = normalizeRecord(requestLike.body) const rawJson = String(body.json || '').trim() - const payload = rawJson ? parsePayloadJson(rawJson) : body + const payload = rawJson ? parsePayloadJson(rawJson) : extractAgisoTradePayload(body) const timestamp = String(query.timestamp || '').trim() const sign = String(query.sign || '').trim().toLowerCase() const provider = 'agiso' @@ -323,19 +327,7 @@ function parseAgisoTradeRequest(requestLike) { const shop = resolveShop(payload) const eventType = resolveEventType(query.aopic, payload) const signatureValid = verifyAgisoSignature({ rawJson, timestamp, sign }) - const itemSources = extractOrderItemSources(payload) - const firstItem = itemSources[0] || {} - const platformOrderId = pickFirstNonEmpty([ - payload.biz_order_id, - payload.Tid, - payload.tid, - payload.Oid, - payload.oid, - payload.order_id, - payload.orderId, - firstItem.Oid, - firstItem.oid, - ]) + const platformOrderId = resolveAgisoTradePlatformOrderId(payload) return { provider, @@ -663,25 +655,6 @@ function normalizeOrderItems(payload) { }) } -function extractOrderItemSources(payload) { - const candidates = [ - payload.items, - payload.Items, - payload.orders, - payload.Orders, - payload.order_list, - payload.OrderList, - ] - - for (const current of candidates) { - if (Array.isArray(current) && current.length > 0) { - return current - } - } - - return [payload] -} - function resolveBusinessPlatform(rawPlatform) { const normalized = normalizePlatformKey(rawPlatform) @@ -910,6 +883,10 @@ function isPlainObject(value) { } function safeParseJson(rawValue) { + if (rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) { + return rawValue + } + const normalized = String(rawValue || '').trim() if (!normalized) { diff --git a/apps/frontend/src/views/admin/AdminWebhookEventsView.vue b/apps/frontend/src/views/admin/AdminWebhookEventsView.vue index b85c7e08..6c845659 100644 --- a/apps/frontend/src/views/admin/AdminWebhookEventsView.vue +++ b/apps/frontend/src/views/admin/AdminWebhookEventsView.vue @@ -17,6 +17,7 @@ const errorMessage = ref('') const items = ref([]) const provider = ref('') const platform = ref('') +const platformOrderId = ref('') const processed = ref('') const relatedOrderId = ref('') const dateFrom = ref('') @@ -37,6 +38,7 @@ async function loadEvents(page = pagination.value.page) { pageSize: pagination.value.pageSize, provider: provider.value.trim(), platform: platform.value.trim(), + platformOrderId: platformOrderId.value.trim(), processed: processed.value.trim(), relatedOrderId: relatedOrderId.value.trim(), dateFrom: dateFrom.value.trim(), @@ -114,10 +116,11 @@ onMounted(loadEvents) {{ option.label }} - +
+ 查询