From cc7c2f09548baa1caf2f564fe45ddd87670c240f Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 15:38:19 +0800 Subject: [PATCH] feat: support mixed-SKU dropship cartons --- deploy/sql/头程数据库统一增量-MySQL8.sql | 35 ++++ firstleg/AGENTS.md | 6 +- .../firstleg/client/OrderCargoClient.java | 6 +- .../firstleg/client/PrintPdfClient.java | 16 ++ .../dto/DropshipPalletizationDtos.java | 21 ++- .../mapper/DropshipPalletizationMapper.java | 40 ++++- .../mapper/InboundCartonLabelSkuMapper.java | 48 +++++ .../service/DropshipPalletizationService.java | 69 +++++--- .../InboundCartonLabelPrintService.java | 29 +-- .../service/InboundCartonLabelService.java | 59 ++++++- .../db/phase2-inbound-carton-label.sql | 16 ++ .../resources/db/phase2-inbound-schema.sql | 33 ++++ .../openapi/dropship-palletization-v1.yaml | 44 +++-- .../DropshipPalletizationServiceTest.java | 17 +- .../InboundCartonLabelPrintServiceTest.java | 20 ++- .../InboundCartonLabelServiceTest.java | 29 +++ .../InternalOrderCargoController.java | 14 +- .../com/baiyue/order/dto/OrderRequests.java | 23 ++- .../baiyue/order/entity/OrderCargoEntity.java | 2 +- .../MerchantInboundOrderListService.java | 8 +- .../service/OrderApplicationService.java | 99 +++++++++-- .../service/OrderCargoPackageCounts.java | 24 +++ .../service/OrderInboundOutboxPublisher.java | 9 +- .../PrintableInboundSnapshotService.java | 5 +- .../service/UnifiedOrderQueryService.java | 12 +- order/src/main/resources/db/order.sql | 1 + .../resources/db/phase1-order-extension.sql | 1 + .../src/main/resources/openapi/order-v1.yaml | 9 +- ...MerchantBatchDraftOpenApiContractTest.java | 2 + .../OrderApplicationServiceStateTest.java | 79 ++++++++- .../service/OrderCargoPackageCountsTest.java | 29 +++ print/AGENTS.md | 10 +- print/README.md | 3 +- .../controller/InternalPdfController.java | 16 ++ .../print/dto/DropshipPalletA4PdfDtos.java | 23 +++ .../renderer/DropshipPalletA4Renderer.java | 167 ++++++++++++++++++ .../print/renderer/PdfBarcodeRenderer.java | 36 ++++ .../service/DropshipPalletA4PdfService.java | 100 +++++++++++ .../resources/openapi/print-internal-v1.yaml | 63 +++++++ .../controller/InternalPdfControllerTest.java | 53 ++++++ .../renderer/PdfBarcodeRendererTest.java | 22 +++ ...pshipPalletA4PdfServiceGenerationTest.java | 93 ++++++++++ 42 files changed, 1263 insertions(+), 128 deletions(-) create mode 100644 firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundCartonLabelSkuMapper.java create mode 100644 order/src/main/java/com/baiyue/order/service/OrderCargoPackageCounts.java create mode 100644 order/src/test/java/com/baiyue/order/service/OrderCargoPackageCountsTest.java create mode 100644 print/src/main/java/com/baiyue/print/dto/DropshipPalletA4PdfDtos.java create mode 100644 print/src/main/java/com/baiyue/print/renderer/DropshipPalletA4Renderer.java create mode 100644 print/src/main/java/com/baiyue/print/renderer/PdfBarcodeRenderer.java create mode 100644 print/src/main/java/com/baiyue/print/service/DropshipPalletA4PdfService.java create mode 100644 print/src/test/java/com/baiyue/print/renderer/PdfBarcodeRendererTest.java create mode 100644 print/src/test/java/com/baiyue/print/service/DropshipPalletA4PdfServiceGenerationTest.java diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 459e78656..bb8add3c1 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -1793,6 +1793,7 @@ CALL phase1_add_column('ord_order_cargo', 'product_height_cm', "DECIMAL(12,2) NU CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Shipment ID'"); CALL phase1_add_column('ord_order_cargo', 'reference_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Reference ID'"); CALL phase1_add_column('ord_order_cargo', 'product_image_file_id', "BIGINT UNSIGNED NULL COMMENT 'Product image logical file ID'"); +CALL phase1_add_column('ord_order_cargo', 'packing_group_no', "VARCHAR(32) NULL COMMENT '混箱组合号;同组货物行共享同一批物理箱'"); CALL phase1_add_column('ord_order_cargo', 'wooden_product', "TINYINT(1) NOT NULL DEFAULT 0 COMMENT '木制品权威判定快照;1时订单必须商检'"); UPDATE ord_order_cargo cargo @@ -2906,6 +2907,23 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label ( KEY idx_carton_label_detail (tenant_id, inbound_id, inbound_detail_id), CONSTRAINT fk_carton_label_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id) ) ENGINE=InnoDB COMMENT='首次打印前冻结的实际箱号与订单货物明细映射'; +CREATE TABLE IF NOT EXISTS fl_inbound_carton_label_sku ( + id BIGINT UNSIGNED NOT NULL COMMENT '箱唛SKU快照ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', + carton_label_id BIGINT UNSIGNED NOT NULL COMMENT '物理箱唛ID', + inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID', + sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照', + cargo_name VARCHAR(255) NULL COMMENT '货物名称快照', + expected_quantity INT UNSIGNED NOT NULL COMMENT '该物理箱内应收SKU数量', + line_no INT UNSIGNED NOT NULL COMMENT '箱内SKU展示顺序', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + PRIMARY KEY (id), + UNIQUE KEY uk_carton_label_sku_detail (carton_label_id,inbound_detail_id), + KEY idx_carton_label_sku_code (tenant_id,inbound_id,sku_code), + CONSTRAINT fk_carton_label_sku_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label(id) +) ENGINE=InnoDB COMMENT='物理箱唛内的多SKU冻结快照'; + -- 存量环境可能缺少稳定箱码映射兼容字段;CREATE TABLE IF NOT EXISTS 不会补列。 DROP PROCEDURE IF EXISTS unified_carton_label_add_column; @@ -18242,6 +18260,23 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet_carton ( CONSTRAINT fk_dropship_carton_pallet FOREIGN KEY (pallet_id) REFERENCES fl_dropship_pallet(id), CONSTRAINT fk_dropship_carton_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label(id) ) ENGINE=InnoDB COMMENT='一件代发托盘内逐箱SKU和入仓单明细'; +CREATE TABLE IF NOT EXISTS fl_dropship_pallet_carton_sku ( + id BIGINT UNSIGNED NOT NULL COMMENT '清点箱SKU明细ID', + carton_id BIGINT UNSIGNED NOT NULL COMMENT '清点物理箱ID', + inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '入仓货物明细ID', + sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照', + cargo_name VARCHAR(255) NULL COMMENT '货物名称快照', + expected_quantity INT UNSIGNED NOT NULL COMMENT '应收数量', + actual_quantity INT UNSIGNED NOT NULL COMMENT '实收数量', + discrepancy_status VARCHAR(24) NOT NULL COMMENT 'MATCH或ACCEPTED_VARIANCE', + discrepancy_remark VARCHAR(500) NULL COMMENT '差异处理说明', + line_no INT UNSIGNED NOT NULL COMMENT '箱内SKU顺序', + PRIMARY KEY (id), + UNIQUE KEY uk_dropship_carton_sku_detail (carton_id,inbound_detail_id), + KEY idx_dropship_carton_sku_code (sku_code,carton_id), + CONSTRAINT fk_dropship_carton_sku_carton FOREIGN KEY (carton_id) REFERENCES fl_dropship_pallet_carton(id) +) ENGINE=InnoDB COMMENT='一件代发物理箱内逐SKU清点事实'; + -- 已存在开发库直接收敛到逐箱清点与整托分柜结构;新建库由上面的最新定义一次完成。 DROP PROCEDURE IF EXISTS converge_dropship_palletization_schema; diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index d40a3a231..32956daa5 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -19,8 +19,8 @@ | 入库单扣货、履约门禁与凭证 | `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`,只接受内部服务令牌 | | 平台测量/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.sku_code` 是箱唛的 SKU 冻结快照,生成时经 `client/OrderCargoClient.java` 获取;既有记录按 `inbound_detail_id` 回填。`ONE_CLICK_DROPSHIP` 缺 SKU 时禁止生成箱唛,打印面显示 SKU;`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` 托盘码并下发独立任务,不创建国内库存;PDA 逐箱清点,支持先扫箱或先扫托、归托和逐托封托,预计托数与已生成托盘数不一致时禁止作业,全部箱和全部预生成托盘完成后进入 `WAIT_SPLIT`。契约 `src/main/resources/openapi/dropship-palletization-v1.yaml` | +| 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `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` | +| 一件代发测量后 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` | | 称重确认与整票库位块 | `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` | | 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` | @@ -65,7 +65,7 @@ - 价格簿查询消费者依赖 `PriceBookQueryUseCase` 或稳定查询端口,不得反向依赖持久化 Mapper;移动旧查询类时同步更新生产调用、测试、Mock 与 import,避免同一职责在根 `repository/mapper` 和 `pricebookquery/` 双写。 - 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。 - 稳定箱码映射以入库快照和 Order 货物明细为事实;订单审核创建入库单时必须幂等生成,打印接口只复用或补齐同一映射,用户是否下载箱唛不得决定箱码有效性。手工测量、墨家设备和 PDA 扫码统一通过 `InboundCartonLabelService` 校验客户、入库单、货物明细、总箱数/下标和有效批次,不得在测量时临时伪造箱码或接受跨客户、跨入库箱码。 -- 一件代发以 `fl_inbound_carton_label.sku_code` 作为不可变 SKU 快照;PDA 仍只扫描稳定 `box_code`,不得将 SKU 塞入二维码或以 SKU 替代箱码。实际打托后的查询必须能按“现场托盘 -> 客户 -> SKU -> 箱数”聚合;历史箱唛仅能在同客户、同入库单范围内按 `inbound_detail_id` 回填,禁止按货名、箱规或箱号猜测 SKU。修改本链路时同步检查 `OrderCargoClient`、Order 内部货物接口及 PDA 打托实现。 +- 一件代发以 `fl_inbound_carton_label_sku` 作为物理箱内不可变 SKU 快照;一个稳定 `box_code` 可以对应多条 SKU,PDA 扫箱后必须一次清点全部 SKU,不得将 SKU 塞入二维码或以 SKU 替代箱码。实际打托后的查询必须能按“现场托盘 -> 客户 -> SKU -> 箱数”聚合;禁止按货名、箱规或箱号猜测 SKU。修改本链路时同步检查 `OrderCargoClient`、Order 内部货物接口及 PDA 打托实现。 - 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。 - 客户箱规配置必须恰好保持四档且默认算法在四行一致;`ADD` 以实测尺寸为基准对三个维度分别增加命中箱规的尺寸值,`MULTIPLY` 使用当前客户唯一默认计费规则,并由服务端按规则中的规格档位选择系数。测量行必须保存实际尺寸、选用规则和算法快照,页面不得自行重算后覆盖服务端结果;计价使用用户确认的重量/体积选择值并纳入幂等请求哈希。 - 报关、订舱和拖车引用的供应商必须通过 `WarehouseSupplierSnapshotService` 按 ID 向 Warehouse 校验:报关使用 `CUSTOMS`,订舱使用 `BOOKING`(不区分国内/国外 scope),拖车使用 `TRUCKING`;供应商必须为 `ACTIVE`。请求中的名称只能作为一致性提示,持久化名称必须来自 Warehouse 快照,禁止用自由文本绕过 ID、类别或启用状态校验。修改任一供应商引用 DTO、OpenAPI 或内部客户端时,必须同步检查三条业务链、Warehouse `supplier-v1.yaml`、数据库快照字段和对应契约测试。 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java index ee11c2d60..45387e73e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java @@ -65,18 +65,18 @@ public interface OrderCargoClient { record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, String skuNo, Integer packageCount, Integer unitsPerCarton, BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, - String shipmentId) { + String shipmentId, String packingGroupNo) { public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm) { this(cargoId, orderId, itemNo, cargoName, null, - packageCount, null, lengthCm, widthCm, heightCm, null); + packageCount, null, lengthCm, widthCm, heightCm, null, null); } public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName, String skuNo, Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, String shipmentId) { this(cargoId, orderId, itemNo, cargoName, skuNo, - packageCount, null, lengthCm, widthCm, heightCm, shipmentId); + packageCount, null, lengthCm, widthCm, heightCm, shipmentId, null); } } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java index 923ba2e5c..1275279de 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/PrintPdfClient.java @@ -26,6 +26,12 @@ public interface PrintPdfClient { @RequestHeader(value = "X-Request-Id", required = false) String requestId, @RequestBody FreightA4PdfRequest request); + @PostMapping(value = "/dropship-pallet-a4-documents", consumes = "application/json") + ResponseEntity generateDropshipPalletA4Documents( + @RequestHeader("X-Internal-Service-Token") String token, + @RequestHeader(value = "X-Request-Id", required = false) String requestId, + @RequestBody DropshipPalletA4PdfRequest request); + record InboundMarkPdfRequest(String templateCode, PageSpec pageSpec, String downloadFileName, List