优化
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -4,6 +4,8 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.web.CorrelationContext;
|
||||
import com.baiyue.print.dto.FreightA4PdfDtos;
|
||||
import com.baiyue.print.dto.FbaPalletPdfDtos;
|
||||
import com.baiyue.print.service.FbaPalletPdfService;
|
||||
import com.baiyue.print.dto.DispatchBolPdfDtos;
|
||||
import com.baiyue.print.dto.DropshipPalletA4PdfDtos;
|
||||
import com.baiyue.print.dto.InboundMarkPdfDtos;
|
||||
@@ -36,8 +38,22 @@ public class InternalPdfController {
|
||||
@Resource private FreightA4PdfService freightA4PdfService;
|
||||
@Resource private DispatchBolPdfService dispatchBolPdfService;
|
||||
@Resource private DropshipPalletA4PdfService dropshipPalletA4PdfService;
|
||||
@Resource private FbaPalletPdfService fbaPalletPdfService;
|
||||
@Resource private InternalServiceTokenGuard tokenGuard;
|
||||
|
||||
@PostMapping(value = "/fba-pallet-labels", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<byte[]> generateFbaPalletLabels(
|
||||
@RequestHeader(value = INTERNAL_TOKEN_HEADER, required = false) String internalToken,
|
||||
@Valid @RequestBody FbaPalletPdfDtos.Request request) {
|
||||
tokenGuard.verify(internalToken);
|
||||
FbaPalletPdfService.GeneratedPdf pdf = fbaPalletPdfService.generate(request);
|
||||
byte[] content = pdf.content();
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore()).header(HttpHeaders.PRAGMA, "no-cache")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(pdf.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.header("X-Content-Type-Options", "nosniff").header(PAGE_COUNT_HEADER, Integer.toString(pdf.pageCount()))
|
||||
.contentType(MediaType.APPLICATION_PDF).contentLength(content.length).body(content);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/inbound-marks", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<byte[]> generateInboundMarks(
|
||||
@RequestHeader(value = INTERNAL_TOKEN_HEADER, required = false) String internalToken,
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
public class PrintRequestSizeFilter extends OncePerRequestFilter {
|
||||
private static final java.util.Set<String> PATHS = java.util.Set.of(
|
||||
"/internal/print/v1/pdfs/inbound-marks",
|
||||
"/internal/print/v1/pdfs/fba-pallet-labels",
|
||||
"/internal/print/v1/pdfs/freight-a4-documents");
|
||||
|
||||
@Resource private PrintPdfProperties properties;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.baiyue.print.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public final class FbaPalletPdfDtos {
|
||||
public static final String TEMPLATE = "FBA_PALLET_A4_LANDSCAPE_V1";
|
||||
private FbaPalletPdfDtos() {}
|
||||
|
||||
public record Request(String templateCode, PageSpec pageSpec, String downloadFileName, List<Label> labels) {}
|
||||
public record PageSpec(BigDecimal widthMm, BigDecimal heightMm) {}
|
||||
/** Barcode and visible pallet number always use the same authoritative code. */
|
||||
public record Label(String fbaWarehouseCode, String palletCode) {}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.print.renderer;
|
||||
|
||||
import com.baiyue.print.dto.FbaPalletPdfDtos;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Shared domestic/overseas FBA label: destination, Code 128, full pallet code. */
|
||||
@Component
|
||||
public class FbaPalletRenderer {
|
||||
@Resource private PdfBarcodeRenderer barcodeRenderer;
|
||||
|
||||
public void renderPage(PDDocument pdf, PDFont font, FbaPalletPdfDtos.Label label, BitMatrix barcode)
|
||||
throws IOException {
|
||||
PDPage page = new PDPage(new PDRectangle(PdfUnits.mmToPt(297), PdfUnits.mmToPt(210)));
|
||||
pdf.addPage(page);
|
||||
PdfUnits units = new PdfUnits(210);
|
||||
try (PDPageContentStream stream = new PDPageContentStream(pdf, page)) {
|
||||
stream.setNonStrokingColor(0, 0, 0);
|
||||
centered(stream, units, font, label.fbaWarehouseCode(), 96, 49);
|
||||
barcodeRenderer.draw(stream, barcode, PdfUnits.mmToPt(33.5), units.topDownMmToPdfY(140),
|
||||
PdfUnits.mmToPt(230), PdfUnits.mmToPt(55));
|
||||
centered(stream, units, font, label.palletCode(), 64, 174);
|
||||
}
|
||||
}
|
||||
|
||||
private void centered(PDPageContentStream stream, PdfUnits units, PDFont font, String value,
|
||||
float preferredSize, double centerY) throws IOException {
|
||||
float unitWidth = font.getStringWidth(value) / 1000;
|
||||
float size = Math.min(preferredSize, PdfUnits.mmToPt(267) / unitWidth);
|
||||
stream.beginText();
|
||||
stream.setFont(font, size);
|
||||
stream.newLineAtOffset((PdfUnits.mmToPt(297) - unitWidth * size) / 2,
|
||||
units.centeredTextBaseline(font, size, centerY));
|
||||
stream.showText(value);
|
||||
stream.endText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.print.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.print.config.PrintPdfProperties;
|
||||
import com.baiyue.print.dto.FbaPalletPdfDtos;
|
||||
import com.baiyue.print.renderer.FbaPalletRenderer;
|
||||
import com.baiyue.print.renderer.PdfBarcodeRenderer;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Locale;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class FbaPalletPdfService {
|
||||
@Resource private PrintPdfProperties properties;
|
||||
@Resource private PdfGenerationLimiter limiter;
|
||||
@Resource private FbaPalletRenderer renderer;
|
||||
@Resource private PdfBarcodeRenderer barcodeRenderer;
|
||||
|
||||
public GeneratedPdf generate(FbaPalletPdfDtos.Request request) {
|
||||
validate(request);
|
||||
try (PdfGenerationLimiter.Permit ignored = limiter.acquire(); PDDocument pdf = new PDDocument();
|
||||
InputStream fontStream = new ClassPathResource(properties.getFontResource()).getInputStream();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
PDType0Font font = PDType0Font.load(pdf, fontStream, true);
|
||||
for (var label : request.labels()) {
|
||||
renderer.renderPage(pdf, font, label, barcodeRenderer.encode(label.palletCode()));
|
||||
}
|
||||
pdf.save(output);
|
||||
byte[] content = output.toByteArray();
|
||||
if (content.length > properties.getMaxOutputBytes()) invalid("生成的 PDF 超过输出大小上限");
|
||||
return new GeneratedPdf(content, request.downloadFileName(), request.labels().size());
|
||||
} catch (IOException exception) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "PDF 生成失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(FbaPalletPdfDtos.Request request) {
|
||||
if (request == null || !FbaPalletPdfDtos.TEMPLATE.equals(request.templateCode())) invalid("templateCode is not supported");
|
||||
if (request.pageSpec() == null || request.pageSpec().widthMm() == null || request.pageSpec().heightMm() == null
|
||||
|| request.pageSpec().widthMm().compareTo(new BigDecimal("297")) != 0
|
||||
|| request.pageSpec().heightMm().compareTo(new BigDecimal("210")) != 0) invalid("pageSpec must be landscape A4 (297 x 210 mm)");
|
||||
String fileName = request.downloadFileName();
|
||||
if (fileName == null || fileName.isBlank() || fileName.length() > 120
|
||||
|| !fileName.toLowerCase(Locale.ROOT).endsWith(".pdf")
|
||||
|| fileName.matches(".*[\\\\/:*?\"<>|].*")
|
||||
|| fileName.codePoints().anyMatch(Character::isISOControl)) invalid("downloadFileName is invalid");
|
||||
if (request.labels() == null || request.labels().isEmpty() || request.labels().size() > properties.getMaxPages()) invalid("labels count is invalid");
|
||||
for (var label : request.labels()) {
|
||||
if (label == null) invalid("label is required");
|
||||
// Reject malformed identifiers instead of trimming, truncating or substituting scan identities.
|
||||
if (label.fbaWarehouseCode() == null || !label.fbaWarehouseCode().matches("[A-Z0-9][A-Z0-9-]{0,31}")) invalid("fbaWarehouseCode must be an FBA warehouse code such as SCK8");
|
||||
if (label.palletCode() == null || label.palletCode().isBlank()
|
||||
|| !label.palletCode().equals(label.palletCode().strip())
|
||||
|| !label.palletCode().matches("[\\x20-\\x7E]{1,80}")) invalid("palletCode must contain 1-80 printable ASCII characters without surrounding spaces");
|
||||
}
|
||||
}
|
||||
|
||||
private void invalid(String message) { throw new BizException(ResultCode.VALIDATION_FAILED, message); }
|
||||
public record GeneratedPdf(byte[] content, String fileName, int pageCount) {}
|
||||
}
|
||||
+9
@@ -12,6 +12,15 @@ import org.springframework.web.bind.annotation.RequestHeader;
|
||||
@FeignClient(name = "print-service", contextId = "overseasWarehousePrintPdfClient",
|
||||
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,
|
||||
|
||||
+12
-5
@@ -27,19 +27,26 @@ public class PalletLabelPrintService {
|
||||
public ResponseEntity<byte[]> print(long warehouseId, long palletId, String requestId) {
|
||||
var pallet = resources.pallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE, warehouseId, palletId);
|
||||
String code = pallet.getPalletCode();
|
||||
// Keep the stored business code intact, including the QR payload.
|
||||
if (code == null || !code.matches("[\\x20-\\x7E]{1,34}") || code.isBlank()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "当前标签模板要求托盘编码为 1–34 位可打印 ASCII 字符,请核对托盘编码");
|
||||
String fbaCode = pallet.getFbaWarehouseCode();
|
||||
boolean fba = fbaCode != null && !fbaCode.isBlank();
|
||||
int maxCodeLength = fba ? 80 : 34;
|
||||
// Preserve the authoritative pallet identity on both the barcode and visible text.
|
||||
if (code == null || !code.matches("[\\x20-\\x7E]{1," + maxCodeLength + "}") || code.isBlank()
|
||||
|| !code.equals(code.strip())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "当前标签模板要求托盘编码为 1–" + maxCodeLength + " 位可打印 ASCII 字符,且无首尾空格,请核对托盘编码");
|
||||
}
|
||||
var warehouse = warehouses.required(warehouseId);
|
||||
String fbaCode = pallet.getFbaWarehouseCode();
|
||||
String fileName = code.replaceAll("[\\\\/:*?\"<>|]", "-") + "-托盘码.pdf";
|
||||
var label = new PrintPdfClient.Label(code, fbaCode == null || fbaCode.isBlank() ? "—" : fbaCode,
|
||||
"PALLET", warehouse.warehouseName(), code);
|
||||
var request = new PrintPdfClient.Request("INBOUND_MARK_V1",
|
||||
new PrintPdfClient.PageSpec(new BigDecimal("100"), new BigDecimal("100")), fileName, List.of(label));
|
||||
try {
|
||||
var response = print.generateInboundMarks(internalToken, requestId, request);
|
||||
var response = fba
|
||||
? print.generateFbaPalletLabels(internalToken, requestId, new PrintPdfClient.FbaPalletRequest(
|
||||
"FBA_PALLET_A4_LANDSCAPE_V1", new PrintPdfClient.PageSpec(new BigDecimal("297"), new BigDecimal("210")),
|
||||
fileName, List.of(new PrintPdfClient.FbaPalletLabel(fbaCode, code))))
|
||||
: print.generateInboundMarks(internalToken, requestId, request);
|
||||
byte[] body = response == null ? null : response.getBody();
|
||||
MediaType type = response == null ? null : response.getHeaders().getContentType();
|
||||
if (response == null || !response.getStatusCode().is2xxSuccessful() || body == null || body.length == 0
|
||||
|
||||
Reference in New Issue
Block a user