fix(print): remove SKU from carton labels while retaining dropship A4 details

This commit is contained in:
2026-09-20 11:22:29 +08:00
parent 78a36d1a39
commit bbf000a38a
11 changed files with 16 additions and 29 deletions
+1 -1
View File
@@ -19,7 +19,7 @@
| 入库单扣货、履约门禁与凭证 | `service/InboundHoldService.java`、`InboundHoldGuard.java`、`InboundHoldEvidenceService.java`、`client/InboundHoldFileClient.java`;凭证固定按客户、入库单和 `INBOUND_HOLD_EVIDENCE` 精确归属,只有进入不可变扣货历史的文件可下载 | | 入库单扣货、履约门禁与凭证 | `service/InboundHoldService.java`、`InboundHoldGuard.java`、`InboundHoldEvidenceService.java`、`client/InboundHoldFileClient.java`;凭证固定按客户、入库单和 `INBOUND_HOLD_EVIDENCE` 精确归属,只有进入不可变扣货历史的文件可下载 |
| Order 申报同步与渠道变更履约门禁 | `controller/InternalInboundOrderController.java` 的 `/order-channel-changes/validate`、`/order-declaration-updated`,`service/OrderDeclarationSyncService.java`、`mapper/OrderDeclarationSyncMapper.java`、`dto/OrderDeclarationSync.java`;契约 `src/main/resources/openapi/firstleg-v1.yaml`,只接受内部服务令牌 | | Order 申报同步与渠道变更履约门禁 | `controller/InternalInboundOrderController.java` 的 `/order-channel-changes/validate`、`/order-declaration-updated`,`service/OrderDeclarationSyncService.java`、`mapper/OrderDeclarationSyncMapper.java`、`dto/OrderDeclarationSync.java`;契约 `src/main/resources/openapi/firstleg-v1.yaml`,只接受内部服务令牌 |
| 平台测量/OCR 与箱规计费快照 | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java`、`service/CustomerDimensionWeightingService.java`;手工单箱测量保存实际尺寸、可选外箱修正、`chargeRuleId` 与 `coefficientMode`,计算结果以服务端返回值为准 | | 平台测量/OCR 与箱规计费快照 | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java`、`service/CustomerDimensionWeightingService.java`;手工单箱测量保存实际尺寸、可选外箱修正、`chargeRuleId` 与 `coefficientMode`,计算结果以服务端返回值为准 |
| 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`。一件代发箱唛以 `fl_inbound_carton_label` 表示物理箱,`fl_inbound_carton_label_sku` 冻结箱内一条或多条 SKU;生成时经 `client/OrderCargoClient.java` 获取。缺 SKU 时禁止生成,打印面显示箱内 SKU;`box_code` 与二维码内容仍只携带稳定箱码。基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql`、`../deploy/sql/头程数据库统一增量-MySQL8.sql` | | 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`。一件代发箱唛以 `fl_inbound_carton_label` 表示物理箱,`fl_inbound_carton_label_sku` 冻结箱内一条或多条 SKU;生成时经 `client/OrderCargoClient.java` 获取。缺 SKU 时禁止生成;所有头程箱唛打印面均不显示 SKU,SKU 只保留在业务快照及一件代发 A4 托盘信息单;`box_code` 与二维码内容仍只携带稳定箱码。基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql`、`../deploy/sql/头程数据库统一增量-MySQL8.sql` |
| 一件代发测量后 PDA 清点组托 | `controller/PlatformDropshipPalletizationController.java`、`service/DropshipPalletizationService.java`、`mapper/DropshipPalletizationMapper.java`;测量审核通过进入 `WAIT_PALLETIZE`,按预计托数预生成 `DP` 托盘码并下发独立任务,不创建国内库存;托盘信息单固定为每托一页 A4,包含托盘 Code 128 一维码、入仓码、总箱数与 SKU/商品名称/箱数/实点数量,不展示作业状态。PDA 逐箱清点,支持先扫箱或先扫托、归托、逐托封托和在未进入装柜计划前解除封托;合托解封时同步恢复所有参与任务及入仓单。预计托数与已生成托盘数不一致时禁止作业,全部箱和实际参与托盘完成后进入 `WAIT_SPLIT`。契约 `src/main/resources/openapi/dropship-palletization-v1.yaml` | | 一件代发测量后 PDA 清点组托 | `controller/PlatformDropshipPalletizationController.java`、`service/DropshipPalletizationService.java`、`mapper/DropshipPalletizationMapper.java`;测量审核通过进入 `WAIT_PALLETIZE`,按预计托数预生成 `DP` 托盘码并下发独立任务,不创建国内库存;托盘信息单固定为每托一页 A4,包含托盘 Code 128 一维码、入仓码、总箱数与 SKU/商品名称/箱数/实点数量,不展示作业状态。PDA 逐箱清点,支持先扫箱或先扫托、归托、逐托封托和在未进入装柜计划前解除封托;合托解封时同步恢复所有参与任务及入仓单。预计托数与已生成托盘数不一致时禁止作业,全部箱和实际参与托盘完成后进入 `WAIT_SPLIT`。契约 `src/main/resources/openapi/dropship-palletization-v1.yaml` |
| 墨家设备测量上传 | `controller/OpenApiInboundMeasurementController.java`、`dto/MojiaMeasurementDtos.java`、`service/MojiaMeasurementOpenApiService.java`、`security/MeasurementRequestAuthenticator.java`;接口契约见 `src/main/resources/openapi/firstleg-v1.yaml` 与 `../../docs/墨家设备测量上传接口说明-V3.md` | | 墨家设备测量上传 | `controller/OpenApiInboundMeasurementController.java`、`dto/MojiaMeasurementDtos.java`、`service/MojiaMeasurementOpenApiService.java`、`security/MeasurementRequestAuthenticator.java`;接口契约见 `src/main/resources/openapi/firstleg-v1.yaml` 与 `../../docs/墨家设备测量上传接口说明-V3.md` |
| 称重确认与整票库位块 | `service/MeasurementLocationOccupationService.java` 的 `ensureForConfirmedWeight/ensureForSubmission`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`client/WarehousePalletLocationClient.java`;Warehouse 块服务见 `../warehouse/src/main/java/com/baiyue/warehouse/service/LocationBlockService.java`,接口与上架引导见 `src/main/resources/openapi/firstleg-v1.yaml` | | 称重确认与整票库位块 | `service/MeasurementLocationOccupationService.java` 的 `ensureForConfirmedWeight/ensureForSubmission`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`client/WarehousePalletLocationClient.java`;Warehouse 块服务见 `../warehouse/src/main/java/com/baiyue/warehouse/service/LocationBlockService.java`,接口与上架引导见 `src/main/resources/openapi/firstleg-v1.yaml` |
@@ -49,7 +49,7 @@ public interface PrintPdfClient {
} }
record Label(String topCode, String warehouseText, String cartonText, record Label(String topCode, String warehouseText, String cartonText,
String routeText, String qrContent, String skuCode) { String routeText, String qrContent) {
} }
record FreightA4PdfRequest(String templateCode, PageSpec pageSpec, record FreightA4PdfRequest(String templateCode, PageSpec pageSpec,
@@ -200,7 +200,7 @@ public class InboundCartonLabelPrintService {
private PrintPdfClient.Label toPrintLabel(InboundCartonLabels.Label label) { private PrintPdfClient.Label toPrintLabel(InboundCartonLabels.Label label) {
return new PrintPdfClient.Label(label.topCode(), label.warehouseText(), label.cartonText(), return new PrintPdfClient.Label(label.topCode(), label.warehouseText(), label.cartonText(),
label.routeText(), label.boxCode(), label.skuCode()); label.routeText(), label.boxCode());
} }
private ResponseEntity<byte[]> requirePdf(ResponseEntity<byte[]> response) { private ResponseEntity<byte[]> requirePdf(ResponseEntity<byte[]> response) {
@@ -109,7 +109,7 @@ class InboundCartonLabelPrintServiceTest {
} }
@Test @Test
void forwardsStableLabelsAndOnlySafePdfHeaders() { void forwardsStableLabelsWithoutSkuAndOnlySafePdfHeaders() throws Exception {
when(labels.list(10001L, 70001L, List.of(1))).thenReturn(batch()); when(labels.list(10001L, 70001L, List.of(1))).thenReturn(batch());
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF); headers.setContentType(MediaType.APPLICATION_PDF);
@@ -132,6 +132,9 @@ class InboundCartonLabelPrintServiceTest {
assertThat(label.qrContent()).isEqualTo("PCHK2608040174#19-1"); assertThat(label.qrContent()).isEqualTo("PCHK2608040174#19-1");
assertThat(label.cartonText()).isEqualTo("CTNS:19-1"); assertThat(label.cartonText()).isEqualTo("CTNS:19-1");
}); });
assertThat(batch().labels().get(0).skuCode()).isEqualTo("SKU-BOX-ONLY-IN-BUSINESS-DATA");
assertThat(new ObjectMapper().writeValueAsString(request.getValue()))
.doesNotContain("skuCode", "SKU-BOX-ONLY-IN-BUSINESS-DATA");
assertThat(response.getBody()).containsExactly(1, 2, 3); assertThat(response.getBody()).containsExactly(1, 2, 3);
assertThat(response.getHeaders().getFirst("X-Print-Page-Count")).isEqualTo("1"); assertThat(response.getHeaders().getFirst("X-Print-Page-Count")).isEqualTo("1");
assertThat(response.getHeaders()).doesNotContainKey("X-Internal-Service-Token"); assertThat(response.getHeaders()).doesNotContainKey("X-Internal-Service-Token");
@@ -229,6 +232,7 @@ class InboundCartonLabelPrintServiceTest {
InboundCartonLabels.Label label = new InboundCartonLabels.Label( InboundCartonLabels.Label label = new InboundCartonLabels.Label(
1L, 2L, 1, "音箱", 19, "PCHK2608040174#19-1", 19, 1, 1L, 2L, 1, "音箱", 19, "PCHK2608040174#19-1", 19, 1,
"50x40x30cm", new BigDecimal("50"), new BigDecimal("40"), new BigDecimal("30"), "50x40x30cm", new BigDecimal("50"), new BigDecimal("40"), new BigDecimal("30"),
"SKU-BOX-ONLY-IN-BUSINESS-DATA",
"PCHK2608040174", "MCC1", "CTNS:19-1", "洛杉矶普船海卡"); "PCHK2608040174", "MCC1", "CTNS:19-1", "洛杉矶普船海卡");
return new InboundCartonLabels.Batch(70001L, 80001L, "PCHK2608040174", 19, List.of(label)); return new InboundCartonLabels.Batch(70001L, 80001L, "PCHK2608040174", 19, List.of(label));
} }
+1
View File
@@ -4,6 +4,7 @@
- 本模块是无数据库的无状态 PDF 渲染服务;调用方传入完整 `labels[]` 或 `documents[]`,模块在内存中生成一次性多页 PDF 并同步返回。 - 本模块是无数据库的无状态 PDF 渲染服务;调用方传入完整 `labels[]` 或 `documents[]`,模块在内存中生成一次性多页 PDF 并同步返回。
- 不保存模板、任务、历史或文件,不连接数据库、File 服务或打印机,不反查或修正业务数据。 - 不保存模板、任务、历史或文件,不连接数据库、File 服务或打印机,不反查或修正业务数据。
- `INBOUND_MARK_V1` 箱唛不接收或展示 SKU(包括一件代发箱唛),二维码仅编码稳定箱码;SKU 明细由一件代发 A4 托盘信息单展示,不能重新加入箱唛版面。
- 当前支持 `INBOUND_MARK_V1`(100×100 mm)、`FBA_PALLET_A4_LANDSCAPE_V1`(297×210 mm),以及纵向 A4 的 `DROPSHIP_PALLET_A4_V2`、`SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1`、`BILL_OF_LADING_CONFIRMATION_A4_V1`、`DISPATCH_BILL_OF_LADING_A4_V2`。FBA 模板由国内和海外统一使用,每托一页,只展示 SCK8 等 FBA 仓码、完整托盘号的 Code 128 条码和完整托盘号;141 等内部编号保留在托盘号中。一件代发每托一页,展示托盘 Code 128、目的仓、最多 10 行入仓码/客户昵称和最多 34 行 SKU,不展示状态、重量或体积。新尺寸或布局必须使用新模板编码。 - 当前支持 `INBOUND_MARK_V1`(100×100 mm)、`FBA_PALLET_A4_LANDSCAPE_V1`(297×210 mm),以及纵向 A4 的 `DROPSHIP_PALLET_A4_V2`、`SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1`、`BILL_OF_LADING_CONFIRMATION_A4_V1`、`DISPATCH_BILL_OF_LADING_A4_V2`。FBA 模板由国内和海外统一使用,每托一页,只展示 SCK8 等 FBA 仓码、完整托盘号的 Code 128 条码和完整托盘号;141 等内部编号保留在托盘号中。一件代发每托一页,展示托盘 Code 128、目的仓、最多 10 行入仓码/客户昵称和最多 34 行 SKU,不展示状态、重量或体积。新尺寸或布局必须使用新模板编码。
## 快速索引 ## 快速索引
+2
View File
@@ -38,6 +38,8 @@ Firstleg 从冻结标签的 `destinationWarehouseCode/palletCode` 映射;海
### 2.1 入仓标签 ### 2.1 入仓标签
所有头程箱唛(包括一件代发订单的箱唛)均不打印 SKU,标签请求不包含 `skuCode`。箱内 SKU 快照由业务服务保留用于清点、组托;一件代发 A4 托盘信息单继续展示 SKU 明细。二维码只使用稳定箱码。
```http ```http
POST /internal/print/v1/pdfs/inbound-marks HTTP/1.1 POST /internal/print/v1/pdfs/inbound-marks HTTP/1.1
Content-Type: application/json Content-Type: application/json
@@ -26,12 +26,7 @@ public final class InboundMarkPdfDtos {
String warehouseText, String warehouseText,
String cartonText, String cartonText,
String routeText, String routeText,
String qrContent, String qrContent
String skuCode
) { ) {
public Label(String topCode, String warehouseText, String cartonText,
String routeText, String qrContent) {
this(topCode, warehouseText, cartonText, routeText, qrContent, null);
}
} }
} }
@@ -41,9 +41,6 @@ public class InboundMarkV1Renderer {
drawCenteredText(stream, units, font, label.warehouseText(), 36, 26); drawCenteredText(stream, units, font, label.warehouseText(), 36, 26);
drawCenteredText(stream, units, font, label.cartonText(), 22, 37); drawCenteredText(stream, units, font, label.cartonText(), 22, 37);
drawCenteredText(stream, units, font, label.routeText(), 21, 47); drawCenteredText(stream, units, font, label.routeText(), 21, 47);
if (label.skuCode() != null) {
drawCenteredText(stream, units, font, "SKU: " + label.skuCode(), 14, 55);
}
qrRenderer.draw(stream, qrMatrix, QR_LEFT_PT, QR_BOTTOM_PT, QR_SIZE_PT); qrRenderer.draw(stream, qrMatrix, QR_LEFT_PT, QR_BOTTOM_PT, QR_SIZE_PT);
drawCenteredText(stream, units, font, label.qrContent(), 12, 85.5); drawCenteredText(stream, units, font, label.qrContent(), 12, 85.5);
drawCenteredText(stream, units, font, "MADE IN CHINA", 14, 92); drawCenteredText(stream, units, font, "MADE IN CHINA", 14, 92);
@@ -110,8 +110,7 @@ public class InboundMarkPdfService {
normalizeRequired(label.warehouseText(), field(index, "warehouseText")), normalizeRequired(label.warehouseText(), field(index, "warehouseText")),
normalizeRequired(label.cartonText(), field(index, "cartonText")), normalizeRequired(label.cartonText(), field(index, "cartonText")),
normalizeRequired(label.routeText(), field(index, "routeText")), normalizeRequired(label.routeText(), field(index, "routeText")),
normalizeQr(label.qrContent(), field(index, "qrContent")), normalizeQr(label.qrContent(), field(index, "qrContent"))));
normalizeOptional(label.skuCode(), field(index, "skuCode"))));
} }
return new NormalizedRequest(normalizeFileName(request.downloadFileName()), List.copyOf(labels)); return new NormalizedRequest(normalizeFileName(request.downloadFileName()), List.copyOf(labels));
} }
@@ -166,14 +165,6 @@ public class InboundMarkPdfService {
return normalized; return normalized;
} }
private String normalizeOptional(String value, String field) {
if (value == null || value.isBlank()) return null;
String normalized = Normalizer.normalize(value.trim(), Normalizer.Form.NFC);
if (normalized.codePointCount(0, normalized.length()) > 64) invalid(field + " must not exceed 64 characters");
normalized.codePoints().forEach(codePoint -> { if (isForbiddenControl(codePoint)) invalid(field + " contains a control character"); });
return normalized;
}
private boolean isForbiddenControl(int codePoint) { private boolean isForbiddenControl(int codePoint) {
int type = Character.getType(codePoint); int type = Character.getType(codePoint);
return Character.isISOControl(codePoint) return Character.isISOControl(codePoint)
@@ -229,6 +229,7 @@ components:
enum: [100] enum: [100]
InboundMarkLabel: InboundMarkLabel:
type: object type: object
description: 箱唛仅展示入仓码、仓库、箱序、线路、稳定箱码二维码及产地,不包含 SKU;SKU 明细由一件代发 A4 托盘信息单展示。
required: [topCode, warehouseText, cartonText, routeText, qrContent] required: [topCode, warehouseText, cartonText, routeText, qrContent]
properties: properties:
topCode: topCode:
@@ -244,11 +245,6 @@ components:
minLength: 1 minLength: 1
maxLength: 34 maxLength: 34
pattern: '^[\\x20-\\x7E]+$' pattern: '^[\\x20-\\x7E]+$'
skuCode:
type: string
maxLength: 64
nullable: true
description: 一件代发箱唛显示的 SKU;二维码仍只使用 qrContent 稳定箱码
FreightA4PdfRequest: FreightA4PdfRequest:
type: object type: object
required: [templateCode, pageSpec, documents] required: [templateCode, pageSpec, documents]
@@ -50,7 +50,8 @@ class InboundMarkPdfServiceGenerationTest {
stripper.setEndPage(1); stripper.setEndPage(1);
assertThat(stripper.getText(document)) assertThat(stripper.getText(document))
.contains("PCHK2608040174", "MCC1", "CTNS:2-1", "洛杉矶普船海卡", .contains("PCHK2608040174", "MCC1", "CTNS:2-1", "洛杉矶普船海卡",
"PCHK2608040174#2-1", "MADE IN CHINA"); "PCHK2608040174#2-1", "MADE IN CHINA")
.doesNotContain("SKU");
stripper.setStartPage(2); stripper.setStartPage(2);
stripper.setEndPage(2); stripper.setEndPage(2);
assertThat(stripper.getText(document)).contains("CTNS:2-2", "PCHK2608040174#2-2"); assertThat(stripper.getText(document)).contains("CTNS:2-2", "PCHK2608040174#2-2");