From 6d6e64beda659b15ae1caf5a453f73af514b1bda Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 14:53:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=A1=A5=E5=85=85=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E8=AF=A6=E6=83=85=E6=9C=8D=E5=8A=A1=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platforms/agiso/shop-config-service.js | 2 + .../agiso/xianyu/order-detail-service.js | 18 +- .../agiso/xianyu/order-detail-service.test.js | 219 ++++++++++++++++++ docs/backend-typescript-migration-plan.md | 12 + 4 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.test.js diff --git a/apps/backend/src/services/platforms/agiso/shop-config-service.js b/apps/backend/src/services/platforms/agiso/shop-config-service.js index 75d4342e..12e360ce 100644 --- a/apps/backend/src/services/platforms/agiso/shop-config-service.js +++ b/apps/backend/src/services/platforms/agiso/shop-config-service.js @@ -16,6 +16,8 @@ const AGISO_SHOP_CONFIG_KEYS = [ 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint', + 'tradeDetailApiVersion', + 'tradeDetailTimeoutMs', ] export function getAgisoShopsFilePath() { diff --git a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js index 1418972f..0e9d3b71 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js @@ -12,11 +12,17 @@ import { parseJsonObject } from '../../../../utils/json.js' const DEFAULT_DETAIL_TIMEOUT_MS = 5000 export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {}) { + return enrichAgisoXianyuTradeOrderWithDeps(parsed, { requestId }) +} + +export async function enrichAgisoXianyuTradeOrderWithDeps(parsed, { requestId = '' } = {}, deps = {}) { + const queryOrderDetail = deps.queryAgisoXianyuOrderDetail || queryAgisoXianyuOrderDetail + if (!shouldHydrateAgisoXianyuTradeOrder(parsed)) { return { parsed, enriched: false, reason: 'not_needed' } } - const detailResult = await queryAgisoXianyuOrderDetail({ + const detailResult = await queryOrderDetail({ shopId: parsed.shopId, platformOrderId: parsed.platformOrderId, requestId, @@ -58,6 +64,14 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = { } export async function queryAgisoXianyuOrderDetail({ shopId = '', platformOrderId = '', requestId = '' } = {}) { + return queryAgisoXianyuOrderDetailWithDeps({ shopId, platformOrderId, requestId }) +} + +export async function queryAgisoXianyuOrderDetailWithDeps( + { shopId = '', platformOrderId = '', requestId = '' } = {}, + deps = {}, +) { + const requestWithTimeout = deps.fetchWithTimeout || fetchWithTimeout const normalizedShopId = String(shopId || '').trim() const normalizedPlatformOrderId = String(platformOrderId || '').trim() const config = resolveAgisoXianyuTradeDetailConfig(normalizedShopId) @@ -94,7 +108,7 @@ export async function queryAgisoXianyuOrderDetail({ shopId = '', platformOrderId }) try { - const response = await fetchWithTimeout(config.endpoint, { + const response = await requestWithTimeout(config.endpoint, { method: 'POST', headers: requestHeaders, body: new URLSearchParams(requestBody).toString(), diff --git a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.test.js b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.test.js new file mode 100644 index 00000000..6277808f --- /dev/null +++ b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.test.js @@ -0,0 +1,219 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { runtimeConfig } from '../../../../config/runtime.js' +import { + enrichAgisoXianyuTradeOrderWithDeps, + queryAgisoXianyuOrderDetailWithDeps, + resolveAgisoXianyuOrderDeliveryState, +} from './order-detail-service.js' + +test('resolveAgisoXianyuOrderDeliveryState detects shipped order by ship time or status', () => { + assert.deepEqual(resolveAgisoXianyuOrderDeliveryState({ ship_time: 1713097840 }), { + shipped: true, + shipTime: 1713097840, + orderStatus: 0, + }) + + assert.deepEqual(resolveAgisoXianyuOrderDeliveryState({ order_status: 3 }), { + shipped: true, + shipTime: 0, + orderStatus: 3, + }) + + assert.deepEqual(resolveAgisoXianyuOrderDeliveryState({ order_status: 2 }), { + shipped: false, + shipTime: 0, + orderStatus: 2, + }) +}) + +test('queryAgisoXianyuOrderDetailWithDeps parses successful detail payload and masks network dependency', async () => { + const originalShops = runtimeConfig.platforms.agiso.messaging.shops + const originalAppSecret = runtimeConfig.platforms.agiso.appSecret + const calls = [] + + runtimeConfig.platforms.agiso.appSecret = '' + runtimeConfig.platforms.agiso.messaging.shops = { + 'shop-detail-test': { + accessToken: 'access-token', + appSecret: 'shop-secret', + tradeDetailEndpoint: 'https://example.com/detail', + tradeDetailApiVersion: '2', + tradeDetailTimeoutMs: '3000', + }, + } + + try { + const result = await queryAgisoXianyuOrderDetailWithDeps({ + shopId: 'shop-detail-test', + platformOrderId: 'P-DETAIL-10001', + requestId: 'req-detail', + }, { + fetchWithTimeout: async (url, options, timeoutMs) => { + calls.push({ url, options, timeoutMs }) + return { + status: 200, + text: async () => JSON.stringify({ + IsSuccess: true, + Data: { + total_fee: '25.50', + pay_time: '2026-04-14 20:30:40', + ship_time: 1713097840, + order_status: 3, + item: { + item_id: 'item-1', + sku: 'dnf-cdk-a|商品名称:DNF礼包', + quantity: 2, + }, + }, + }), + } + }, + }) + + assert.equal(result.success, true) + assert.equal(result.responseStatus, 200) + assert.equal(result.totalAmountFen, 2550) + assert.equal(result.shipped, true) + assert.equal(result.shipTime, 1713097840) + assert.equal(result.orderStatus, 3) + assert.equal(calls[0]?.url, 'https://example.com/detail') + assert.equal(calls[0]?.timeoutMs, 3000) + assert.equal(calls[0]?.options.headers.Authorization, 'Bearer access-token') + assert.equal(calls[0]?.options.headers.ApiVersion, '2') + assert.match(String(calls[0]?.options.body), /tid=P-DETAIL-10001/) + assert.match(String(calls[0]?.options.body), /sign=/) + } finally { + runtimeConfig.platforms.agiso.messaging.shops = originalShops + runtimeConfig.platforms.agiso.appSecret = originalAppSecret + } +}) + +test('queryAgisoXianyuOrderDetailWithDeps returns business_error with structured message', async () => { + const originalShops = runtimeConfig.platforms.agiso.messaging.shops + + runtimeConfig.platforms.agiso.messaging.shops = { + 'shop-detail-test': { + accessToken: 'access-token', + appSecret: 'shop-secret', + tradeDetailEndpoint: 'https://example.com/detail', + }, + } + + try { + const result = await queryAgisoXianyuOrderDetailWithDeps({ + shopId: 'shop-detail-test', + platformOrderId: 'P-DETAIL-10002', + }, { + fetchWithTimeout: async () => ({ + status: 200, + text: async () => JSON.stringify({ + IsSuccess: false, + Error_Msg: '订单不存在', + }), + }), + }) + + assert.equal(result.success, false) + assert.equal(result.reason, 'business_error') + assert.equal(result.errorMessage, '订单不存在') + assert.equal(result.responseStatus, 200) + } finally { + runtimeConfig.platforms.agiso.messaging.shops = originalShops + } +}) + +test('queryAgisoXianyuOrderDetailWithDeps skips missing config without fetching', async () => { + const originalShops = runtimeConfig.platforms.agiso.messaging.shops + const originalAppSecret = runtimeConfig.platforms.agiso.appSecret + let fetchCalled = false + + runtimeConfig.platforms.agiso.messaging.shops = {} + runtimeConfig.platforms.agiso.appSecret = '' + + try { + const result = await queryAgisoXianyuOrderDetailWithDeps({ + shopId: 'missing-shop', + platformOrderId: 'P-DETAIL-10003', + }, { + fetchWithTimeout: async () => { + fetchCalled = true + return { status: 200, text: async () => '{}' } + }, + }) + + assert.equal(result.success, false) + assert.equal(result.reason, 'missing_config') + assert.equal(fetchCalled, false) + } finally { + runtimeConfig.platforms.agiso.messaging.shops = originalShops + runtimeConfig.platforms.agiso.appSecret = originalAppSecret + } +}) + +test('enrichAgisoXianyuTradeOrderWithDeps merges detail payload into incomplete trade', async () => { + const parsed = { + provider: 'agiso', + platform: 'xianyu', + shopId: 'shop-detail-test', + shopName: '', + platformOrderId: 'P-DETAIL-10004', + totalAmount: 0, + paidAt: null, + buyerId: '', + buyerName: '', + receiverContact: '', + rawPayload: {}, + items: [], + } + + const result = await enrichAgisoXianyuTradeOrderWithDeps(parsed, { requestId: 'req-enrich' }, { + queryAgisoXianyuOrderDetail: async (input) => { + assert.deepEqual(input, { + shopId: 'shop-detail-test', + platformOrderId: 'P-DETAIL-10004', + requestId: 'req-enrich', + }) + + return { + success: true, + reason: '', + errorMessage: '', + responseStatus: 200, + payload: {}, + detailPayload: { + total_fee: '19.90', + pay_time: '2026-04-14 20:30:40', + buyer_id: 'buyer-1', + buyer_name: '测试买家', + receiver_mobile: '13800138000', + shop_name: '详情店铺', + items: [ + { + item_id: 'item-1', + sku: 'dnf-cdk-a|商品名称:DNF礼包', + quantity: 2, + }, + ], + }, + totalAmountFen: 1990, + shipped: false, + shipTime: 0, + orderStatus: 2, + } + }, + }) + + assert.equal(result.enriched, true) + assert.equal(result.totalAmountFen, 1990) + assert.equal(result.parsed.totalAmount, 1990) + assert.equal(result.parsed.buyerId, 'buyer-1') + assert.equal(result.parsed.buyerName, '测试买家') + assert.equal(result.parsed.receiverContact, '13800138000') + assert.equal(result.parsed.shopName, '详情店铺') + assert.equal(result.parsed.items.length, 1) + assert.equal(result.parsed.items[0]?.skuCode, 'dnf-cdk-a') + assert.equal(result.parsed.items[0]?.quantity, 2) + assert.deepEqual(result.parsed.rawPayload._agisoTradeDetail.total_fee, '19.90') +}) diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 6403bfb1..53569525 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -337,6 +337,18 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 131 个用例通过 +54. Agiso 订单详情服务迁移前补直接测试: + - 新增 `src/services/platforms/agiso/xianyu/order-detail-service.test.js` + - 新增 `enrichAgisoXianyuTradeOrderWithDeps` 与 `queryAgisoXianyuOrderDetailWithDeps`,便于隔离订单详情 HTTP 请求 + - 覆盖发货状态识别、详情成功解析、业务失败、缺配置跳过、详情补全 webhook 订单五个关键分支 +55. 修复 Agiso 店铺配置白名单: + - `tradeDetailApiVersion` + - `tradeDetailTimeoutMs` +56. Docker 内验证通过: + - `src/services/platforms/agiso/xianyu/order-detail-service.test.js` 共 5 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 136 个用例通过 ## 下一步建议