From 312501cc7036fb06327e333e872e953e9cf41084 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 18:19:58 +0800 Subject: [PATCH] feat: use FBA internal code in pallet labels --- firstleg/AGENTS.md | 2 +- .../client/WarehouseMasterDataClient.java | 2 +- .../baiyue/firstleg/dto/LoadingListDtos.java | 5 +- .../mapper/LoadingPalletLabelMapper.java | 3 +- .../service/LoadingPalletLabelService.java | 68 +++++++++++++++---- .../resources/openapi/loading-lists-v1.yaml | 2 +- .../LoadingListControllerContractTest.java | 2 +- .../service/OceanBookingServiceTest.java | 2 +- .../service/ContainerServiceTest.java | 2 +- .../LoadingPalletLabelServiceTest.java | 19 +++++- 10 files changed, 84 insertions(+), 23 deletions(-) diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index 0b2f42efb..872160af5 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -9,7 +9,7 @@ ## 模块入口 -散货托盘码:`service/LoadingPalletLabelService.java`、`mapper/LoadingPalletLabelMapper.java`、`controller/PlatformLoadingListController.java`,契约 `src/main/resources/openapi/loading-lists-v1.yaml` 的 `/pallet-labels/prepare` 与 `/pallet-labels/print`。仅在计划 `LOADING/COMPLETED` 且真实柜号有效时,为非一件代发的 FBA 明细按柜号、FBA 仓编码、三位序号生成冻结标签;重复生成须核对已有标签与明细一致,冲突直接拒绝。支持全部或按仓指定张数输出 100×100 mm PDF;装柜完成通过 `LoadingCompletedOutboxService.java` 传递来源托盘。 +散货托盘码:`service/LoadingPalletLabelService.java`、`mapper/LoadingPalletLabelMapper.java`、`controller/PlatformLoadingListController.java`,契约 `src/main/resources/openapi/loading-lists-v1.yaml` 的 `/pallet-labels/prepare` 与 `/pallet-labels/print`。仅在计划 `LOADING/COMPLETED` 且真实柜号有效时,为非一件代发的 FBA 明细按柜号、FBA 地址内部编号、三位序号生成冻结标签;内部编号从 Warehouse 启用主数据读取,缺失或同柜重复时拒绝生成。重复生成须核对已有标签与明细一致,冲突直接拒绝。支持全部或按仓指定张数输出 100×100 mm PDF;装柜完成通过 `LoadingCompletedOutboxService.java` 传递来源托盘。 | 目标 | 入口 | |---|---| diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseMasterDataClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseMasterDataClient.java index d24820e4a..3985ff209 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseMasterDataClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseMasterDataClient.java @@ -18,7 +18,7 @@ public interface WarehouseMasterDataClient { record Detail(Warehouse warehouse) { } - record Warehouse(Long id, Long tenantId, String warehouseCode, String warehouseName, + record Warehouse(Long id, Long tenantId, String warehouseCode, String internalCode, String warehouseName, String warehouseType, String status) { } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java index 924466efc..f3947ae1b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java @@ -133,13 +133,14 @@ public final class LoadingListDtos { public record PalletLabel( Long labelId, Long itemId, Long accountId, Long inboundId, String inboundCode, - String containerNo, String destinationWarehouseCode, String destinationWarehouseName, + String containerNo, String destinationWarehouseCode, String destinationWarehouseInternalCode, + String destinationWarehouseName, int palletIndex, String palletCode ) { } public record PalletLabelGroup( - String destinationWarehouseCode, String destinationWarehouseName, + String destinationWarehouseCode, String destinationWarehouseInternalCode, String destinationWarehouseName, int totalCount, List labels ) { public PalletLabelGroup { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java index 5c798ed19..df49db14f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java @@ -13,7 +13,7 @@ import java.util.List; public interface LoadingPalletLabelMapper { @Select(""" SELECT p.id AS plan_id,p.plan_no,p.plan_status,b.container_no,b.warehouse_id, - i.id AS item_id,i.account_id,i.inbound_id,i.inbound_code, + i.id AS item_id,i.account_id,i.inbound_id,i.inbound_code,i.destination_warehouse_id, i.destination_warehouse_code,i.destination_warehouse_name, COALESCE(NULLIF(i.pallet_count,0), CEIL(COALESCE(NULLIF(o.pallet_count,0), @@ -56,6 +56,7 @@ public interface LoadingPalletLabelMapper { class SourceRow { private Long planId; private String planNo; private String planStatus; private String containerNo; private Long warehouseId; private Long itemId; private Long accountId; private Long inboundId; + private Long destinationWarehouseId; private String inboundCode; private String destinationWarehouseCode; private String destinationWarehouseName; private Integer palletCount; private String fulfillmentType; private Integer loadSequence; private String destinationType; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java index a9eafa154..ce875d97c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPalletLabelService.java @@ -6,6 +6,7 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.common.event.ContainerNumbers; import com.baiyue.firstleg.client.PrintPdfClient; +import com.baiyue.firstleg.client.WarehouseMasterDataClient; import com.baiyue.firstleg.dto.LoadingListDtos.PalletLabel; import com.baiyue.firstleg.dto.LoadingListDtos.PalletLabelGroup; import com.baiyue.firstleg.dto.LoadingListDtos.PalletLabelManifest; @@ -34,12 +35,13 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -/** 散货托盘码是柜号与 FBA 仓维度的稳定业务标识;一件代发继续使用装箱打托事实。 */ +/** 散货托盘码按真实柜号、FBA 地址内部编号和仓内序号组成;一件代发继续使用装箱打托事实。 */ @Service public class LoadingPalletLabelService { private static final BigDecimal PAGE_MM = new BigDecimal("100"); @Resource private LoadingPalletLabelMapper mapper; @Resource private PrintPdfClient printClient; + @Resource private WarehouseMasterDataClient warehouseClient; @Resource private ObjectMapper objectMapper; @Value("${baiyue.security.internal-service-token}") private String internalToken; @@ -58,11 +60,13 @@ public class LoadingPalletLabelService { List standard = sources.stream().filter(row -> !"ONE_CLICK_DROPSHIP".equals(row.getFulfillmentType())) .filter(row -> "FBA".equals(row.getDestinationType())) .filter(row -> Objects.requireNonNullElse(row.getPalletCount(), 0) > 0).toList(); - if (standard.stream().anyMatch(row -> !StringUtils.hasText(row.getDestinationWarehouseCode()))) { - throw new BizException(ResultCode.CONFLICT, "散货明细缺少 FBA 仓编码,不能生成托盘码"); + if (standard.stream().anyMatch(row -> row.getDestinationWarehouseId() == null + || !StringUtils.hasText(row.getDestinationWarehouseCode()))) { + throw new BizException(ResultCode.CONFLICT, "散货明细缺少 FBA 仓信息,不能生成托盘码"); } + Map internalCodes = resolveInternalCodes(standard); List existing = mapper.labels(planId); - List expected = expected(first, standard, containerNo); + List expected = expected(first, standard, containerNo, internalCodes); if (existing.isEmpty()) { expected.forEach(row -> { if (mapper.insert(row) != 1) throw new BizException(ResultCode.CONFLICT, "托盘码生成失败,请刷新后重试"); }); existing = mapper.labels(planId); @@ -91,19 +95,21 @@ public class LoadingPalletLabelService { catch (FeignException | NoFallbackAvailableException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务暂不可用,请稍后重试"); } } - private List expected(SourceRow plan, List sources, String containerNo) { - Map indexes = new LinkedHashMap<>(); + private List expected(SourceRow plan, List sources, String containerNo, + Map internalCodes) { + Map indexes = new LinkedHashMap<>(); List result = new ArrayList<>(); for (SourceRow source : sources) { - String code = source.getDestinationWarehouseCode().trim().toUpperCase(Locale.ROOT); + String warehouseCode = source.getDestinationWarehouseCode().trim().toUpperCase(Locale.ROOT); + String internalCode = internalCodes.get(source.getDestinationWarehouseId()); for (int local = 0; local < source.getPalletCount(); local++) { - int index = indexes.merge(code, 1, Integer::sum); + int index = indexes.merge(source.getDestinationWarehouseId(), 1, Integer::sum); if (index > 999) throw new BizException(ResultCode.CONFLICT, "单柜同一 FBA 仓托盘数不能超过 999"); LabelRow row = new LabelRow(); row.setLabelId(IdWorker.getId()); row.setPlanId(plan.getPlanId()); row.setItemId(source.getItemId()); row.setAccountId(source.getAccountId()); row.setInboundId(source.getInboundId()); row.setInboundCode(source.getInboundCode()); row.setContainerNo(containerNo); - row.setDestinationWarehouseCode(code); row.setDestinationWarehouseName(source.getDestinationWarehouseName()); - row.setPalletIndex(index); row.setPalletCode(containerNo + "-" + code + "-" + String.format("%03d", index)); + row.setDestinationWarehouseCode(warehouseCode); row.setDestinationWarehouseName(source.getDestinationWarehouseName()); + row.setPalletIndex(index); row.setPalletCode(containerNo + "-" + internalCode + "-" + String.format("%03d", index)); if (row.getPalletCode().length() > 128) throw new BizException(ResultCode.CONFLICT, "托盘码长度超过系统限制"); result.add(row); } @@ -111,6 +117,37 @@ public class LoadingPalletLabelService { return result; } + private Map resolveInternalCodes(List sources) { + Map codes = new LinkedHashMap<>(); + Map owners = new LinkedHashMap<>(); + for (SourceRow source : sources) { + long warehouseId = source.getDestinationWarehouseId(); + if (codes.containsKey(warehouseId)) continue; + WarehouseMasterDataClient.Warehouse warehouse; + try { + ApiResponse response = warehouseClient.active(internalToken, 0L, warehouseId); + warehouse = response == null || !response.successful() || response.data() == null + ? null : response.data().warehouse(); + } catch (FeignException | NoFallbackAvailableException exception) { + throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库主数据暂不可用,请稍后重试"); + } + if (warehouse == null || !"FBA".equals(warehouse.warehouseType())) { + throw new BizException(ResultCode.CONFLICT, "目的 FBA 仓不存在或未启用"); + } + String internalCode = normalize(warehouse.internalCode()); + if (internalCode == null) { + throw new BizException(ResultCode.CONFLICT, "FBA 仓“" + warehouse.warehouseCode() + "”未配置内部编号"); + } + internalCode = internalCode.toUpperCase(Locale.ROOT); + Long owner = owners.putIfAbsent(internalCode, warehouseId); + if (owner != null && owner != warehouseId) { + throw new BizException(ResultCode.CONFLICT, "同柜 FBA 仓内部编号重复,请先修正仓库主数据"); + } + codes.put(warehouseId, internalCode); + } + return codes; + } + private void requireSame(List expected, List actual) { List left = expected.stream().map(this::identity).toList(); List right = actual.stream().map(this::identity).toList(); @@ -122,12 +159,19 @@ public class LoadingPalletLabelService { Map> grouped = rows.stream().collect(java.util.stream.Collectors.groupingBy( LabelRow::getDestinationWarehouseCode, LinkedHashMap::new, java.util.stream.Collectors.toList())); List groups = grouped.values().stream().map(values -> new PalletLabelGroup( - values.get(0).getDestinationWarehouseCode(), values.get(0).getDestinationWarehouseName(), values.size(), + values.get(0).getDestinationWarehouseCode(), internalCode(values.get(0)), + values.get(0).getDestinationWarehouseName(), values.size(), values.stream().map(this::label).toList())).toList(); String containerNo = rows.isEmpty() ? plan.getContainerNo() : rows.get(0).getContainerNo(); return new PalletLabelManifest(plan.getPlanId(), plan.getPlanNo(), containerNo, rows.size(), groups); } - private PalletLabel label(LabelRow row) { return new PalletLabel(row.getLabelId(), row.getItemId(), row.getAccountId(), row.getInboundId(), row.getInboundCode(), row.getContainerNo(), row.getDestinationWarehouseCode(), row.getDestinationWarehouseName(), row.getPalletIndex(), row.getPalletCode()); } + private PalletLabel label(LabelRow row) { return new PalletLabel(row.getLabelId(), row.getItemId(), row.getAccountId(), row.getInboundId(), row.getInboundCode(), row.getContainerNo(), row.getDestinationWarehouseCode(), internalCode(row), row.getDestinationWarehouseName(), row.getPalletIndex(), row.getPalletCode()); } + private String internalCode(LabelRow row) { + String prefix = row.getContainerNo() + "-"; + String suffix = "-" + String.format("%03d", row.getPalletIndex()); + String palletCode = row.getPalletCode(); + return palletCode.substring(prefix.length(), palletCode.length() - suffix.length()); + } private static String normalize(String value) { return value == null || value.isBlank() ? null : value.trim(); } private ResponseEntity requirePdf(ResponseEntity response) { diff --git a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml index 773d54235..06cc71d0c 100644 --- a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml +++ b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml @@ -77,7 +77,7 @@ paths: /api/platform/v1/first-leg/loading-plans/{planId}/pallet-labels/prepare: post: summary: 生成并查询散货托盘码清单 - description: 预装确认且已有真实柜号后,按柜号-FBA仓编码-三位序号生成稳定清单;一件代发不参与。 + description: 预装确认且已有真实柜号后,按柜号-FBA地址内部编号-三位序号生成稳定清单;一件代发不参与。内部编号缺失时拒绝生成。 parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/WarehouseId'}] responses: '200': {description: 按 FBA 仓分组的散货托盘码清单, content: {application/json: {schema: {type: object}}}} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java index 4f52501c2..f1fb2e42f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java @@ -77,7 +77,7 @@ class LoadingListControllerContractTest { .contains("/loading-plans/{planId}/lifecycle") .contains("/loading-plans/{planId}/pallet-labels/prepare") .contains("/loading-plans/{planId}/pallet-labels/print") - .contains("柜号-FBA仓编码-三位序号") + .contains("柜号-FBA地址内部编号-三位序号") .contains("不得以错误文案或 A0404 猜测阶段") .contains("confirmationStatus") .contains("canConfirm") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java index ab6b3acd1..643aef318 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java @@ -390,7 +390,7 @@ class OceanBookingServiceTest { when(priceBooks.channels(51L, 701L)).thenReturn(List.of(row)); when(warehouses.active(any(), eq(0L), eq(701L))).thenReturn(ApiResponse.success( new WarehouseMasterDataClient.Detail(new WarehouseMasterDataClient.Warehouse( - 701L, 0L, "SZX", "深圳中转仓", "DOMESTIC", "ACTIVE")))); + 701L, 0L, "SZX", null, "深圳中转仓", "DOMESTIC", "ACTIVE")))); when(suppliers.booking(81L, null)).thenReturn( new WarehouseSupplierClient.Snapshot(81L, "订舱代理", "BOOKING", "ACTIVE", List.of("MATSON"))); when(partners.active(any(), eq(0L), eq(71L))).thenReturn(ApiResponse.success( diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java index ea1f0b0c9..09e195623 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java @@ -468,7 +468,7 @@ class ContainerServiceTest { String type, String name) { return ApiResponse.success(new WarehouseMasterDataClient.Detail( - new WarehouseMasterDataClient.Warehouse(warehouseId, 0L, "CN-SHA-01", name, type, "ACTIVE"))); + new WarehouseMasterDataClient.Warehouse(warehouseId, 0L, "CN-SHA-01", null, name, type, "ACTIVE"))); } private static ContainerBookingEntity booking(String status, int version) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPalletLabelServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPalletLabelServiceTest.java index 46759a201..2e34d2b2a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPalletLabelServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPalletLabelServiceTest.java @@ -1,6 +1,8 @@ package com.baiyue.firstleg.service; import com.baiyue.firstleg.mapper.LoadingPalletLabelMapper; +import com.baiyue.firstleg.client.WarehouseMasterDataClient; +import com.baiyue.common.core.api.ApiResponse; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; @@ -18,21 +20,28 @@ class LoadingPalletLabelServiceTest { LoadingPalletLabelMapper mapper = mock(LoadingPalletLabelMapper.class); LoadingPalletLabelService service = new LoadingPalletLabelService(); ReflectionTestUtils.setField(service, "mapper", mapper); + WarehouseMasterDataClient warehouseClient = mock(WarehouseMasterDataClient.class); + ReflectionTestUtils.setField(service, "warehouseClient", warehouseClient); + ReflectionTestUtils.setField(service, "internalToken", "test-token"); List saved = new ArrayList<>(); when(mapper.lockSources(10L)).thenReturn(List.of(source(3, "LAX9", 1, "STANDARD"), source(1, "ONT8", 2, "STANDARD"), source(2, "ONT8", 1, "STANDARD"), source(4, "ONT8", 4, "ONE_CLICK_DROPSHIP"))); when(mapper.labels(10L)).thenAnswer(ignored -> List.copyOf(saved)); when(mapper.insert(any())).thenAnswer(invocation -> { saved.add(invocation.getArgument(0)); return 1; }); + when(warehouseClient.active("test-token", 0L, 101L)).thenReturn(warehouse("LAX9", "154")); + when(warehouseClient.active("test-token", 0L, 102L)).thenReturn(warehouse("ONT8", "CADF")); var manifest = service.prepare(10L, 20L); assertThat(manifest.totalCount()).isEqualTo(4); assertThat(manifest.groups()).extracting(group -> group.destinationWarehouseCode() + ":" + group.totalCount()) .containsExactly("LAX9:1", "ONT8:3"); + assertThat(manifest.groups()).extracting(group -> group.destinationWarehouseInternalCode()) + .containsExactly("154", "CADF"); assertThat(saved).extracting(LoadingPalletLabelMapper.LabelRow::getPalletCode) - .containsExactly("MSCU6639870-LAX9-001", "MSCU6639870-ONT8-001", - "MSCU6639870-ONT8-002", "MSCU6639870-ONT8-003"); + .containsExactly("MSCU6639870-154-001", "MSCU6639870-CADF-001", + "MSCU6639870-CADF-002", "MSCU6639870-CADF-003"); } private LoadingPalletLabelMapper.SourceRow source(long itemId, String destination, int pallets, String fulfillment) { @@ -40,7 +49,13 @@ class LoadingPalletLabelServiceTest { row.setPlanStatus("LOADING"); row.setContainerNo("MSCU6639870"); row.setWarehouseId(20L); row.setItemId(itemId); row.setAccountId(100L); row.setInboundId(itemId + 1000); row.setInboundCode("RK-" + itemId); row.setDestinationWarehouseCode(destination); + row.setDestinationWarehouseId("LAX9".equals(destination) ? 101L : 102L); row.setDestinationWarehouseName(destination); row.setPalletCount(pallets); row.setFulfillmentType(fulfillment); row.setDestinationType("FBA"); return row; } + + private ApiResponse warehouse(String code, String internalCode) { + return ApiResponse.success(new WarehouseMasterDataClient.Detail(new WarehouseMasterDataClient.Warehouse( + "LAX9".equals(code) ? 101L : 102L, 0L, code, internalCode, code, "FBA", "ACTIVE"))); + } }