优化
This commit is contained in:
@@ -1,18 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { randomInt, randomUUID } from 'node:crypto'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const MAX_COUNT = 100
|
||||
const MAX_ROUTE_POOL_SIZE = 40
|
||||
const MAX_DESTINATIONS_PER_LOGISTICS = 3
|
||||
|
||||
export const PRODUCT_CATALOG = [
|
||||
{ cargoName: '蓝牙音箱', cargoNameEn: 'Bluetooth speaker', hsCode: '8518210000', material: '塑料和电子元件 / Plastic and electronic components', usage: '音频播放 / Audio playback', brand: 'SOUNDTEST', modelPrefix: 'SPK', lengthCm: 42, widthCm: 32, heightCm: 28, unitsPerPackage: 8, cartonGrossWeightKg: 12.8, unitDeclaredValue: 18.5, productLengthCm: 18, productWidthCm: 10, productHeightCm: 8 },
|
||||
{ cargoName: '手机保护壳', cargoNameEn: 'Mobile phone case', hsCode: '3926909090', material: '热塑性聚氨酯 / TPU', usage: '手机保护 / Mobile phone protection', brand: 'CASETEST', modelPrefix: 'CASE', lengthCm: 48, widthCm: 38, heightCm: 32, unitsPerPackage: 100, cartonGrossWeightKg: 16.5, unitDeclaredValue: 2.2, productLengthCm: 17, productWidthCm: 8, productHeightCm: 1.5 },
|
||||
{ cargoName: '不锈钢保温杯', cargoNameEn: 'Stainless steel vacuum bottle', hsCode: '9617009000', material: '不锈钢 / Stainless steel', usage: '饮品保温 / Beverage insulation', brand: 'CUPTEST', modelPrefix: 'CUP', lengthCm: 50, widthCm: 40, heightCm: 32, unitsPerPackage: 24, cartonGrossWeightKg: 15.2, unitDeclaredValue: 9.8, productLengthCm: 8, productWidthCm: 8, productHeightCm: 25 },
|
||||
{ cargoName: '棉质短袖T恤', cargoNameEn: 'Cotton short sleeve T-shirt', hsCode: '6109100021', material: '棉 / Cotton', usage: '日常穿着 / Daily wear', brand: 'SHIRTTEST', modelPrefix: 'TS', lengthCm: 55, widthCm: 42, heightCm: 35, unitsPerPackage: 60, cartonGrossWeightKg: 18.6, unitDeclaredValue: 6.5, productLengthCm: 30, productWidthCm: 25, productHeightCm: 2 },
|
||||
{ cargoName: '瑜伽垫', cargoNameEn: 'Yoga mat', hsCode: '9506919000', material: '热塑性弹性体 / TPE', usage: '健身运动 / Fitness exercise', brand: 'YOGATEST', modelPrefix: 'YOGA', lengthCm: 65, widthCm: 45, heightCm: 45, unitsPerPackage: 10, cartonGrossWeightKg: 13.5, unitDeclaredValue: 12, productLengthCm: 61, productWidthCm: 18, productHeightCm: 18 },
|
||||
{ cargoName: '塑料收纳盒', cargoNameEn: 'Plastic storage box', hsCode: '3924900000', material: '聚丙烯 / Polypropylene', usage: '家居收纳 / Household storage', brand: 'BOXTEST', modelPrefix: 'BOX', lengthCm: 62, widthCm: 48, heightCm: 40, unitsPerPackage: 12, cartonGrossWeightKg: 17.8, unitDeclaredValue: 7.2, productLengthCm: 38, productWidthCm: 28, productHeightCm: 20 },
|
||||
{ cargoName: 'LED台灯', cargoNameEn: 'LED desk lamp', hsCode: '9405210090', material: '铝和塑料 / Aluminum and plastic', usage: '桌面照明 / Desk lighting', brand: 'LAMPTEST', modelPrefix: 'LAMP', lengthCm: 58, widthCm: 42, heightCm: 36, unitsPerPackage: 12, cartonGrossWeightKg: 14.4, unitDeclaredValue: 15.6, productLengthCm: 35, productWidthCm: 15, productHeightCm: 40 },
|
||||
{ cargoName: '宠物橡胶玩具', cargoNameEn: 'Pet rubber toy', hsCode: '4016999090', material: '天然橡胶 / Natural rubber', usage: '宠物玩耍 / Pet play', brand: 'PETTEST', modelPrefix: 'PET', lengthCm: 46, widthCm: 36, heightCm: 34, unitsPerPackage: 48, cartonGrossWeightKg: 15.6, unitDeclaredValue: 3.4, productLengthCm: 12, productWidthCm: 7, productHeightCm: 7 },
|
||||
]
|
||||
|
||||
// Local test-environment settings. Keep PASSWORD empty in committed code.
|
||||
const CONFIG = {
|
||||
GATEWAY: 'http://8.163.30.128:9000',
|
||||
USERNAME: '17512095155',
|
||||
PASSWORD: 'd6KW2ZC4vv44WJ4a',
|
||||
COUNT: 10,
|
||||
COUNT: 50,
|
||||
}
|
||||
|
||||
export function parseArguments(values) {
|
||||
@@ -63,6 +76,40 @@ function items(value) {
|
||||
return Array.isArray(value) ? value : []
|
||||
}
|
||||
|
||||
function shuffled(values) {
|
||||
const result = [...values]
|
||||
for (let index = result.length - 1; index > 0; index -= 1) {
|
||||
const target = randomInt(index + 1)
|
||||
;[result[index], result[target]] = [result[target], result[index]]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function round(value, scale) {
|
||||
return Number(value.toFixed(scale))
|
||||
}
|
||||
|
||||
function logisticsKey(route) {
|
||||
return `${route.receiving.id}|${route.channel.channelCode}|${route.pricingTaxMode}`
|
||||
}
|
||||
|
||||
export function diversifyRoutes(routes) {
|
||||
const grouped = new Map()
|
||||
for (const route of routes) {
|
||||
const key = logisticsKey(route)
|
||||
if (!grouped.has(key)) grouped.set(key, [])
|
||||
grouped.get(key).push(route)
|
||||
}
|
||||
const queues = shuffled([...grouped.values()]).map((group) => shuffled(group))
|
||||
const result = []
|
||||
while (queues.some((queue) => queue.length > 0)) {
|
||||
for (const queue of queues) {
|
||||
if (queue.length > 0) result.push(queue.shift())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function requireSuccess(response, action) {
|
||||
if (response.http !== 200 || String(response.body?.code) !== '200') {
|
||||
throw new Error(`${action} failed: http=${response.http}, code=${response.body?.code ?? 'missing'}, message=${response.body?.message ?? 'missing'}, requestId=${response.requestId || 'missing'}, traceId=${response.traceId || 'missing'}`)
|
||||
@@ -101,7 +148,7 @@ async function request(gateway, timeoutMs, method, path, { token, body, headers
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMasterData(gateway, timeoutMs, token) {
|
||||
async function loadPricedRoutes(gateway, timeoutMs, token) {
|
||||
const domesticResponse = await request(gateway, timeoutMs, 'GET',
|
||||
'/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC', { token })
|
||||
const domestic = records(requireSuccess(domesticResponse, 'Query active domestic warehouses'))
|
||||
@@ -112,14 +159,16 @@ async function loadMasterData(gateway, timeoutMs, token) {
|
||||
throw new Error('No active domestic receiving warehouse or FBA destination warehouse is available')
|
||||
}
|
||||
|
||||
for (const receiving of domestic) {
|
||||
const routes = []
|
||||
receivingLoop: for (const receiving of shuffled(domestic)) {
|
||||
const channelResponse = await request(gateway, timeoutMs, 'GET',
|
||||
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`, { token })
|
||||
const channels = items(requireSuccess(channelResponse, 'Query active pricing channels'))
|
||||
.filter((channel) => channel?.channelCode && items(channel.pricingTaxModes).length > 0)
|
||||
for (const channel of channels) {
|
||||
for (const pricingTaxMode of channel.pricingTaxModes) {
|
||||
for (const destination of destinations) {
|
||||
for (const channel of shuffled(channels)) {
|
||||
for (const pricingTaxMode of shuffled(channel.pricingTaxModes)) {
|
||||
let logisticsDestinationCount = 0
|
||||
for (const destination of shuffled(destinations)) {
|
||||
const estimateResponse = await request(gateway, timeoutMs, 'POST',
|
||||
'/api/merchant/v1/first-leg/pricing/estimate', {
|
||||
token,
|
||||
@@ -136,13 +185,17 @@ async function loadMasterData(gateway, timeoutMs, token) {
|
||||
},
|
||||
})
|
||||
if (estimateResponse.http === 200 && String(estimateResponse.body?.code) === '200') {
|
||||
return { receiving, destination, channel, pricingTaxMode }
|
||||
routes.push({ receiving, destination, channel, pricingTaxMode })
|
||||
logisticsDestinationCount += 1
|
||||
if (routes.length >= MAX_ROUTE_POOL_SIZE) break receivingLoop
|
||||
if (logisticsDestinationCount >= MAX_DESTINATIONS_PER_LOGISTICS) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('No priced route is available for the fixed merchant account')
|
||||
if (routes.length === 0) throw new Error('No priced route is available for the fixed merchant account')
|
||||
return routes
|
||||
}
|
||||
|
||||
function testPng() {
|
||||
@@ -161,11 +214,16 @@ async function uploadProductImage(gateway, timeoutMs, token, marker) {
|
||||
return data.fileId
|
||||
}
|
||||
|
||||
export function buildOrderPayload({ marker, index, profile, master, productImageFileId }) {
|
||||
export function buildOrderPayload({ marker, index, profile, master, product, packageCount, productImageFileId }) {
|
||||
const sequence = String(index).padStart(3, '0')
|
||||
const shipmentId = `SHIP-${sequence}-${marker}`.slice(0, 64)
|
||||
const referenceId = `REF-${sequence}-${marker}`.slice(0, 64)
|
||||
const companyName = profile.tenantName || `固定客户-${marker}`
|
||||
const totalQuantity = product.unitsPerPackage * packageCount
|
||||
const grossWeightKg = round(product.cartonGrossWeightKg * packageCount, 3)
|
||||
const netWeightKg = round(grossWeightKg * 0.9, 3)
|
||||
const volumeCbm = round(product.lengthCm * product.widthCm * product.heightCm / 1_000_000 * packageCount, 6)
|
||||
const declaredValue = round(product.unitDeclaredValue * totalQuantity, 2)
|
||||
return {
|
||||
customerId: profile.id,
|
||||
customerName: companyName,
|
||||
@@ -206,30 +264,30 @@ export function buildOrderPayload({ marker, index, profile, master, productImage
|
||||
receiver: null,
|
||||
cargoes: [{
|
||||
skuNo: `SKU-${sequence}-${marker}`.slice(0, 64),
|
||||
cargoName: '待审核测试产品',
|
||||
cargoNameEn: 'Pending review test product',
|
||||
hsCode: '8518210000',
|
||||
customsHsCode: '8518210000',
|
||||
cargoName: product.cargoName,
|
||||
cargoNameEn: product.cargoNameEn,
|
||||
hsCode: product.hsCode,
|
||||
customsHsCode: product.hsCode,
|
||||
packageType: 'CARTON',
|
||||
lengthCm: 30,
|
||||
widthCm: 20,
|
||||
heightCm: 20,
|
||||
unitsPerPackage: 12,
|
||||
packageCount: 2,
|
||||
grossWeightKg: 20.5,
|
||||
volumeCbm: 0.024,
|
||||
totalQuantity: 24,
|
||||
netWeightKg: 18,
|
||||
unitDeclaredValue: 8.5,
|
||||
declaredValue: 204,
|
||||
lengthCm: product.lengthCm,
|
||||
widthCm: product.widthCm,
|
||||
heightCm: product.heightCm,
|
||||
unitsPerPackage: product.unitsPerPackage,
|
||||
packageCount,
|
||||
grossWeightKg,
|
||||
volumeCbm,
|
||||
totalQuantity,
|
||||
netWeightKg,
|
||||
unitDeclaredValue: product.unitDeclaredValue,
|
||||
declaredValue,
|
||||
currency: 'USD',
|
||||
material: '塑料 / Plastic',
|
||||
usage: '接口测试 / API testing',
|
||||
model: `MODEL-${sequence}`,
|
||||
brand: 'TEST-NO-LOGO',
|
||||
productLengthCm: 12,
|
||||
productWidthCm: 8,
|
||||
productHeightCm: 6,
|
||||
material: product.material,
|
||||
usage: product.usage,
|
||||
model: `${product.modelPrefix}-${sequence}`,
|
||||
brand: product.brand,
|
||||
productLengthCm: product.productLengthCm,
|
||||
productWidthCm: product.productWidthCm,
|
||||
productHeightCm: product.productHeightCm,
|
||||
shipmentId,
|
||||
referenceId,
|
||||
upcNo: `${Date.now()}${sequence}`.slice(-12),
|
||||
@@ -289,11 +347,16 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
throw new Error(`Fixed merchant account is not ready: tenantId=${profile?.id ?? 'missing'}, status=${profile?.status ?? 'missing'}`)
|
||||
}
|
||||
|
||||
const master = await loadMasterData(gateway, timeoutMs, token)
|
||||
const routePool = await loadPricedRoutes(gateway, timeoutMs, token)
|
||||
const productImageFileId = await uploadProductImage(gateway, timeoutMs, token, marker)
|
||||
const randomizedProducts = shuffled(PRODUCT_CATALOG)
|
||||
const randomizedRoutes = diversifyRoutes(routePool)
|
||||
|
||||
for (let index = 1; index <= count; index += 1) {
|
||||
const payload = buildOrderPayload({ marker, index, profile, master, productImageFileId })
|
||||
const master = randomizedRoutes[(index - 1) % randomizedRoutes.length]
|
||||
const product = randomizedProducts[(index - 1) % randomizedProducts.length]
|
||||
const packageCount = randomInt(1, 31)
|
||||
const payload = buildOrderPayload({ marker, index, profile, master, product, packageCount, productImageFileId })
|
||||
const createResponse = await request(gateway, timeoutMs, 'POST',
|
||||
'/api/merchant/v1/orders/first-leg', {
|
||||
token,
|
||||
@@ -330,9 +393,19 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
orderNo,
|
||||
clientStatus: detail.order.status,
|
||||
mainStatus: unifiedOrder.summary.mainStatus,
|
||||
destinationWarehouseId: String(master.destination.id),
|
||||
destinationWarehouseCode: master.destination.warehouseCode,
|
||||
destinationWarehouseName: master.destination.warehouseName,
|
||||
receivingWarehouseId: String(master.receiving.id),
|
||||
receivingWarehouseName: master.receiving.warehouseName,
|
||||
channelCode: master.channel.channelCode,
|
||||
channelName: master.channel.channelName,
|
||||
pricingTaxMode: master.pricingTaxMode,
|
||||
cargoName: product.cargoName,
|
||||
packageCount,
|
||||
}
|
||||
orders.push(result)
|
||||
console.log(`CREATED ${index}/${count} orderId=${result.orderId} orderNo=${result.orderNo} status=PENDING_REVIEW`)
|
||||
console.log(`CREATED ${index}/${count} orderId=${result.orderId} orderNo=${result.orderNo} logistics=${result.channelCode}/${result.pricingTaxMode} destination=${result.destinationWarehouseCode} product=${result.cargoName} cartons=${result.packageCount} status=PENDING_REVIEW`)
|
||||
}
|
||||
|
||||
const report = {
|
||||
@@ -346,6 +419,9 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
startedAt,
|
||||
completedAt: new Date().toISOString(),
|
||||
productImageFileId: String(productImageFileId),
|
||||
pricedRoutePoolSize: routePool.length,
|
||||
logisticsPoolSize: new Set(routePool.map(logisticsKey)).size,
|
||||
productCatalogSize: PRODUCT_CATALOG.length,
|
||||
orders,
|
||||
securityNote: '报告不保存密码或访问令牌。',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user