feat: finalize dropship pallet print documents

This commit is contained in:
2026-09-14 17:52:01 +08:00
parent b3d74c5441
commit 3910b8fcce
15 changed files with 34 additions and 50 deletions
@@ -55,7 +55,6 @@ public interface PrintPdfClient {
String downloadFileName, List<DropshipPalletDocument> documents) {}
record DropshipPalletDocument(String palletNo, String destinationWarehouse,
int palletSequence, int totalPallets,
String barcodeContent,
List<DropshipPalletInboundRow> inboundRows,
List<DropshipPalletSkuRow> skuRows) {}
@@ -150,6 +150,17 @@ public class PlatformDropshipPalletizationController {
return ApiResponse.success(service.inboundPallets(accountId, inboundId));
}
@GetMapping("/inbound-orders/{inboundId}/pallets/print")
@PreAuthorize("hasAuthority('platform:firstleg:inbound:list')")
public ResponseEntity<byte[]> printInboundPallets(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long inboundId,
@RequestParam @Positive long accountId,
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
FirstlegScopeGuard.platform(user, accountId);
return printService.generateDropshipPallets(
service.inboundPallets(accountId, inboundId), service.inboundCode(accountId, inboundId), requestId);
}
@GetMapping("/inbound-orders/{inboundId}/pallets/{palletId}/print")
@PreAuthorize("hasAuthority('platform:firstleg:inbound:list')")
public ResponseEntity<byte[]> printPallet(@AuthenticationPrincipal AuthenticatedUser user,
@@ -254,15 +254,6 @@ public interface DropshipPalletizationMapper {
""")
List<PrintSkuRow> printSkuRows(@Param("palletId") long palletId);
@Select("""
SELECT (SELECT COUNT(*) FROM fl_dropship_pallet listed
WHERE listed.task_id=target.task_id AND listed.id<=target.id) sequence_no,
(SELECT COUNT(*) FROM fl_dropship_pallet listed
WHERE listed.task_id=target.task_id) total
FROM fl_dropship_pallet target
WHERE target.id=#{palletId}
""")
PalletPosition palletPosition(@Param("palletId") long palletId);
@Select("""
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(DISTINCT c.id) carton_count,
(SELECT COUNT(DISTINCT sku.sku_code) FROM fl_dropship_pallet_carton pc
@@ -306,6 +297,5 @@ public interface DropshipPalletizationMapper {
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
@Getter @Setter class PrintInboundRow { private Long tenantId,inboundId; private String inboundCode,destinationWarehouse; private Integer cartonCount,itemQuantity; }
@Getter @Setter class PrintSkuRow { private String skuCode; private Integer cartonCount,itemQuantity,minUnitsPerCarton,maxUnitsPerCarton; }
record PalletPosition(int sequenceNo,int total) {}
@Getter @Setter class PalletLine { private Long palletId,tenantId,accountId,inboundId,orderId,warehouseId,destinationWarehouseId,inboundDetailId; private String palletNo,palletStatus,orderNo,inboundCode,channelCode,channelName,destinationWarehouseCode,destinationWarehouseName,customerName,skuCode,cargoName; private Integer cartonCount,actualSkuQuantity; private BigDecimal unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm; }
}
@@ -172,12 +172,8 @@ public class InboundCartonLabelPrintService {
.stream().map(row -> new PrintPdfClient.DropshipPalletSkuRow(
row.getSkuCode(), number(row.getCartonCount()), number(row.getItemQuantity()),
unitsPerCarton(row))).toList();
DropshipPalletizationMapper.PalletPosition position =
palletizationMapper.palletPosition(pallet.palletId());
return new PrintPdfClient.DropshipPalletDocument(
pallet.palletNo(), destinationWarehouse,
position == null ? 1 : position.sequenceNo(), position == null ? 1 : position.total(),
pallet.palletNo(), inboundRows, skuRows);
pallet.palletNo(), destinationWarehouse, pallet.palletNo(), inboundRows, skuRows);
}
private String unitsPerCarton(DropshipPalletizationMapper.PrintSkuRow row) {
@@ -68,7 +68,7 @@ paths:
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/print:
get:
summary: PDA 获取预生成托盘码的 A4 托盘信息单
description: 测量审核通过并生成托盘码后即可获取;每个托盘固定一页 A4,包含托盘号及其 Code 128 一维码、目的仓、托盘序号、入仓码/客户昵称/箱数/件数汇总以及 SKU/箱数/件数/每箱件数汇总,不展示作业状态、重量或体积。空托显示暂无已归托 SKU。仅当前仓库、当前操作员可访问。
description: 测量审核通过并生成托盘码后即可获取;每个托盘固定一页 A4,包含托盘号及其 Code 128 一维码、目的仓、入仓码/客户昵称/箱数/件数汇总以及 SKU/箱数/件数/每箱件数汇总,不展示托盘序号、作业状态、重量或体积。空托显示暂无已归托 SKU。仅当前仓库、当前操作员可访问。
x-required-authority: platform:firstleg:pda:view
parameters:
- {$ref: '#/components/parameters/TaskId'}
@@ -89,6 +89,13 @@ paths:
summary: Admin 查询入仓单权威托盘结果
x-required-authority: platform:firstleg:inbound:list
responses: {'200': {description: OK}}
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets/print:
get:
summary: Admin 合并打印入仓单关联的全部一件代发托盘
description: 测量审核预生成托盘码后即可打印;每托一页,空托也保留托盘码和当前入仓单信息,供电脑连接打印机后在 PDA 作业前贴码。
x-required-authority: platform:firstleg:inbound:list
responses:
'200': {description: 多页 A4 托盘信息单 PDF, content: {application/pdf: {schema: {type: string, format: binary}}}}
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets/{palletId}/print:
get:
summary: 获取包含托盘码、目的仓、入仓码和 SKU 汇总的 A4 托盘信息单
@@ -16,7 +16,8 @@ class DropshipPalletizationSqlContractTest {
void inboundPalletReadAndPrintUseTheRegisteredPlatformInboundPermission() throws Exception {
var controller = com.baiyue.firstleg.controller.PlatformDropshipPalletizationController.class;
for (var method : controller.getDeclaredMethods()) {
if (method.getName().equals("inboundPallets") || method.getName().equals("printPallet")) {
if (method.getName().equals("inboundPallets") || method.getName().equals("printPallet")
|| method.getName().equals("printInboundPallets")) {
assertThat(method.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class).value())
.isEqualTo("hasAuthority('platform:firstleg:inbound:list')");
}
@@ -24,6 +25,7 @@ class DropshipPalletizationSqlContractTest {
String api = new ClassPathResource("openapi/dropship-palletization-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(api).contains("x-required-authority: platform:firstleg:inbound:list")
.contains("/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets/print:")
.doesNotContain("x-required-authority: firstleg:inbound:list");
}
@@ -45,7 +45,7 @@ class InboundCartonLabelPrintServiceTest {
@Test
void dropshipPalletLabelIsAvailableBeforeSeal() {
Pallet open = dropshipPallet("OPEN");
stubPalletFacts(90001L, 1, 2);
stubPalletFacts(90001L);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(),
@@ -66,8 +66,6 @@ class InboundCartonLabelPrintServiceTest {
assertThat(request.getValue().documents()).singleElement().satisfies(document -> {
assertThat(document.palletNo()).isEqualTo("DP-260911-0001");
assertThat(document.destinationWarehouse()).isEqualTo("NJDF");
assertThat(document.palletSequence()).isEqualTo(1);
assertThat(document.totalPallets()).isEqualTo(2);
assertThat(document.inboundRows()).singleElement().satisfies(row -> {
assertThat(row.inboundCode()).isEqualTo("PCHK2608040174");
assertThat(row.customerNickname()).isEqualTo("小虫科技");
@@ -88,8 +86,8 @@ class InboundCartonLabelPrintServiceTest {
Pallet first = dropshipPallet("OPEN");
Pallet second = new Pallet(90002L, "DP-260911-0002", "OPEN", 0, 0, 0,
BigDecimal.ZERO, BigDecimal.ZERO, 1, List.of(), List.of(), null);
stubPalletFacts(90001L, 1, 2);
stubPalletFacts(90002L, 2, 2);
stubPalletFacts(90001L);
stubPalletFacts(90002L);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
when(print.generateDropshipPalletA4Documents(org.mockito.ArgumentMatchers.anyString(),
@@ -242,7 +240,7 @@ class InboundCartonLabelPrintServiceTest {
BigDecimal.ONE, BigDecimal.ONE, 1, List.of(item), List.of(), null);
}
private void stubPalletFacts(long palletId, int sequence, int total) {
private void stubPalletFacts(long palletId) {
DropshipPalletizationMapper.PrintInboundRow inbound =
new DropshipPalletizationMapper.PrintInboundRow();
inbound.setTenantId(10001L);
@@ -260,8 +258,6 @@ class InboundCartonLabelPrintServiceTest {
sku.setMaxUnitsPerCarton(19);
when(palletization.printSkuRows(palletId))
.thenReturn(palletId == 90001L ? List.of(sku) : List.of());
when(palletization.palletPosition(palletId))
.thenReturn(new DropshipPalletizationMapper.PalletPosition(sequence, total));
when(customerSnapshots.customerNickname(10001L)).thenReturn("小虫科技");
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
- 服务发现名:`print-service`。
- 内部地址:`POST /internal/print/v1/pdfs/inbound-marks`。
- 一件代发 A4 托盘信息单:`POST /internal/print/v1/pdfs/dropship-pallet-a4-documents`;模板固定为 `DROPSHIP_PALLET_A4_V2`,采用黑白打印版,每托一页 A4,包含托盘 Code 128 一维码、目的仓、托盘序号、最多 10 行入仓码/客户昵称汇总和最多 34 行 SKU 汇总,不展示作业状态、重量或体积。
- 一件代发 A4 托盘信息单:`POST /internal/print/v1/pdfs/dropship-pallet-a4-documents`;模板固定为 `DROPSHIP_PALLET_A4_V2`,采用黑白打印版,每托一页 A4,包含托盘 Code 128 一维码、目的仓、最多 10 行入仓码/客户昵称汇总和最多 34 行 SKU 汇总,不展示托盘序号、作业状态、重量或体积。
- A4 海运单据内部地址:`POST /internal/print/v1/pdfs/freight-a4-documents`。
- 海外仓排车 BOL 内部地址:`POST /internal/print/v1/pdfs/dispatch-bol-documents`;模板固定为 `DISPATCH_BILL_OF_LADING_A4_V2`,每个 document 对应一张 A4、包含 1-26 行明细。调用方首张按 18 行、续页按 26 行稳定分页;同页连续相同柜号和托盘号由 Print 纵向合并显示。
- 本地直连:`http://127.0.0.1:9207`。
Binary file not shown.
@@ -15,7 +15,6 @@ public final class DropshipPalletA4PdfDtos {
public record PageSpec(BigDecimal widthMm, BigDecimal heightMm) {}
public record Document(String palletNo, String destinationWarehouse,
int palletSequence, int totalPallets,
String barcodeContent, List<InboundRow> inboundRows,
List<SkuRow> skuRows) {}
@@ -35,9 +35,6 @@ public class DropshipPalletA4Renderer {
stream.setLineWidth(PdfUnits.mmToPt(.25));
border(stream, units);
colorText(stream, units, font, "一件代发备货托", 14, 18, 22, INK);
rightText(stream, units, font,
"第 " + data.palletSequence() + " 托 / 共 " + data.totalPallets() + " 托",
10, 192, 22, new Color(75, 75, 75));
barcodeRenderer.draw(stream, barcodeMatrix, PdfUnits.mmToPt(18), units.topDownMmToPdfY(51),
PdfUnits.mmToPt(80), PdfUnits.mmToPt(22));
@@ -157,12 +154,6 @@ public class DropshipPalletA4Renderer {
colorText(s, u, f, fit(f, value, size, PdfUnits.mmToPt(widthMm)), size, x, y, color);
}
private void rightText(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
double right, double y, Color color) throws IOException {
double widthMm = f.getStringWidth(value) / 1000 * size / PdfUnits.mmToPt(1);
colorText(s, u, f, value, size, right - widthMm, y, color);
}
private void centeredCell(PDPageContentStream s, PdfUnits u, PDFont f, String value, float size,
double x1, double x2, double y) throws IOException {
String fitted = fit(f, value, size, PdfUnits.mmToPt(x2 - x1));
@@ -68,8 +68,6 @@ public class DropshipPalletA4PdfService {
private DropshipPalletA4PdfDtos.Document normalizeDocument(DropshipPalletA4PdfDtos.Document value, int index) {
if (value == null) invalid("documents[" + index + "] is required");
if (value.palletSequence() <= 0 || value.totalPallets() <= 0
|| value.palletSequence() > value.totalPallets()) invalid("documents[" + index + "] pallet position is invalid");
if (value.inboundRows() == null || value.inboundRows().isEmpty()
|| value.inboundRows().size() > MAX_INBOUND_ROWS) invalid("documents[" + index + "].inboundRows exceeds the one-page A4 capacity");
if (value.skuRows() == null || value.skuRows().size() > MAX_SKU_ROWS) invalid("documents[" + index + "].skuRows exceeds the one-page A4 capacity");
@@ -86,8 +84,8 @@ public class DropshipPalletA4PdfService {
row.itemQuantity(), text(row.unitsPerCarton(), 30)));
}
return new DropshipPalletA4PdfDtos.Document(text(value.palletNo(), 80),
text(value.destinationWarehouse(), 200), value.palletSequence(), value.totalPallets(),
barcode(value.barcodeContent()), List.copyOf(inboundRows), List.copyOf(rows));
text(value.destinationWarehouse(), 200), barcode(value.barcodeContent()),
List.copyOf(inboundRows), List.copyOf(rows));
}
private String barcode(String value) {
@@ -290,12 +290,10 @@ components:
items: { $ref: '#/components/schemas/DropshipPalletA4Document' }
DropshipPalletA4Document:
type: object
required: [palletNo, destinationWarehouse, palletSequence, totalPallets, barcodeContent, inboundRows, skuRows]
required: [palletNo, destinationWarehouse, barcodeContent, inboundRows, skuRows]
properties:
palletNo: { type: string, maxLength: 80 }
destinationWarehouse: { type: string, maxLength: 200 }
palletSequence: { type: integer, minimum: 1 }
totalPallets: { type: integer, minimum: 1 }
barcodeContent:
type: string
maxLength: 80
@@ -274,8 +274,6 @@ class InternalPdfControllerTest {
"documents": [{
"palletNo": "DP-260911-0034",
"destinationWarehouse": "NJDF",
"palletSequence": 1,
"totalPallets": 3,
"barcodeContent": "DP-260911-0034",
"inboundRows": [{
"inboundCode": "NYEXX2609110249",
@@ -27,7 +27,7 @@ class DropshipPalletA4PdfServiceGenerationTest {
@Test
void generatesOneA4PageWithPalletBarcodeAndSkuDetails() throws Exception {
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
"DP-260911-0034", "NJDF", 1, 3, "DP-260911-0034",
"DP-260911-0034", "NJDF", "DP-260911-0034",
List.of(new DropshipPalletA4PdfDtos.InboundRow(
"NYEXX2609110249", "小虫科技", 3, 90)),
List.of(
@@ -64,8 +64,7 @@ class DropshipPalletA4PdfServiceGenerationTest {
"SKU-" + String.format("%03d", index), 2, index * 5, "5-10"));
}
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
"DP-260911-0034", "NJDF", 1, 1,
"DP-260911-0034", inboundRows, rows);
"DP-260911-0034", "NJDF", "DP-260911-0034", inboundRows, rows);
var generated = service.generate(new DropshipPalletA4PdfDtos.Request(
DropshipPalletA4PdfDtos.TEMPLATE,
new DropshipPalletA4PdfDtos.PageSpec(new BigDecimal("210"), new BigDecimal("297")),
@@ -85,7 +84,7 @@ class DropshipPalletA4PdfServiceGenerationTest {
rows.add(new DropshipPalletA4PdfDtos.SkuRow("SKU-" + index, 1, 1, "1"));
}
DropshipPalletA4PdfDtos.Document document = new DropshipPalletA4PdfDtos.Document(
"DP-260911-0034", "NJDF", 1, 1, "DP-260911-0034",
"DP-260911-0034", "NJDF", "DP-260911-0034",
List.of(new DropshipPalletA4PdfDtos.InboundRow("INBOUND", "客户", 35, 35)), rows);
assertThatThrownBy(() -> service.generate(new DropshipPalletA4PdfDtos.Request(