From 861b7e00349e0cc296d1bf840cd81e6375012e04 Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 14:42:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=A1=A5=E5=85=85=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8F=91=E8=B4=A7=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 --- .../agiso/xianyu/auto-delivery-service.js | 49 +++-- .../xianyu/auto-delivery-service.test.js | 179 ++++++++++++++++++ docs/backend-typescript-migration-plan.md | 8 + 3 files changed, 220 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js index e72169e6..ba058c90 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js @@ -28,6 +28,18 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ task, trigger = 'task_delivered', } = {}) { + return ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ order, task, trigger }) +} + +export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ + order, + task, + trigger = 'task_delivered', +} = {}, deps = {}) { + const listOrderTasks = deps.listTasksByOrderId || listTasksByOrderId + const sendRequest = deps.fetch || fetch + const confirmShipped = deps.confirmAgisoXianyuAutoDeliveryShipped || confirmAgisoXianyuAutoDeliveryShipped + if (!isAgisoXianyuOrder(order) || !task?.id) { return { sent: false, skipped: true, reason: 'not_supported', task } } @@ -36,7 +48,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ return { sent: false, skipped: true, reason: 'task_not_delivered', task } } - const orderTasks = await listTasksByOrderId(order.id) + const orderTasks = await listOrderTasks(order.id) if (hasAgisoAutoDeliverySucceeded(orderTasks)) { return { sent: false, skipped: true, reason: 'already_sent', task } } @@ -64,7 +76,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ trigger, reason: 'auto_delivery_disabled', order, - }) + }, deps) } if (!config.endpoint || !config.accessToken || !config.appSecret) { @@ -78,7 +90,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ hasAccessToken: Boolean(config.accessToken), hasAppSecret: Boolean(config.appSecret), }, - }) + }, deps) } const requestHeaders = buildRequestHeaders({ @@ -100,7 +112,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ }) try { - const response = await fetch(config.endpoint, { + const response = await sendRequest(config.endpoint, { method: 'POST', headers: requestHeaders, body: new URLSearchParams(requestBody).toString(), @@ -113,7 +125,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ const requestId = String(parsed?.RequestId || '').trim() // 发货接口返回成功后,再查一次 Order/Detail 确认订单真的进入已发货状态 meow~ - const confirmResult = await confirmAgisoXianyuAutoDeliveryShipped({ + const confirmResult = await confirmShipped({ shopId: order.shop_id, platformOrderId: order.platform_order_id, requestId, @@ -146,7 +158,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ confirmOrderStatus: confirmResult.orderStatus, confirmShipTime: confirmResult.shipTime, }, - }) + }, deps) } logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功,订单已确认进入已发货状态', { @@ -173,10 +185,10 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ confirmOrderStatus: confirmResult.orderStatus, confirmShipTime: confirmResult.shipTime, }, - }) + }, deps) // 发货确认成功后,发送自定义消息通知 meow~ - await sendAutoDeliveryMessage({ order, task }) + await sendAutoDeliveryMessage({ order, task }, deps) return result } @@ -201,7 +213,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ responseStatus: response.status, response: parsed, errorMessage, - }) + }, deps) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼自动发货失败') logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货异常', { @@ -221,7 +233,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ responseStatus: 0, response: {}, errorMessage, - }) + }, deps) } } @@ -333,8 +345,11 @@ async function persistAgisoAutoDeliveryResult(task, { response = {}, errorMessage = '', detail = {}, -} = {}) { - const now = nowIso() +} = {}, deps = {}) { + const getNowIso = deps.nowIso || nowIso + const patchTask = deps.updateTask || updateTask + const insertTaskEvent = deps.createTaskEvent || createTaskEvent + const now = getNowIso() const currentContext = parseTaskContext(task) const nextContext = { ...currentContext, @@ -350,13 +365,13 @@ async function persistAgisoAutoDeliveryResult(task, { ...detail, }, } - const updatedTask = await updateTask(task.id, { + const updatedTask = await patchTask(task.id, { context_json: JSON.stringify(nextContext), updated_at: now, }) if (status === 'success' || status === 'failed' || status === 'skipped') { - await createTaskEvent(task.id, `agiso_auto_delivery_${status}`, { + await insertTaskEvent(task.id, `agiso_auto_delivery_${status}`, { trigger: String(trigger || '').trim(), reason: String(reason || '').trim(), platformOrderId: String(order?.platform_order_id || '').trim(), @@ -471,9 +486,11 @@ async function confirmAgisoXianyuAutoDeliveryShipped({ shopId = '', platformOrde /** * 自动发货成功后发送消息通知 meow~ */ -async function sendAutoDeliveryMessage({ order, task } = {}) { +async function sendAutoDeliveryMessage({ order, task } = {}, deps = {}) { try { - const result = await ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ order, task }) + const deliverMessage = deps.ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask + || ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask + const result = await deliverMessage({ order, task }) logWebhook('[agiso/xianyu/auto-delivery]', '自动发货消息通知结果', { orderId: order?.id, taskId: task?.id, diff --git a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js index 2714517f..5284b13f 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.test.js @@ -1,14 +1,36 @@ import test from 'node:test' import assert from 'node:assert/strict' +import { runtimeConfig } from '../../../../config/runtime.js' import { hasAgisoAutoDeliverySucceeded, + ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps, isOrderReadyForAgisoAutoDelivery, isAgisoAutoDeliverySuccess, resolveAgisoAutoDeliveryEndpoint, resolveAgisoAutoDeliveryErrorMessage, } from './auto-delivery-service.js' +const agisoOrder = { + id: 101, + provider: 'agiso', + platform: 'xianyu', + shop_id: 'shop-auto-delivery-test', + shop_name: '自动发货测试店', + platform_order_id: 'P-AUTO-10001', +} + +function createDeliveredTask(patch = {}) { + return { + id: 201, + order_id: agisoOrder.id, + task_status: 'completed', + delivery_status: 'delivered', + context_json: '{}', + ...patch, + } +} + test('isOrderReadyForAgisoAutoDelivery requires every task to be delivered', () => { assert.equal(isOrderReadyForAgisoAutoDelivery([]), false) assert.equal(isOrderReadyForAgisoAutoDelivery([ @@ -83,3 +105,160 @@ test('resolveAgisoAutoDeliveryEndpoint falls back to DummySend', () => { 'https://gw-api.agiso.com/aldsIdle/Order/DummySend', ) }) + +test('ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps skips while other order tasks are not delivered', async () => { + const calls = { + update: 0, + fetch: 0, + } + + const result = await ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ + order: agisoOrder, + task: createDeliveredTask(), + }, { + listTasksByOrderId: async () => [ + createDeliveredTask(), + createDeliveredTask({ id: 202, delivery_status: 'processing' }), + ], + updateTask: async () => { + calls.update += 1 + return null + }, + fetch: async () => { + calls.fetch += 1 + return { status: 200, text: async () => '{}' } + }, + }) + + assert.equal(result.sent, false) + assert.equal(result.skipped, true) + assert.equal(result.reason, 'waiting_other_tasks') + assert.equal(calls.update, 0) + assert.equal(calls.fetch, 0) +}) + +test('ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps persists missing config as skipped', async () => { + const originalAppSecret = runtimeConfig.platforms.agiso.appSecret + const originalShops = runtimeConfig.platforms.agiso.messaging.shops + const updates = [] + const events = [] + + runtimeConfig.platforms.agiso.appSecret = '' + runtimeConfig.platforms.agiso.messaging.shops = {} + + try { + const task = createDeliveredTask({ context_json: JSON.stringify({ existing: true }) }) + const result = await ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ + order: { + ...agisoOrder, + shop_id: 'missing-auto-delivery-config-shop', + }, + task, + trigger: 'unit_test', + }, { + listTasksByOrderId: async () => [task], + updateTask: async (taskId, patch) => { + updates.push({ taskId, patch }) + return { ...task, ...patch } + }, + createTaskEvent: async (taskId, eventType, payload, createdAt) => { + events.push({ taskId, eventType, payload, createdAt }) + return { id: 1 } + }, + fetch: async () => { + throw new Error('fetch should not be called without config') + }, + nowIso: () => '2026-05-21T12:00:00.000Z', + }) + + assert.equal(result.sent, false) + assert.equal(result.skipped, true) + assert.equal(result.reason, 'missing_config') + assert.equal(updates.length, 1) + assert.equal(events[0]?.eventType, 'agiso_auto_delivery_skipped') + + const context = JSON.parse(updates[0].patch.context_json) + assert.equal(context.existing, true) + assert.equal(context.agisoAutoDelivery.status, 'skipped') + assert.equal(context.agisoAutoDelivery.reason, 'missing_config') + assert.deepEqual({ + hasEndpoint: context.agisoAutoDelivery.hasEndpoint, + hasAccessToken: context.agisoAutoDelivery.hasAccessToken, + hasAppSecret: context.agisoAutoDelivery.hasAppSecret, + }, { + hasEndpoint: true, + hasAccessToken: false, + hasAppSecret: false, + }) + } finally { + runtimeConfig.platforms.agiso.appSecret = originalAppSecret + runtimeConfig.platforms.agiso.messaging.shops = originalShops + } +}) + +test('ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps fails when accepted delivery is not confirmed as shipped', async () => { + const originalAppSecret = runtimeConfig.platforms.agiso.appSecret + const originalShops = runtimeConfig.platforms.agiso.messaging.shops + const updates = [] + const events = [] + const messages = [] + + runtimeConfig.platforms.agiso.appSecret = 'runtime-secret' + runtimeConfig.platforms.agiso.messaging.shops = { + [agisoOrder.shop_id]: { + accessToken: 'access-token', + appSecret: 'shop-secret', + apiVersion: '1', + }, + } + + try { + const task = createDeliveredTask() + const result = await ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ + order: agisoOrder, + task, + trigger: 'unit_test', + }, { + listTasksByOrderId: async () => [task], + fetch: async () => ({ + status: 200, + text: async () => JSON.stringify({ IsSuccess: true, RequestId: 'req-1' }), + }), + confirmAgisoXianyuAutoDeliveryShipped: async (input) => ({ + shipped: false, + orderStatus: 2, + shipTime: 0, + reason: `not shipped: ${input.requestId}`, + }), + updateTask: async (taskId, patch) => { + updates.push({ taskId, patch }) + return { ...task, ...patch } + }, + createTaskEvent: async (taskId, eventType, payload, createdAt) => { + events.push({ taskId, eventType, payload, createdAt }) + return { id: 2 } + }, + ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask: async (payload) => { + messages.push(payload) + return { sent: true } + }, + nowIso: () => '2026-05-21T12:01:00.000Z', + }) + + assert.equal(result.sent, false) + assert.equal(result.skipped, false) + assert.equal(result.reason, 'delivery_not_confirmed') + assert.equal(messages.length, 0) + assert.equal(events[0]?.eventType, 'agiso_auto_delivery_failed') + + const context = JSON.parse(updates[0].patch.context_json) + assert.equal(context.agisoAutoDelivery.status, 'failed') + assert.equal(context.agisoAutoDelivery.reason, 'delivery_not_confirmed') + assert.equal(context.agisoAutoDelivery.requestId, 'req-1') + assert.equal(context.agisoAutoDelivery.confirmOrderStatus, 2) + assert.match(context.agisoAutoDelivery.errorMessage, /订单仍未进入已发货/) + } finally { + runtimeConfig.platforms.agiso.appSecret = originalAppSecret + runtimeConfig.platforms.agiso.messaging.shops = originalShops + } +}) diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 407419c7..3f38a307 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -312,6 +312,14 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 128 个用例通过 +45. Agiso 自动发货服务迁移前补直接测试: + - 新增 `ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps`,便于隔离 task repository、HTTP 请求、发货确认与消息通知依赖 + - 覆盖订单下仍有任务未交付时跳过、配置缺失时记录 `skipped`、接口受理但未确认发货时记录 `failed` 三个关键分支 +46. Docker 内验证通过: + - `src/services/platforms/agiso/xianyu/auto-delivery-service.test.js` 共 9 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 131 个用例通过 ## 下一步建议