289 lines
8.4 KiB
JavaScript
289 lines
8.4 KiB
JavaScript
#!/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(`用法:
|
|
npm run agiso:auto-delivery:test -- <订单号> [--shop-id <店铺ID>] [--poll 0,3,10]
|
|
|
|
说明:
|
|
1. 该脚本只测试正确的发货接口 DummySend。
|
|
2. 会先查 Order/Detail,再调用 DummySend,再按 poll 秒数轮询发货状态。
|
|
|
|
示例:
|
|
npm run agiso:auto-delivery:test -- 4502285714045005830
|
|
npm run agiso:auto-delivery:test -- 4502285714045005830 --poll 0,5,15
|
|
npm run agiso:auto-delivery:test -- 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()
|
|
})
|
|
}
|