#!/usr/bin/env node import crypto from 'node:crypto' import process from 'node:process' import { fileURLToPath } from 'node:url' import { setTimeout as sleep } from 'node:timers/promises' import { query, closeDb } from '../src/db/client.js' import { parseJsonObject } from '../src/utils/json.js' import { getAgisoShopConfig } from '../src/services/platforms/agiso/shop-config-service.js' import { queryAgisoXianyuOrderDetail } from '../src/services/platforms/agiso/xianyu/order-detail-service.js' const DEFAULT_DUMMY_ENDPOINT = 'https://gw-api.agiso.com/aldsIdle/Order/DummySend' const DEFAULT_POLL_SECONDS = [0, 3, 10] async function main() { const options = parseArgs(process.argv.slice(2)) if (options.help || !options.orderId) { printHelp() process.exit(options.help ? 0 : 1) } const orderRecord = await resolveOrderRecord(options) const shopId = orderRecord?.shop_id || options.shopId const shopConfig = getAgisoShopConfig(shopId) || {} const accessToken = String(shopConfig.accessToken || '').trim() const appSecret = String(shopConfig.appSecret || process.env.AGISO_APP_SECRET || '').trim() if (!shopId) { throw new Error('未提供 shopId,且数据库中也没有查到该订单对应的店铺') } if (!accessToken) { throw new Error(`店铺 ${shopId} 缺少 accessToken 配置,请检查 apps/backend/data/agiso-shops.json`) } if (!appSecret) { throw new Error('缺少 AGISO_APP_SECRET 或店铺级 appSecret 配置') } printJson('测试输入', { orderId: options.orderId, shopId, pollSeconds: options.pollSeconds, orderRecord: orderRecord ? { id: Number(orderRecord.id || 0) || null, platformOrderId: String(orderRecord.platform_order_id || '').trim(), orderStatus: String(orderRecord.order_status || '').trim(), payStatus: String(orderRecord.pay_status || '').trim(), provider: String(orderRecord.provider || '').trim(), platform: String(orderRecord.platform || '').trim(), } : null, }) const beforeDetail = await queryAgisoXianyuOrderDetail({ shopId, platformOrderId: options.orderId, requestId: `diag-before-${Date.now()}`, }) printJson('调用前订单详情', summarizeDetail(beforeDetail)) const result = await executeRequest({ platformOrderId: options.orderId, accessToken, appSecret, }) printJson('接口响应 更新发货状态 DummySend', result) for (const seconds of options.pollSeconds) { if (seconds > 0) { await sleep(seconds * 1000) } const detail = await queryAgisoXianyuOrderDetail({ shopId, platformOrderId: options.orderId, requestId: `diag-after-${seconds}s-${Date.now()}`, }) printJson(`调用后订单详情 +${seconds}s`, summarizeDetail(detail)) } } function parseArgs(argv) { const output = { orderId: '', shopId: '', pollSeconds: [...DEFAULT_POLL_SECONDS], help: false, } for (let index = 0; index < argv.length; index += 1) { const current = String(argv[index] || '').trim() if (!current) { continue } if (!current.startsWith('--')) { if (!output.orderId) { output.orderId = current continue } throw new Error(`无法识别的参数:${current}`) } if (current === '--help') { output.help = true continue } const [rawKey, inlineValue = ''] = current.split('=', 2) const key = rawKey.slice(2) const nextValue = inlineValue || argv[index + 1] || '' const shouldConsumeNext = !inlineValue && argv[index + 1] && !String(argv[index + 1]).startsWith('--') switch (key) { case 'shop-id': output.shopId = String(nextValue || '').trim() break case 'poll': output.pollSeconds = parsePollSeconds(nextValue) break default: throw new Error(`无法识别的参数:${current}`) } if (shouldConsumeNext) { index += 1 } } return output } function parsePollSeconds(value) { const normalized = String(value || '').trim() if (!normalized) { return [...DEFAULT_POLL_SECONDS] } const parsed = normalized .split(',') .map((item) => Number(String(item || '').trim())) .filter((item) => Number.isFinite(item) && item >= 0) .map((item) => Math.floor(item)) if (parsed.length === 0) { throw new Error(`poll 参数格式无效:${value}`) } return parsed } async function resolveOrderRecord(options) { const explicitShopId = String(options.shopId || '').trim() if (explicitShopId) { const result = await query( ` SELECT * FROM orders WHERE provider = 'agiso' AND platform = 'xianyu' AND shop_id = $1 AND platform_order_id = $2 ORDER BY id DESC LIMIT 1 `, [explicitShopId, options.orderId], ) return result.rows[0] || null } const result = await query( ` SELECT * FROM orders WHERE provider = 'agiso' AND platform = 'xianyu' AND platform_order_id = $1 ORDER BY id DESC LIMIT 2 `, [options.orderId], ) if (result.rows.length > 1) { throw new Error(`数据库里命中多条同订单号记录,请显式传入 --shop-id。订单号:${options.orderId}`) } return result.rows[0] || null } async function executeRequest(context) { const timestamp = String(Math.floor(Date.now() / 1000)) const requestBody = buildDummySendBody(context, timestamp) const response = await fetch(DEFAULT_DUMMY_ENDPOINT, { method: 'POST', headers: { Authorization: `Bearer ${context.accessToken}`, ApiVersion: '1', 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', }, body: new URLSearchParams(requestBody).toString(), }) const rawText = await response.text() const payload = parseJsonObject(rawText, { preserveLargeIntegers: true }) return { endpoint: DEFAULT_DUMMY_ENDPOINT, status: response.status, requestBody, response: payload, rawText, } } function buildDummySendBody(context, timestamp) { const payload = { tid: context.platformOrderId, timestamp, } payload.sign = generateSign(payload, context.appSecret) return payload } function generateSign(params, appSecret) { let raw = String(appSecret || '').trim() for (const [key, value] of Object.entries(params).sort(([left], [right]) => left.localeCompare(right))) { raw += `${key}${value}` } raw += String(appSecret || '').trim() return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase() } function summarizeDetail(detailResult) { return { success: Boolean(detailResult?.success), reason: String(detailResult?.reason || '').trim(), errorMessage: String(detailResult?.errorMessage || '').trim(), responseStatus: Number(detailResult?.responseStatus || 0), shipped: Boolean(detailResult?.shipped), shipTime: Number(detailResult?.shipTime || 0), orderStatus: Number(detailResult?.orderStatus || 0), bizOrderId: String(detailResult?.detailPayload?.biz_order_id || '').trim(), sellerNick: String(detailResult?.detailPayload?.seller_nick || '').trim(), buyerNick: String(detailResult?.detailPayload?.buyer_nick || '').trim(), sku: String(detailResult?.detailPayload?.sku || '').trim(), itemTitle: String(detailResult?.detailPayload?.item?.title || '').trim(), } } function printJson(title, value) { process.stdout.write(`\n=== ${title} ===\n`) process.stdout.write(`${JSON.stringify(value, null, 2)}\n`) } function printHelp() { process.stdout.write(`用法: node scripts/test-agiso-auto-delivery.js <订单号> [--shop-id <店铺ID>] [--poll 0,3,10] 说明: 1. 该脚本只测试正确的发货接口 DummySend。 2. 会先查 Order/Detail,再调用 DummySend,再按 poll 秒数轮询发货状态。 示例: node scripts/test-agiso-auto-delivery.js 4502285714045005830 node scripts/test-agiso-auto-delivery.js 4502285714045005830 --poll 0,5,15 node scripts/test-agiso-auto-delivery.js 4502285714045005830 --shop-id 2209880145223 `) } const isNodeTestRunner = Array.isArray(process.execArgv) && process.execArgv.includes('--test') const isDirectRun = !isNodeTestRunner && process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1] if (isDirectRun) { main() .catch((error) => { process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`) process.exitCode = 1 }) .finally(async () => { await closeDb() }) }