From b57ca7def7e0e4902e265ea1c767e2fb26386edf Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 12:26:52 +0800 Subject: [PATCH] fix: align dropship acceptance with precreated pallets --- .../service/DropshipPalletizationService.java | 1 + .../service/InboundMeasurementService.java | 5 + ...oundMeasurementControllerContractTest.java | 2 +- ...tformLoadingPdaControllerContractTest.java | 2 +- .../inbound-measurement.mjs | 107 ++++++++---------- 5 files changed, 57 insertions(+), 60 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java index c5d59aa5f..924cbc16c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java @@ -27,6 +27,7 @@ public class DropshipPalletizationService { if (!"ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) return; if (inbound.getTotalPackageCount()==null || inbound.getTotalPackageCount()<1) throw invalid("一件代发测量完成箱数无效,不能下发清点组托任务"); if (inbound.getPalletCount()==null || inbound.getPalletCount()<1) throw invalid("一件代发预计托数无效,不能生成托盘码"); + if (inbound.getPalletCount()>inbound.getTotalPackageCount()) throw invalid("一件代发预计托数不能超过总箱数"); long taskId=IdWorker.getId(); if(mapper.insertTask(taskId,inbound.getTenantId(),inbound.getId(),inbound.getWarehouseId(),inbound.getTotalPackageCount())!=1) return; for(int sequence=1;sequence<=inbound.getPalletCount();sequence++){ diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index 36200d45d..b8c26a5e2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -135,6 +135,11 @@ public class InboundMeasurementService { && (request.expectedPalletCount() == null || request.expectedPalletCount() < 1)) { throw new BizException(ResultCode.VALIDATION_FAILED, "一件代发必须录入预计托数"); } + if ("ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType()) + && inbound.getTotalPackageCount() != null + && request.expectedPalletCount() > inbound.getTotalPackageCount()) { + throw new BizException(ResultCode.VALIDATION_FAILED, "一件代发预计托数不能超过总箱数"); + } validateInboundDetails(tenantId, inbound, request.items()); request.items().stream().map(InboundMeasurementRequests.ItemRequest::chargeRuleId) .filter(java.util.Objects::nonNull).distinct().forEach(ruleId -> { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java index b70b2dd77..fae7ddf8a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementControllerContractTest.java @@ -37,7 +37,7 @@ class PlatformInboundMeasurementControllerContractTest { void draftRequestOnlyAcceptsEditableGoodsNameAndMeasurementItems() { assertThat(Arrays.stream(InboundMeasurementRequests.SaveDraftRequest.class.getRecordComponents()) .map(RecordComponent::getName)).containsExactly( - "measurementId", "version", "actualGoodsName", "items"); + "measurementId", "version", "actualGoodsName", "expectedPalletCount", "items"); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java index c544acbb8..2c58647f8 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java @@ -13,7 +13,7 @@ class PlatformLoadingPdaControllerContractTest { @Test void exposesAssignmentWorkAndReviewWithDedicatedAuthorities() throws Exception { authority("task", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class, Long.class, long.class); - authority("pendingTasks", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class); + authority("pendingTasks", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class, String.class); authority("inventorySnapshot", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class, long.class); authority("claim", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, ClaimRequest.class); authority("assign", "platform:firstleg:pda:assign", AuthenticatedUser.class, long.class, AssignRequest.class); diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index 7ccb05bc8..d088c1f97 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -90,7 +90,7 @@ async function waitForCompletedMeasurement(context, measurementPath, expectedSta export async function completeDropshipPalletization(context, { inboundId, inboundCode, warehouseId, measurement }) { const pendingResponse = await context.request('GET', - `/api/platform/v1/first-leg/pda-dropship/tasks/pending?keyword=${encodeURIComponent(inboundCode)}`) + `/api/platform/v1/first-leg/pda-dropship/tasks/pending?warehouseId=${warehouseId}&keyword=${encodeURIComponent(inboundCode)}`) const pending = context.apiSuccess(MODULE, '查询一件代发待合托任务', pendingResponse, `inboundId=${inboundId}`, OPTIONS) const task = (pending || []).find((item) => asId(item?.inboundId) === inboundId @@ -110,31 +110,17 @@ export async function completeDropshipPalletization(context, { inboundId, inboun `taskStatus=${claimed?.taskStatus ?? 'missing'}`, OPTIONS) const totalCartons = Number(measurement?.actualPackageCount) - const createPath = `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/create-and-bind` + const expectedPalletCount = Number(measurement?.expectedPalletCount) const taskPath = `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}?warehouseId=${warehouseId}` - const createPayload = { warehouseId, operationId: `${context.marker}-dropship-create-bind`.slice(0, 128) } - let palletNo = '' + const generatedPallets = Array.isArray(claimed?.pallets) ? claimed.pallets : [] + context.assert(MODULE, '审核通过时已按预计托数预生成全部DP托盘码', claimResponse, + generatedPallets.length === expectedPalletCount && generatedPallets.every((item) => + item?.status === 'OPEN' && Number(item?.cartonCount) === 0 && /^DP-\d{6}-\d{4}$/.test(item?.palletNo || '')), + `expected=${expectedPalletCount}, generated=${generatedPallets.length}`, OPTIONS) + if (!generatedPallets.length) return null + let countedCartons = 0 const sealedCartonMarks = [] - let pallet = null - - const palletSnapshot = (value) => JSON.stringify({ - counted: value?.countedCartons, bound: value?.boundCartons, unbound: value?.unboundCartons, - pallets: value?.pallets?.map((item) => ({ id: asId(item.palletId), no: item.palletNo, - count: item.cartonCount, status: item.status, version: item.version })), - }) - async function rejectEmptyCreation(before, suffix) { - const response = await context.request('POST', createPath, - { ...createPayload, operationId: `${context.marker}-dropship-empty-${suffix}`.slice(0, 128) }) - context.assert(MODULE, `无待绑箱创建托盘被拒绝-${suffix}`, response, - response.http === 200 && String(response.body?.code) === 'A0409', - `HTTP=${response.http}, code=${response.body?.code || 'missing'}`, OPTIONS) - const afterResponse = await context.request('GET', taskPath) - const after = context.apiSuccess(MODULE, `拒绝创建后回查任务-${suffix}`, afterResponse, '', OPTIONS) - context.assert(MODULE, `拒绝创建没有空托或计数漂移-${suffix}`, afterResponse, - palletSnapshot(after) === palletSnapshot(before), `pallets=${after?.pallets?.length ?? 'missing'}`, OPTIONS) - } - await rejectEmptyCreation(claimed, 'before-count') for (let index = 0; index < totalCartons; index += 1) { const boxCode = `${inboundCode}#${totalCartons}-${index + 1}` @@ -153,7 +139,7 @@ export async function completeDropshipPalletization(context, { inboundId, inboun actualSkuQuantity: Number(resolved.expectedSkuQuantity), discrepancyAction: 'MATCH', discrepancyRemark: null, - palletNo: index === 0 ? null : palletNo, + palletNo: index === 0 ? null : generatedPallets[Math.min(index, generatedPallets.length - 1)].palletNo, operationId: `${context.marker}-dropship-count-${index + 1}`.slice(0, 128), } const countResponse = await context.request('POST', @@ -184,55 +170,57 @@ export async function completeDropshipPalletization(context, { inboundId, inboun && Number(replayed?.unboundCartons) === Number(counted?.unboundCartons), `counted=${replayed?.countedCartons ?? 'missing'}, unbound=${replayed?.unboundCartons ?? 'missing'}`, OPTIONS) - const bindResponse = await context.request('POST', createPath, createPayload) - const bound = context.apiSuccess(MODULE, '系统发码创建托盘并绑定全部待组托箱', bindResponse, + const firstPallet = generatedPallets[0] + const bindPayload = { warehouseId, palletNo: firstPallet.palletNo, + operationId: `${context.marker}-dropship-bind-first`.slice(0, 128) } + const bindResponse = await context.request('POST', + `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/bind-pending`, bindPayload) + const bound = context.apiSuccess(MODULE, '后扫预生成托盘码并绑定全部待组托箱', bindResponse, `taskId=${taskId}`, OPTIONS) - pallet = bound?.pallets?.find((item) => Number(item.cartonCount) === 1) || null - palletNo = pallet?.palletNo || '' + const pallet = bound?.pallets?.find((item) => asId(item.palletId) === asId(firstPallet.palletId)) || null context.assert(MODULE, '待组托箱绑定到OPEN托盘且不再游离', bindResponse, Number(bound?.unboundCartons) === 0 && Number(pallet?.cartonCount) === 1 - && pallet?.status === 'OPEN' && /^DP-\d{6}-\d{4}$/.test(palletNo) - && bound?.pallets?.length === 1, + && pallet?.status === 'OPEN' && pallet?.palletNo === firstPallet.palletNo + && bound?.pallets?.length === expectedPalletCount, `unbound=${bound?.unboundCartons ?? 'missing'}, palletStatus=${pallet?.status ?? 'missing'}`, OPTIONS) - const createReplayResponse = await context.request('POST', createPath, createPayload) - const createReplay = context.apiSuccess(MODULE, '系统发码同operationId重放', createReplayResponse, '', OPTIONS) - context.assert(MODULE, '系统发码重放不新增托盘且业务码和计数稳定', createReplayResponse, - palletSnapshot(createReplay) === palletSnapshot(bound), `palletNo=${palletNo}`, OPTIONS) - await rejectEmptyCreation(bound, 'after-bind') } else { - pallet = counted?.pallet || pallet + const expectedPallet = generatedPallets[Math.min(index, generatedPallets.length - 1)] context.assert(MODULE, `第${index + 1}箱直接进入当前OPEN托盘`, countResponse, - counted?.pallet?.palletNo === palletNo && counted?.pallet?.status === 'OPEN', + counted?.pallet?.palletNo === expectedPallet.palletNo && counted?.pallet?.status === 'OPEN', `palletNo=${counted?.pallet?.palletNo ?? 'missing'}, status=${counted?.pallet?.status ?? 'missing'}`, OPTIONS) } } - context.assert(MODULE, '一件代发工作托盘覆盖测量冻结的全部箱数', null, - totalCartons > 0 && countedCartons === totalCartons && Number(pallet?.cartonCount) === totalCartons, - `counted=${countedCartons}, palletCartons=${pallet?.cartonCount ?? 'missing'}, expected=${totalCartons || 'missing'}`, OPTIONS) + const countedTaskResponse = await context.request('GET', taskPath) + let sealed = context.apiSuccess(MODULE, '清点完成后回查预生成托盘分配', countedTaskResponse, '', OPTIONS) + context.assert(MODULE, '预生成托盘覆盖测量冻结的全部箱数', countedTaskResponse, + totalCartons > 0 && countedCartons === totalCartons + && Number(sealed?.boundCartons) === totalCartons + && sealed?.pallets?.length === expectedPalletCount + && sealed?.pallets?.every((item) => Number(item?.cartonCount) > 0), + `counted=${countedCartons}, bound=${sealed?.boundCartons ?? 'missing'}, expected=${totalCartons || 'missing'}`, OPTIONS) - const palletId = asId(pallet?.palletId) - if (!palletId) return null - - const sealResponse = await context.request('POST', - `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/${palletId}/seal`, { - warehouseId, expectedVersion: Number(pallet?.version), - operationId: `${context.marker}-dropship-seal`.slice(0, 128), - }) - const sealed = context.apiSuccess(MODULE, '封托并完成一件代发合托任务', sealResponse, - `taskId=${taskId}, palletId=${palletId}`, OPTIONS) - context.assert(MODULE, '全部箱数封托后入仓单允许进入WAIT_SPLIT', sealResponse, + let lastSealResponse = countedTaskResponse + for (let index = 0; index < (sealed?.pallets || []).length; index += 1) { + const pallet = sealed.pallets.find((item) => item.status === 'OPEN') + if (!pallet) break + const sealResponse = await context.request('POST', + `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/${asId(pallet.palletId)}/seal`, { + warehouseId, expectedVersion: Number(pallet.version), + operationId: `${context.marker}-dropship-seal-${index + 1}`.slice(0, 128), + }) + lastSealResponse = sealResponse + sealed = context.apiSuccess(MODULE, `封托第${index + 1}个预生成托盘`, sealResponse, + `taskId=${taskId}, palletNo=${pallet.palletNo}`, OPTIONS) + } + context.assert(MODULE, '全部箱数封托后入仓单允许进入WAIT_SPLIT', lastSealResponse, sealed?.taskStatus === 'COMPLETED' && Number(sealed?.countedCartons) === Number(sealed?.totalCartons) && Number(sealed?.boundCartons) === Number(sealed?.totalCartons) && Number(sealed?.unboundCartons) === 0 && sealed?.pallets?.every((item) => item?.status === 'SEALED'), `taskStatus=${sealed?.taskStatus ?? 'missing'}, counted=${sealed?.countedCartons ?? 'missing'}, bound=${sealed?.boundCartons ?? 'missing'}`, OPTIONS) - const sealedReplayResponse = await context.request('POST', createPath, createPayload) - const sealedReplay = context.apiSuccess(MODULE, '封托完成后系统发码原操作仍可重放', sealedReplayResponse, '', OPTIONS) - context.assert(MODULE, '完成后重放不重开任务或新建空托', sealedReplayResponse, - sealedReplay?.taskStatus === 'COMPLETED' && palletSnapshot(sealedReplay) === palletSnapshot(sealed), - `taskStatus=${sealedReplay?.taskStatus || 'missing'}, palletNo=${palletNo}`, OPTIONS) - return { ...sealed, sealedCartonMarks, sealedSourcePalletNos: [palletNo] } + return { ...sealed, sealedCartonMarks, + sealedSourcePalletNos: (sealed?.pallets || []).map((item) => item.palletNo).filter(Boolean) } } async function provisionWarehouseResources(context, { inboundId, tenantId, warehouseId, palletCount }) { @@ -846,9 +834,13 @@ export async function run(context) { ? 'ADD' : measuredItem.coefficientMode ?? null, remark: context.marker, }] + const palletCount = Math.max(1, Math.min(originalPackageCount, + Number(context.context?.waitSplitVariant?.palletCount || 1))) const draftPayload = { measurementId: measured.measurementId, version: Number(measured.version), actualGoodsName: `墨家测量验收-${context.marker.slice(-12)}`, + expectedPalletCount: context.createdResources?.orders?.fulfillmentType === 'ONE_CLICK_DROPSHIP' + ? palletCount : null, items: draftItems, } const draftResponse = await context.request('PUT', @@ -864,7 +856,6 @@ export async function run(context) { return } - const palletCount = Math.max(1, Number(context.context?.waitSplitVariant?.palletCount || 1)) const warehouseResourcesReady = await provisionWarehouseResources(context, { inboundId, tenantId, warehouseId: asId(summary?.warehouseId), palletCount, })