This commit is contained in:
2026-08-25 17:54:50 +08:00
parent 25cb3bfa54
commit 62407610cb
6 changed files with 68 additions and 14 deletions
@@ -24,7 +24,7 @@ public interface PdaPalletOperationMapper extends BaseMapper<PdaPalletOperation>
@Param("idempotencyKey") String idempotencyKey);
@Select(CONTENT_SELECT + "WHERE b.overseas_warehouse_id=#{warehouseId} AND p.pallet_id=#{palletId} " +
"AND (p.available_packages+p.hold_packages+p.allocated_packages+p.picked_packages+p.receiving_packages)&gt;0 " +
"AND (p.available_packages+p.hold_packages+p.allocated_packages+p.picked_packages+p.receiving_packages)>0 " +
"ORDER BY b.customer_id,b.inbound_code,p.id")
List<PalletInventoryRow> content(@Param("warehouseId") String warehouseId,
@Param("palletId") long palletId);
@@ -55,7 +55,7 @@ public interface PdaStocktakeItemMapper extends BaseMapper<PdaStocktakeItem> {
int counted(@Param("taskId") long taskId);
@Select("SELECT COUNT(*) FROM ll_pda_stocktake_item WHERE task_id=#{taskId} " +
"AND actual_packages IS NOT NULL AND difference_packages&lt;&gt;0")
"AND actual_packages IS NOT NULL AND difference_packages<>0")
int differences(@Param("taskId") long taskId);
@Select("SELECT id item_id,customer_id,balance_id,position_id,sku_code,product_name,inbound_code," +
@@ -1,5 +1,7 @@
package com.baiyue.lastleg.palletops.controller;
import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
@@ -24,4 +26,12 @@ class PdaPalletOperationControllerContractTest {
.flatMap(annotation -> Arrays.stream(annotation.value())).collect(Collectors.toSet());
assertEquals(Set.of("/pallets/resolve", "/assemble", "/disassemble", "/swap", "/unbind"), paths);
}
@Test void palletContentAnnotationUsesNativeGreaterThanOperator() throws Exception {
String sql = String.join("", PdaPalletOperationMapper.class.getMethod("content", String.class, long.class)
.getAnnotation(Select.class).value());
assertTrue(sql.contains("receiving_packages)>0"));
assertFalse(sql.contains("&lt;"));
assertFalse(sql.contains("&gt;"));
}
}
@@ -4,6 +4,8 @@ import com.baiyue.lastleg.pda.move.controller.PdaInventoryMoveController;
import com.baiyue.lastleg.pda.move.dto.PdaInventoryMoveRequest;
import com.baiyue.lastleg.pda.stocktake.controller.PdaStocktakeController;
import com.baiyue.lastleg.pda.stocktake.dto.PdaStocktakeRequests;
import com.baiyue.lastleg.pda.stocktake.mapper.PdaStocktakeItemMapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -38,4 +40,11 @@ class PdaInventoryOperationContractTest {
assertThat(fields).doesNotContainAnyElementsOf(forbidden);
}
}
@Test
void stocktakeAnnotationSqlUsesNativeComparisonOperators() throws Exception {
String sql = String.join("", PdaStocktakeItemMapper.class.getMethod("differences", long.class)
.getAnnotation(Select.class).value());
assertThat(sql).contains("difference_packages<>0").doesNotContain("&lt;", "&gt;");
}
}
@@ -54,7 +54,13 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.match(source, /目的仓 .* 不得代替物理仓/)
assert.match(source, /retained as acceptance evidence/)
assert.match(source, /preferredProducer\?\.cartonMarks\?\.length/)
assert.match(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/)
assert.match(source, /const cartonCandidates =/)
assert.match(source, /candidateExcluded = notFound\(response\)/)
assert.match(source, /if \(candidateExcluded\) continue/)
assert.match(source, /const fixturesConserved = cartonFixtures\.length === expectedPending/)
assert.match(source, /OVERSEAS实装箱唛与待收箱数守恒/)
assert.match(source, /for \(const \[index, fixture\] of cartonFixtures\.entries\(\)\)/)
assert.doesNotMatch(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/)
assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
assert.match(source, /phase4OverseasPda\.orderId = id\(order\.id\)/)
assert.match(source, /phase4OverseasPda\.customerId = id\(order\.customerId\)/)
@@ -338,23 +338,52 @@ async function receiveFba(context, token, inbound, pallet, producer) {
}
async function receiveOverseas(context, token, inbounds, pallets, producerCartonMarks = []) {
const cartonMarks = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks])
const cartonCandidates = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks])
.map(id).filter(nonBlank))]
if (!cartonMarks.length) {
if (!cartonCandidates.length) {
blockerGap(context, 'OVERSEAS逐箱链', '缺少本轮PDA任务冻结箱唛,逐箱实收不能使用伪造箱号')
return false
}
for (const [index, cartonMark] of cartonMarks.entries()) {
const resolved = await pda(context, token, 'POST', `解析OVERSEAS冻结箱唛${index + 1}`,
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark })
const inbound = inbounds.find((item) => item.inboundCode === resolved.data?.inboundCode)
const pallet = inbound ? pallets.get(inbound.inboundCode) : null
if (!success(resolved.response) || !inbound || !pallet) {
blockerGap(context, `OVERSEAS箱唛${cartonMark}归属`,
`冻结箱唛未解析到带真实托盘的本柜入仓码;inboundCode=${resolved.data?.inboundCode || 'missing'}`,
'fixture-mismatch', resolved.response)
const expectedPending = inbounds.reduce((total, inbound) => total + Number(inbound.pendingPackages || 0), 0)
const cartonFixtures = []
let lastResponse = null
for (const [index, cartonMark] of cartonCandidates.entries()) {
const response = await context.requestOverseas('POST',
'/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }, {}, token)
lastResponse = response
const candidateExcluded = notFound(response)
context.addStep(MODULE, `筛选OVERSEAS实装箱唛候选${index + 1}`, response,
success(response) || candidateExcluded,
`${candidateExcluded ? 'candidateSelected=false' : 'candidateSelected=true'}; ${trace(response)}`, OPTIONS)
if (candidateExcluded) continue
if (!success(response)) {
blockerGap(context, `OVERSEAS箱唛候选${cartonMark}预检`,
'候选箱唛既非本柜冻结事实,也不是未被最终实装选中的A0404;禁止继续收货',
'fixture-mismatch', response)
return false
}
const resolved = response.body?.data
const inbound = inbounds.find((item) => item.inboundCode === resolved?.inboundCode)
const pallet = inbound ? pallets.get(inbound.inboundCode) : null
if (!inbound || !pallet) {
blockerGap(context, `OVERSEAS箱唛${cartonMark}归属`,
`冻结箱唛未解析到带真实托盘的本柜入仓码;inboundCode=${resolved?.inboundCode || 'missing'}`,
'fixture-mismatch', response)
return false
}
cartonFixtures.push({ cartonMark, pallet })
}
const fixturesConserved = cartonFixtures.length === expectedPending
context.assert(MODULE, 'OVERSEAS实装箱唛与待收箱数守恒', lastResponse, fixturesConserved,
`resolvedFixtures=${cartonFixtures.length}, pendingPackages=${expectedPending}; ${trace(lastResponse)}`, OPTIONS)
if (!fixturesConserved) {
blockerGap(context, 'OVERSEAS逐箱冻结事实',
`候选池未完整覆盖本柜真实冻结箱唛;resolvedFixtures=${cartonFixtures.length}, pendingPackages=${expectedPending}`,
'fixture-mismatch', lastResponse)
return false
}
for (const [index, fixture] of cartonFixtures.entries()) {
const { cartonMark, pallet } = fixture
const body = { cartonMark, palletCode: pallet.palletCode,
idempotencyKey: key(context, `overseas-carton-${index}`) }
const first = await pda(context, token, 'POST', `OVERSEAS逐箱实收${index + 1}`,