This commit is contained in:
2026-09-11 16:17:16 +08:00
parent 2d928d667b
commit 51ce609ab1
57 changed files with 942 additions and 139 deletions
+6
View File
@@ -295,6 +295,7 @@ function usage() {
--wait-split-package-count <n> 单票指定总箱数,1-1000;省略时使用既有轮换
--wait-split-sequence <n> 单票全局序号,正整数,默认1
--wait-split-random-value <n> 单票随机选择值,正整数,默认使用全局序号
--prepare-dropship-manual-count <true> 一件代发停在PDA清点前,不领取、不清点、不归托
--verify-pallet-location-guide <true> 验证整票库位块、图片槽绑定与删除后 RESERVED_EMPTY
--route-mode <mode> 线路选择:fixed 或 random,默认fixed
--manual-split-fixture-report <path> 人工分柜准备报告路径,仅只读核验模块使用
@@ -364,6 +365,7 @@ export function parseArguments(argv) {
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
prepareDropshipManualCount: values.get('prepare-dropship-manual-count') === 'true',
verifyPalletLocationGuide: values.has('verify-pallet-location-guide'),
routeMode: values.get('route-mode') || 'fixed',
quiet: values.get('quiet') === 'true',
@@ -423,6 +425,10 @@ export function validateExecutionOptions(options) {
&& (!options.originWarehouseName || !options.channelName || !options.channelCode)) {
throw new Error('randomized订单档案必须同时固定起运仓、渠道名称和渠道编码')
}
if (options.prepareDropshipManualCount
&& (!options.modules.includes('orders') || !options.modules.includes('inbound-measurement'))) {
throw new Error('prepare-dropship-manual-count要求同时执行orders和inbound-measurement模块')
}
if (options.waitSplitCount > 1) {
const fixtureModules = new Set(['merchant-qualification-fixture', 'orders', 'inbound-measurement',
'manual-order-splitting-fixture', 'coload-groups', 'firstleg-container-fee-allocation'])
@@ -128,17 +128,24 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
`/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/cartons/resolve?warehouseId=${warehouseId}&boxCode=${encodeURIComponent(boxCode)}`)
const resolved = context.apiSuccess(MODULE, `解析第${index + 1}箱的冻结箱唛`, resolveResponse,
`boxCode=${boxCode}`, OPTIONS)
context.assert(MODULE, `第${index + 1}箱允许清点且每箱SKU数完整`, resolveResponse,
resolved?.canCount === true && Number(resolved?.expectedSkuQuantity) > 0,
`canCount=${resolved?.canCount ?? 'missing'}, expectedSkuQuantity=${resolved?.expectedSkuQuantity ?? 'missing'}`, OPTIONS)
if (resolved?.canCount !== true || Number(resolved?.expectedSkuQuantity) < 1) continue
const resolvedSkuLines = Array.isArray(resolved?.skuLines) ? resolved.skuLines : []
context.assert(MODULE, `第${index + 1}箱允许清点且每箱SKU明细完整`, resolveResponse,
resolved?.canCount === true && resolvedSkuLines.length > 0
&& resolvedSkuLines.every((line) => asId(line?.inboundDetailId) && line?.skuCode
&& Number(line?.expectedQuantity) > 0),
`canCount=${resolved?.canCount ?? 'missing'}, skuLines=${resolvedSkuLines.length}`, OPTIONS)
if (resolved?.canCount !== true || resolvedSkuLines.length < 1) continue
const countPayload = {
warehouseId,
boxCode,
actualSkuQuantity: Number(resolved.expectedSkuQuantity),
discrepancyAction: 'MATCH',
discrepancyRemark: null,
skuLines: resolvedSkuLines.map((line) => ({
inboundDetailId: asId(line.inboundDetailId),
skuCode: line.skuCode,
actualQuantity: Number(line.expectedQuantity),
discrepancyAction: 'MATCH',
discrepancyRemark: null,
})),
palletNo: index === 0 ? null : generatedPallets[Math.min(index, generatedPallets.length - 1)].palletNo,
operationId: `${context.marker}-dropship-count-${index + 1}`.slice(0, 128),
}
@@ -148,10 +155,11 @@ export async function completeDropshipPalletization(context, { inboundId, inboun
`boxCode=${boxCode}`, OPTIONS)
context.assert(MODULE, `第${index + 1}箱清点结果与冻结箱唛一致`, countResponse,
counted?.carton?.boxCode === resolved?.boxCode
&& Number(counted?.carton?.expectedSkuQuantity) === Number(resolved?.expectedSkuQuantity)
&& Number(counted?.carton?.actualSkuQuantity) === Number(resolved?.expectedSkuQuantity)
&& counted?.carton?.discrepancyStatus === 'MATCH',
`cartonId=${asId(counted?.carton?.cartonId) || 'missing'}, discrepancy=${counted?.carton?.discrepancyStatus ?? 'missing'}`, OPTIONS)
&& Array.isArray(counted?.carton?.skuLines)
&& counted.carton.skuLines.length === resolvedSkuLines.length
&& counted.carton.skuLines.every((line) => Number(line?.actualQuantity) === Number(line?.expectedQuantity)
&& line?.discrepancyStatus === 'MATCH'),
`cartonId=${asId(counted?.carton?.cartonId) || 'missing'}, skuLines=${counted?.carton?.skuLines?.length ?? 'missing'}`, OPTIONS)
if (counted?.carton?.boxCode === resolved?.boxCode) {
countedCartons += 1
sealedCartonMarks.push(resolved.boxCode)
@@ -1035,21 +1043,40 @@ export async function run(context) {
let sealedCartonMarks = []
let sealedSourcePalletNos = []
if (dropship && finalMeasurement?.processStatus === 'WAIT_PALLETIZE') {
const sealed = await completeDropshipPalletization(context, {
inboundId, inboundCode, warehouseId: asId(summary?.warehouseId), measurement: finalMeasurement,
})
sealedCartonMarks = sealed?.taskStatus === 'COMPLETED' ? sealed.sealedCartonMarks : []
sealedSourcePalletNos = sealed?.taskStatus === 'COMPLETED'
? (sealed.sealedSourcePalletNos || []).filter(Boolean) : []
completed = await waitForCompletedMeasurement(context, measurementPath, ['WAIT_SPLIT'], 10)
finalResponse = completed.response
finalMeasurement = context.apiSuccess(MODULE, '合托后回查一件代发入仓单', finalResponse,
`pollAttempts=${completed.attempts}`, OPTIONS)
context.assert(MODULE, '一件代发合托完成后进入智能分柜门禁', finalResponse,
finalMeasurement?.processStatus === 'WAIT_SPLIT'
&& finalMeasurement?.measureReviewStatus === 'APPROVED'
&& finalMeasurement?.measurementStatus === 'APPROVED',
`processStatus=${finalMeasurement?.processStatus ?? 'missing'}`, OPTIONS)
if (context.options?.prepareDropshipManualCount) {
const warehouseId = asId(summary?.warehouseId)
const pendingResponse = await context.request('GET',
`/api/platform/v1/first-leg/pda-dropship/tasks/pending?warehouseId=${warehouseId}&keyword=${encodeURIComponent(inboundCode)}`)
const pending = context.apiSuccess(MODULE, '准备PDA人工清点任务', pendingResponse,
`inboundId=${inboundId}`, OPTIONS)
const task = (pending || []).find((item) => asId(item?.inboundId) === inboundId)
context.assert(MODULE, '任务停在未领取、未清点、未归托状态', pendingResponse,
Boolean(asId(task?.taskId)) && task?.taskStatus === 'PENDING'
&& Number(task?.countedCartons) === 0 && Number(task?.boundCartons) === 0
&& Number(task?.totalCartons) === Number(finalMeasurement?.actualPackageCount),
`taskId=${asId(task?.taskId) || 'missing'}, status=${task?.taskStatus ?? 'missing'}, counted=${task?.countedCartons ?? 'missing'}`, OPTIONS)
context.createdResources.dropshipManualCount = {
taskId: asId(task?.taskId), inboundId, inboundCode, warehouseId,
warehouseName: task?.warehouseName || null, totalCartons: Number(task?.totalCartons || 0),
taskStatus: task?.taskStatus || null,
}
} else {
const sealed = await completeDropshipPalletization(context, {
inboundId, inboundCode, warehouseId: asId(summary?.warehouseId), measurement: finalMeasurement,
})
sealedCartonMarks = sealed?.taskStatus === 'COMPLETED' ? sealed.sealedCartonMarks : []
sealedSourcePalletNos = sealed?.taskStatus === 'COMPLETED'
? (sealed.sealedSourcePalletNos || []).filter(Boolean) : []
completed = await waitForCompletedMeasurement(context, measurementPath, ['WAIT_SPLIT'], 10)
finalResponse = completed.response
finalMeasurement = context.apiSuccess(MODULE, '合托后回查一件代发入仓单', finalResponse,
`pollAttempts=${completed.attempts}`, OPTIONS)
context.assert(MODULE, '一件代发合托完成后进入智能分柜门禁', finalResponse,
finalMeasurement?.processStatus === 'WAIT_SPLIT'
&& finalMeasurement?.measureReviewStatus === 'APPROVED'
&& finalMeasurement?.measurementStatus === 'APPROVED',
`processStatus=${finalMeasurement?.processStatus ?? 'missing'}`, OPTIONS)
}
}
const approvedScanResponse = await multipartRequest(context, scanPath, {
+12 -4
View File
@@ -402,6 +402,11 @@ function buildPayload(context, profile, master, productImageFileId, customerAddr
? context.context.waitSplitVariant.packageCounts : []
const selectedBoxes = BOXES.slice(0, skuCount)
const orderProfile = context.context?.orderProfile || {}
const dropship = String(context.context?.orderFulfillmentType || 'STANDARD').trim().toUpperCase()
=== 'ONE_CLICK_DROPSHIP'
const mixedPackageCount = Math.max(1,
Number(context.context?.waitSplitVariant?.totalPackageCount || packageCountPerSku))
const mixedPackingGroupNo = `MIX-${context.marker}`.slice(0, 32)
const isPrivate = master.destinationType === 'PRIVATE'
const pickupMethod = orderProfile.pickupMethod || 'SELF_EXPRESS'
const receiver = isPrivate ? {
@@ -411,7 +416,7 @@ function buildPayload(context, profile, master, productImageFileId, customerAddr
addressLine: `${context.marker} 88 Randomized Test Ave`, postalCode: '11354',
} : null
return {
fulfillmentType: String(context.context?.orderFulfillmentType || 'STANDARD').trim().toUpperCase(),
fulfillmentType: dropship ? 'ONE_CLICK_DROPSHIP' : 'STANDARD',
customerId: profile.id,
customerName: companyName,
transportMode: 'SEA', tradeTerm: 'FOB',
@@ -441,14 +446,17 @@ function buildPayload(context, profile, master, productImageFileId, customerAddr
cargoes: selectedBoxes.map((box, index) => {
const volumeCbm = box.lengthCm * box.widthCm * box.heightCm / 1_000_000
const unitsPerPackage = 10 + index
const packageCount = Number(packageCounts[index] || packageCountPerSku)
const packageCount = dropship ? mixedPackageCount : Number(packageCounts[index] || packageCountPerSku)
const carton = dropship ? selectedBoxes[0] : box
const cartonVolumeCbm = carton.lengthCm * carton.widthCm * carton.heightCm / 1_000_000
return {
skuNo: `SKU-${box.boxType}-${context.marker}`.slice(0, 64),
cargoName: `${box.boxType} 测试产品`, cargoNameEn: `${box.boxType} test product`,
hsCode: '8518210000', customsHsCode: '8518210000', packageType: 'CARTON',
lengthCm: box.lengthCm, widthCm: box.widthCm, heightCm: box.heightCm,
packingGroupNo: dropship ? mixedPackingGroupNo : null,
lengthCm: carton.lengthCm, widthCm: carton.widthCm, heightCm: carton.heightCm,
unitsPerPackage, packageCount, grossWeightKg: (10 + index) * packageCount,
volumeCbm: volumeCbm * packageCount, totalQuantity: unitsPerPackage * packageCount,
volumeCbm: cartonVolumeCbm * packageCount, totalQuantity: unitsPerPackage * packageCount,
netWeightKg: (9 + index) * packageCount,
unitDeclaredValue: 8.5, declaredValue: unitsPerPackage * packageCount * 8.5, currency: 'USD',
material: '塑料 / Plastic', usage: '接口验收 / API acceptance',
+2 -2
View File
@@ -518,13 +518,13 @@ foreach ($pdaApplication in $pdaApplications) {
}
}
# 海外 PDA 已开放拆柜收货、库内、换标和现场查询真实作业。
# 海外 PDA 已开放拆柜收货、移库、托盘、换标和现场查询真实作业;库存盘点仅保留底层实现。
$overseasWorkbenchPath = Join-Path $workspaceRoot 'apps/pda-overseas-uniapp/src/data/overseas-workbench.js'
$overseasWorkbench = Read-RequiredText $overseasWorkbenchPath
Assert-Matches $overseasWorkbench "(?s)name:\s*'拆柜收货'.{0,300}?available:\s*true" 'Overseas PDA 拆柜收货开关'
Assert-Matches $overseasWorkbench "(?s)name:\s*'托盘管理'.{0,300}?available:\s*true.{0,200}?route:\s*'/pages/pallet/pallet'" 'Overseas PDA 托盘管理开关'
Assert-NotContains $overseasWorkbench "key: 'stocktake'" 'Overseas PDA 隐藏库存盘点入口'
foreach ($enabledOperation in @(
@{ Name = '库存盘点'; Route = '/pages/stocktake/stocktake' },
@{ Name = '商品移库'; Route = '/pages/move/move' },
@{ Name = '换标收货'; Route = '/pages/relabel/relabel\?op=relabelReceive' },
@{ Name = '换标执行'; Route = '/pages/relabel/relabel\?op=relabelExecute' },