fix: align dropship acceptance with precreated pallets

This commit is contained in:
2026-09-11 12:26:52 +08:00
parent e61050c490
commit b57ca7def7
5 changed files with 57 additions and 60 deletions
@@ -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++){
@@ -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 -> {
@@ -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
@@ -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);
@@ -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,
})