This commit is contained in:
2026-09-16 12:02:38 +08:00
parent de29e639e4
commit 912feca56f
11 changed files with 275 additions and 17 deletions
@@ -14,6 +14,15 @@ import java.util.Map;
@FeignClient(name = "print-service", contextId = "firstlegPrintPdfClient",
url = "${baiyue.clients.print.url:}", path = "/internal/print/v1/pdfs")
public interface PrintPdfClient {
@PostMapping(value = "/fba-pallet-labels", consumes = "application/json")
ResponseEntity<byte[]> generateFbaPalletLabels(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestHeader(value = "X-Request-Id", required = false) String requestId,
@RequestBody FbaPalletRequest request);
record FbaPalletRequest(String templateCode, PageSpec pageSpec, String downloadFileName, List<FbaPalletLabel> labels) {}
record FbaPalletLabel(String fbaWarehouseCode, String palletCode) {}
@PostMapping(value = "/inbound-marks", consumes = "application/json")
ResponseEntity<byte[]> generateInboundMarks(
@RequestHeader("X-Internal-Service-Token") String token,
@@ -32,6 +32,8 @@ import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -636,11 +638,7 @@ public class InboundMeasurementService {
|| cargo.cargoId() <= 0 || !java.util.Objects.equals(cargo.orderId(), inbound.getOrderId()))) {
throw new BizException(ResultCode.CONFLICT, "订单服务返回的货物明细归属不一致");
}
long packages = cargoes.stream()
.map(OrderCargoClient.CargoLabelBinding::packageCount)
.filter(java.util.Objects::nonNull)
.mapToLong(Integer::longValue)
.sum();
long packages = physicalPackageCount(cargoes, inbound.getFulfillmentType());
if (packages > Integer.MAX_VALUE) {
throw new BizException(ResultCode.VALIDATION_FAILED, "客户最新申报总箱数超过系统上限");
}
@@ -671,6 +669,41 @@ public class InboundMeasurementService {
return new LatestDeclaration(snapshot, Map.copyOf(fbaShipmentNos));
}
/**
* 普通订单每条货物明细代表独立箱规;一件代发允许同一物理箱包含多个 SKU,
* 因此同一 packingGroupNo 只计算一次箱数。该口径与箱唛生成保持一致。
*/
private long physicalPackageCount(List<OrderCargoClient.CargoLabelBinding> cargoes,
String fulfillmentType) {
if (!"ONE_CLICK_DROPSHIP".equals(fulfillmentType)) {
return cargoes.stream().mapToLong(this::requireDeclaredPackageCount).sum();
}
Map<String, List<OrderCargoClient.CargoLabelBinding>> grouped = new LinkedHashMap<>();
int single = 0;
for (OrderCargoClient.CargoLabelBinding cargo : cargoes) {
requireDeclaredPackageCount(cargo);
String group = StringUtils.hasText(cargo.packingGroupNo())
? cargo.packingGroupNo().trim() : "__single_" + (++single);
grouped.computeIfAbsent(group, ignored -> new ArrayList<>()).add(cargo);
}
return grouped.entrySet().stream().mapToLong(entry -> {
List<OrderCargoClient.CargoLabelBinding> lines = entry.getValue();
int count = lines.get(0).packageCount();
if (lines.stream().anyMatch(line -> line.packageCount() != count)) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"同一混箱组合的箱数必须一致:" + entry.getKey());
}
return count;
}).sum();
}
private int requireDeclaredPackageCount(OrderCargoClient.CargoLabelBinding cargo) {
if (cargo.packageCount() == null || cargo.packageCount() < 1) {
throw new BizException(ResultCode.VALIDATION_FAILED, "订单货物明细缺少有效箱数");
}
return cargo.packageCount();
}
private List<OrderCargoClient.CargoLabelBinding> loadDeclarationCargoes(InboundOrderEntity inbound) {
try {
ApiResponse<List<OrderCargoClient.CargoLabelBinding>> response = orderCargoClient.listForCartonLabels(
@@ -38,7 +38,6 @@ import java.util.Objects;
/** 散货托盘码按真实柜号、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;
@@ -85,13 +84,12 @@ public class LoadingPalletLabelService {
: manifest.groups().stream().filter(group -> group.destinationWarehouseCode().equalsIgnoreCase(warehouseCode))
.flatMap(group -> group.labels().stream()).limit(count).toList();
if (selected.isEmpty() || (count != null && selected.size() != count)) throw new BizException(ResultCode.BAD_REQUEST, "所选 FBA 仓或打印张数无效");
List<PrintPdfClient.Label> labels = selected.stream().map(label -> new PrintPdfClient.Label(
label.palletCode(), label.destinationWarehouseName(), "PALLET " + label.palletIndex(),
"CONTAINER " + label.containerNo(), label.palletCode(), null)).toList();
List<PrintPdfClient.FbaPalletLabel> labels = selected.stream().map(label -> new PrintPdfClient.FbaPalletLabel(
label.destinationWarehouseCode(), label.palletCode())).toList();
String suffix = warehouseCode == null ? "全部" : warehouseCode + "-" + count + "张";
var pdfRequest = new PrintPdfClient.InboundMarkPdfRequest("INBOUND_MARK_V1",
new PrintPdfClient.PageSpec(PAGE_MM, PAGE_MM), manifest.containerNo() + "-托盘码-" + suffix + ".pdf", labels);
try { return requirePdf(printClient.generateInboundMarks(internalToken, requestId, pdfRequest)); }
var pdfRequest = new PrintPdfClient.FbaPalletRequest("FBA_PALLET_A4_LANDSCAPE_V1",
new PrintPdfClient.PageSpec(new BigDecimal("297"), new BigDecimal("210")), manifest.containerNo() + "-托盘码-" + suffix + ".pdf", labels);
try { return requirePdf(printClient.generateFbaPalletLabels(internalToken, requestId, pdfRequest)); }
catch (FeignException | NoFallbackAvailableException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务暂不可用,请稍后重试"); }
}
@@ -524,6 +524,27 @@ class InboundMeasurementServiceTest {
verify(repository, never()).deleteWeight(anyLong(), anyLong(), anyLong(), anyInt());
}
@Test
void detailCountsSameCartonMultiSkuGroupOnlyOnceForDropship() {
InboundOrderEntity inbound = inbound("MEASURING");
inbound.setFulfillmentType("ONE_CLICK_DROPSHIP");
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
ApiResponse.success(List.of(
new OrderCargoClient.CargoLabelBinding(
80001L, 80L, 1, "SKU A", "SKU-A", 20, 5,
new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1"),
new OrderCargoClient.CargoLabelBinding(
80002L, 80L, 2, "SKU B", "SKU-B", 20, 3,
new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1"))));
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
assertThat(result.latestDeclaration().totalPackageCount()).isEqualTo(20);
assertThat(result.latestDeclaration().items()).hasSize(2);
}
@Test
void creatingWeightResultNormalizesProviderConfidenceAtBusinessBoundary() {
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
@@ -1215,6 +1236,44 @@ class InboundMeasurementServiceTest {
assertThat(inbound.getProcessStatus()).isEqualTo("MEASURE_PENDING");
}
@Test
void submitValidatesDropshipMultiSkuCartonsByPhysicalCartonCount() {
InboundOrderEntity inbound = inbound("MEASURING");
inbound.setFulfillmentType("ONE_CLICK_DROPSHIP");
inbound.setPalletCount(1);
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(30L);
measurement.setStatus("RECEIVED");
measurement.setVersion(6);
measurement.setTotalPackageCount(20);
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setPackageCount(20);
InboundWeightResultEntity weight = weight(20L, "197.000", "KG", "CONFIRMED", 1);
weight.setGrossWeightKg(new BigDecimal("197.000"));
weight.setNetWeightKg(new BigDecimal("181.000"));
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findMeasurement(1L, 10L, 30L)).thenReturn(measurement);
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
when(repository.updateMeasurement(measurement)).thenReturn(true);
when(repository.updateInbound(inbound)).thenReturn(true);
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
ApiResponse.success(List.of(
new OrderCargoClient.CargoLabelBinding(
80001L, 80L, 1, "SKU A", "SKU-A", 20, 5,
new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1"),
new OrderCargoClient.CargoLabelBinding(
80002L, 80L, 2, "SKU B", "SKU-B", 20, 3,
new BigDecimal("40"), new BigDecimal("30"), new BigDecimal("20"), null, "MIX-1"))));
service.submit(1L, 10L, 9L,
new InboundMeasurementRequests.SubmitRequest(30L, 6));
assertThat(inbound.getTotalPackageCount()).isEqualTo(20);
assertThat(inbound.getProcessStatus()).isEqualTo("MEASURE_PENDING");
assertThat(measurement.getStatus()).isEqualTo("SUBMITTED");
}
@Test
void submitRejectsWhenNoWeightImageHasBeenConfirmed() {
InboundOrderEntity inbound = inbound("MEASURING");