diff --git a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java index b4ab07155..f05f4adeb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java @@ -428,8 +428,11 @@ public class ContainerCustomsService { if (mapper.insertClearanceResult(resultId, batch.id(), batch.customsContainerId(), type, resultStatus, batch.customsEntryNo(), request.occurredAt(), trim(request.resolution()), request.evidenceFileId(), operatorId, operatorName, null) != 1) throw conflict("海关结果保存失败"); - if ("OPEN".equals(resultStatus)) mapper.markDeclarationResultPending(batch.id()); - else mapper.closeDeclarationBatch(batch.id()); + if ("OPEN".equals(resultStatus) || mapper.openClearanceResultCount(batch.id()) > 0) { + mapper.markDeclarationResultPending(batch.id()); + } else { + mapper.closeDeclarationBatch(batch.id()); + } ContainerRow container = mapper.lockContainerById(batch.customsContainerId()); if (container != null) history(container, "RECORD_CUSTOMS_CLEARANCE_RESULT", container.status(), container.status(), type, operatorId, operatorName); completeCommand(operatorId, action, key, resultId); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java index 279227547..153637d77 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java @@ -4,11 +4,14 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.customs.container.dto.ContainerCustomsDtos.ActionRequest; import com.baiyue.firstleg.customs.container.dto.ContainerCustomsDtos.ScopeRequest; +import com.baiyue.firstleg.customs.container.dto.ContainerCustomsDtos.ClearanceResultRequest; import com.baiyue.firstleg.customs.container.mapper.ContainerCustomsMapper; import com.baiyue.firstleg.customs.container.service.ContainerCustomsService; import com.baiyue.firstleg.customs.client.CustomsFileClient; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.*; import org.springframework.context.ApplicationEventPublisher; import org.springframework.mock.web.MockMultipartFile; @@ -438,6 +441,44 @@ class ContainerCustomsServiceTest { verify(events,times(1)).publishEvent(new ContainerCustomsService.CustomsCompletedEvent(8L)); } + @ParameterizedTest + @CsvSource({ + "INSPECTION,true,1,true", + "RELEASED,false,2,true", + "OTHER,true,0,false", + "RELEASED,false,0,false", + "DETAINED,false,1,true" + }) + void newlyResolvedResultDoesNotCloseBatchWithOtherOpenExceptions( + String type, boolean resolved, int openResults, boolean remainsPending) { + var at = java.time.LocalDateTime.of(2026,9,30,11,0); + var batch = new ContainerCustomsMapper.DeclarationBatchRow( + 41L,2L,9L,"CD-LP9-01","RESULT_PENDING",3,"CUSTOMS-001","admin",at, + "admin",at,"",4,at,at); + String status = "RELEASED".equals(type) || resolved ? "RESOLVED" : "OPEN"; + when(mapper.claimCommand(anyLong(),eq(5L),anyString(),eq("result-key"),anyString())).thenReturn(1); + when(mapper.lockDeclarationBatch(41L)).thenReturn(batch); + when(mapper.insertClearanceResult(anyLong(),eq(41L),eq(2L),eq(type),eq(status), + eq("CUSTOMS-001"),eq(at),eq("人工处理说明"),isNull(),eq(5L),eq("admin"),isNull())).thenReturn(1); + when(mapper.openClearanceResultCount(41L)).thenReturn(openResults); + when(mapper.completeCommand(eq(5L),anyString(),eq("result-key"),anyLong())).thenReturn(1); + when(mapper.clearanceResult(anyLong())).thenAnswer(call -> new ContainerCustomsMapper.ClearanceResultRow( + call.getArgument(0),41L,2L,type,status,"CUSTOMS-001",at,"人工处理说明",null,5L,"admin",at,null)); + + var result = service.addClearanceResult(41L,5L,"admin","result-key", + new ClearanceResultRequest(4,type,at,"人工处理说明",null,resolved)); + + assertThat(result.resultStatus()).isEqualTo(status); + if (remainsPending) { + verify(mapper).markDeclarationResultPending(41L); + verify(mapper,never()).closeDeclarationBatch(anyLong()); + } else { + verify(mapper).closeDeclarationBatch(41L); + verify(mapper,never()).markDeclarationResultPending(anyLong()); + } + verifyNoInteractions(events,files); + } + private static ContainerCustomsMapper.DocumentRow document(long id){return new ContainerCustomsMapper.DocumentRow(id,2L,7L,10L,"source.bin","a".repeat(64),"application/octet-stream",1L,"ACTIVE",5L,java.time.LocalDateTime.now(),1,java.time.LocalDateTime.now(),1);} private static ContainerCustomsMapper.BrokerSubmissionRow brokerSubmission(){return new ContainerCustomsMapper.BrokerSubmissionRow(41L,2L,9L,"BR-LP9-01","SENT",3,"admin",java.time.LocalDateTime.now(),1);} private static ContainerCustomsMapper.ContainerRow container(String status,int version){return new ContainerCustomsMapper.ContainerRow(2L,9L,8L,status,null,null,null,null,null,null,null,null,version);} diff --git a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs index 6d447dff7..af1766291 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost-contract.test.mjs @@ -53,7 +53,7 @@ test('钱包闭环覆盖充值、授信、采购冻结结算和换标结算释 test('托盘汇总和拆柜成本验证总数、分摊及跨区金额守恒', () => { for (const token of [ '/api/platform/v1/pallets/summary', - '/api/platform/v1/warehouse-admin/pallets/summary', + '/api/platform/v1/overseas-warehouse-resources/pallets/summary', '/pallets/summary?page=1&size=20', 'OVERSEAS_CONTAINER', 'DESTUFF_PALLET', @@ -61,6 +61,7 @@ test('托盘汇总和拆柜成本验证总数、分摊及跨区金额守恒', () 'sourceFeeId', 'pollRequests=', ]) assert.ok(source.includes(token), token) + assert.doesNotMatch(source, /\/warehouse-admin\/pallets\/summary/) assert.match(source, /Number\(d\.totalCount\) === Number\(d\.idleCount\) \+ Number\(d\.occupiedCount\)/) assert.match(source, /allocated === money\(fact\.expectedPalletCost\)/) assert.match(source, /delivered\.reduce[\s\S]*fact\.expectedPalletCost/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs index ac32a2cbc..9fb7d1a42 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs @@ -304,7 +304,7 @@ async function verifyPalletSummaries(context, warehouseId) { success(domestic) && summaryValid(d, ['totalCount', 'idleCount', 'occupiedCount']) && Number(d.totalCount) === Number(d.idleCount) + Number(d.occupiedCount), `total=${d?.totalCount}, idle=${d?.idleCount}, occupied=${d?.occupiedCount}`) - const overseas = await context.request('GET', '/api/platform/v1/warehouse-admin/pallets/summary?page=1&size=20') + const overseas = await context.request('GET', '/api/platform/v1/overseas-warehouse-resources/pallets/summary?page=1&size=20') const o = overseas?.body?.data assert(context, '海外托盘汇总含闲置、占用和报废数量', overseas, success(overseas) && summaryValid(o, ['totalCount', 'idleCount', 'occupiedCount', 'scrappedCount'])