feat: complete dropship palletization workflow
This commit is contained in:
@@ -57,7 +57,7 @@
|
||||
| 海外仓排车查询、候选与智能预排 | `lastleg-service/` | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`;草稿柜、托盘锁和预排归 `dispatch/`,只读排车和预排创建独立开关,不自动开放编辑、取消、发车、PDA 出库或派送 |
|
||||
| 海外排车执行、PDA 出库、派送与一件代发 | `lastleg-service/` | `dispatch/controller/DispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`、`delivery/controller/DeliveryTransferController.java`、`OutboundPdaController.java`、`MerchantDeliveryController.java`,一件代发库存归 `dropship/`;统一经 `gateway-overseas/`,设备端正式入口继续查 `../apps/pda-overseas-uniapp/AGENTS.md` |
|
||||
| 海外异常退回与客户回访 | `lastleg-service/` | `exceptionorder/`、`customervisit/`;平台异常建档和关系解析、派送退回入仓、派送完成/状态、回访资料与附件均以 Lastleg 自身客户范围和状态为准 |
|
||||
| 一件代发 SKU/箱唛与暂扣跨区协同 | `order/`、`firstleg/`、`common-event-contract/`、`lastleg-service/` | Order `InternalOrderCargoController` 提供强制 SKU;Firstleg `InboundCartonLabelService` 冻结 SKU、`LoadingWorkPalletService` 维护装柜工作托盘并消费 `OverseasInboundOrderDetainedChangedV1`;Lastleg `dropship/` 建立库存并发布暂扣变更 |
|
||||
| 一件代发 SKU/箱唛与暂扣跨区协同 | `order/`、`firstleg/`、`common-event-contract/`、`lastleg-service/` | Order `InternalOrderCargoController` 提供强制 SKU 与每箱数量;Firstleg `InboundCartonLabelService` 冻结箱唛 SKU,`DropshipPalletizationService` 完成清点、组托和封托,智能排柜只按 `SEALED` 整托分配,并消费 `OverseasInboundOrderDetainedChangedV1`;Lastleg `dropship/` 建立库存并发布暂扣变更 |
|
||||
| 客户海外仓贴标、转运、废弃与退货换标 | `lastleg-service/` | `relabel/`、`transfer/`、`discard/` 的 Merchant Controller/Service;Client 消费端 `../apps/client-web/src/modules/overseas-warehouse/`,统一经 `gateway-overseas/` |
|
||||
| 海外仓 PDA 盘点、移库、托盘、换标与查询 | `lastleg-service/`、`warehouse-overseas-service/` | Lastleg `pda/stocktake/`、`pda/move/`、`pda/warehouse/`、`palletops/`、`relabel/pda/`;契约 `lastleg-service/src/main/resources/openapi/overseas-pda-operations-v1.yaml`;物理托盘流转与补偿进入 Warehouse `InternalPdaPalletTransitionController`,设备端入口是否开放以 `../apps/pda-overseas-uniapp/AGENTS.md` 为准 |
|
||||
| 国内船节点向海外接柜门禁事件 | `firstleg/`、`common-event-contract/`、`lastleg-service/` | Firstleg `portschedule/service/CarrierScheduleNodeOutboxService.java` 发布 `FirstLegContainerShippingNodeRecordedV1`;Lastleg `shipping/service/ContainerShippingNodeConsumer.java` 单调消费,只有实际 `PICKUP_READY` 及以后节点允许海外 PDA 候选进入可收货 |
|
||||
|
||||
@@ -2860,6 +2860,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
||||
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||
sku_code VARCHAR(128) NULL COMMENT '箱唛绑定的SKU快照;二维码内容不变',
|
||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱SKU数量快照;一件代发必须为正数',
|
||||
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
||||
@@ -2877,7 +2878,6 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_carton_label_box_code (tenant_id, box_code),
|
||||
UNIQUE KEY uk_carton_label_index (tenant_id, inbound_id, label_version, carton_index),
|
||||
KEY idx_carton_label_active (tenant_id, inbound_id, batch_status, label_version),
|
||||
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='首次打印前冻结的实际箱号与订单货物明细映射';
|
||||
@@ -5190,6 +5190,10 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
product_image_url VARCHAR(1000) NULL COMMENT '产品图片访问地址快照',
|
||||
package_count INT UNSIGNED NOT NULL COMMENT '本次装柜箱数',
|
||||
product_quantity INT UNSIGNED NULL COMMENT '产品总数量',
|
||||
unit_type VARCHAR(24) NOT NULL DEFAULT 'PACKAGE' COMMENT '分配单元:PACKAGE普通箱、SEALED_PALLET一件代发封托',
|
||||
pallet_id BIGINT UNSIGNED NULL COMMENT '一件代发封托ID;普通箱为空',
|
||||
pallet_no VARCHAR(32) NULL COMMENT '一件代发系统托盘码快照',
|
||||
indivisible TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为不可拆整托单元',
|
||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱产品数',
|
||||
pallet_count INT UNSIGNED NULL COMMENT '托盘数',
|
||||
gross_weight_kg DECIMAL(18,3) NULL COMMENT '本次装柜毛重kg',
|
||||
@@ -5210,24 +5214,23 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
removed_by_name VARCHAR(64) NULL COMMENT '推送前逻辑删除操作人名称快照',
|
||||
removed_at DATETIME(3) NULL COMMENT '推送前逻辑删除时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '推送前逻辑删除标记;冻结后不可修改',
|
||||
active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS (
|
||||
CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END
|
||||
) STORED COMMENT '未删除明细唯一锁;删除后返回NULL释放分配资格',
|
||||
active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END) STORED COMMENT '未删除明细查询键;删除后返回NULL',
|
||||
active_assignment_key VARCHAR(96) GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN CONCAT(CASE WHEN pallet_id IS NULL THEN 'D:0' ELSE CONCAT('P:', pallet_id) END, ':', inbound_detail_id) ELSE NULL END) STORED COMMENT '普通明细或已封托托盘明细的有效唯一锁',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
COMMENT '装柜明细最后更新时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '装柜明细最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_item_plan_detail (loading_plan_id, active_inbound_detail_id),
|
||||
UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id, active_assignment_key),
|
||||
KEY idx_loading_item_active_detail (account_id, active_inbound_detail_id),
|
||||
KEY idx_loading_item_plan (loading_plan_id, deleted, load_sequence, id),
|
||||
KEY idx_loading_item_account (account_id, created_at, id),
|
||||
KEY idx_loading_item_inbound (account_id, inbound_id, deleted),
|
||||
KEY idx_loading_item_candidate (account_id, inbound_id, inbound_detail_id, deleted),
|
||||
KEY idx_loading_item_pallet (pallet_id, deleted),
|
||||
KEY idx_loading_item_order (account_id, order_id, deleted),
|
||||
CONSTRAINT fk_loading_item_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||
CONSTRAINT chk_loading_item_account CHECK (account_id > 0),
|
||||
@@ -5555,65 +5558,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_history (
|
||||
CONSTRAINT fk_loading_pick_history_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id)
|
||||
) ENGINE=InnoDB COMMENT='PDA拣货幂等和操作审计历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场作业托盘ID',
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID快照',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
||||
warehouse_pallet_id BIGINT UNSIGNED NULL COMMENT '仓库实物托盘ID;未绑定仓库资源时为空',
|
||||
pallet_code VARCHAR(64) NOT NULL COMMENT '现场托盘号',
|
||||
open_operation_id VARCHAR(128) NOT NULL COMMENT '开托幂等操作号',
|
||||
seal_operation_id VARCHAR(128) NULL COMMENT '封托幂等操作号',
|
||||
pallet_status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT '托盘状态:OPEN打托中、SEALED已封托、CANCELLED已取消',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人名称快照',
|
||||
sealed_by BIGINT UNSIGNED NULL COMMENT '封托操作人ID',
|
||||
sealed_by_name VARCHAR(64) NULL COMMENT '封托操作人名称快照',
|
||||
sealed_at DATETIME(3) NULL COMMENT '封托时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_work_pallet_code (pick_task_id, pallet_code),
|
||||
UNIQUE KEY uk_loading_work_pallet_open_operation (open_operation_id),
|
||||
UNIQUE KEY uk_loading_work_pallet_seal_operation (seal_operation_id),
|
||||
KEY idx_loading_work_pallet_plan (loading_plan_id, pallet_status, id),
|
||||
CONSTRAINT fk_loading_work_pallet_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
||||
CONSTRAINT fk_loading_work_pallet_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||
CONSTRAINT chk_loading_work_pallet_status CHECK (pallet_status IN ('OPEN','SEALED','CANCELLED'))
|
||||
) ENGINE=InnoDB COMMENT='一件代发PDA现场实际托盘;允许同托盘承载多个客户订单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet_carton (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场托盘箱唛绑定ID',
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
||||
work_pallet_id BIGINT UNSIGNED NOT NULL COMMENT '现场作业托盘ID',
|
||||
loading_plan_item_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划货物明细ID',
|
||||
pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '箱唛所属客户ID',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓单ID',
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓货物明细ID',
|
||||
carton_label_id BIGINT UNSIGNED NOT NULL COMMENT '稳定箱唛记录ID',
|
||||
box_code VARCHAR(80) NOT NULL COMMENT '稳定箱唛箱码快照',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照',
|
||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱产品数量快照',
|
||||
bound_by BIGINT UNSIGNED NOT NULL COMMENT '最后绑定操作人ID',
|
||||
bound_by_name VARCHAR(64) NULL COMMENT '最后绑定操作人名称快照',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '最近一次绑定或移托幂等操作号',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次上托时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后移托时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_work_carton_task_label (pick_task_id, carton_label_id),
|
||||
UNIQUE KEY uk_loading_work_carton_operation (operation_id),
|
||||
KEY idx_loading_work_carton_pallet (work_pallet_id, sku_code, id),
|
||||
KEY idx_loading_work_carton_plan_item (loading_plan_item_id, id),
|
||||
CONSTRAINT fk_loading_work_carton_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
||||
CONSTRAINT fk_loading_work_carton_pallet FOREIGN KEY (work_pallet_id) REFERENCES fl_loading_work_pallet (id),
|
||||
CONSTRAINT fk_loading_work_carton_plan_item FOREIGN KEY (loading_plan_item_id) REFERENCES fl_loading_plan_item (id),
|
||||
CONSTRAINT fk_loading_work_carton_pick_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
|
||||
CONSTRAINT fk_loading_work_carton_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label (id),
|
||||
CONSTRAINT chk_loading_work_carton_units CHECK (units_per_carton IS NULL OR units_per_carton > 0)
|
||||
) ENGINE=InnoDB COMMENT='一件代发PDA逐箱上托事实;客户订单不合并但实际托盘可混托';
|
||||
|
||||
DROP PROCEDURE IF EXISTS repair_phase3_loading_pick_task_foreign_keys;
|
||||
DELIMITER $$
|
||||
@@ -6276,12 +6221,13 @@ BEGIN
|
||||
'destination_warehouse_name', 'product_name_zh', 'product_name_en',
|
||||
'product_image_file_id', 'pallet_count', 'net_weight_kg',
|
||||
'dimensional_weight_kg', 'product_quantity', 'units_per_carton',
|
||||
'fba_tracking_no', 'shipment_no', 'shipping_mark', 'version')) <> 17 THEN
|
||||
'unit_type', 'pallet_id', 'pallet_no', 'indivisible', 'active_assignment_key',
|
||||
'fba_tracking_no', 'shipment_no', 'shipping_mark', 'version')) <> 22 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '智能装柜迁移失败:明细归属、清单快照或冻结字段缺失';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_plan_item'
|
||||
AND index_name = 'uk_loading_item_plan_detail' AND non_unique = 0) <> 2 THEN
|
||||
AND index_name = 'uk_loading_item_plan_assignment' AND non_unique = 0) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '智能装柜迁移失败:计划内唯一活动明细锁缺失';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
@@ -17393,7 +17339,9 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet_task (
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
||||
task_status VARCHAR(24) NOT NULL COMMENT '任务状态:PENDING待领取、CLAIMED已领取、COMPLETED已完成',
|
||||
total_cartons INT UNSIGNED NOT NULL COMMENT '测量确认总箱数',
|
||||
counted_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已逐箱确认实收数量的箱数',
|
||||
allocated_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已上托箱数',
|
||||
discrepancy_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已确认差异箱数',
|
||||
assignee_id BIGINT UNSIGNED NULL COMMENT '任务领取人ID',
|
||||
assignee_name VARCHAR(64) NULL COMMENT '任务领取人姓名快照',
|
||||
claimed_at DATETIME(3) NULL COMMENT '任务领取时间',
|
||||
@@ -17406,7 +17354,7 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet_task (
|
||||
KEY idx_dropship_task_operator (task_status,assignee_id,warehouse_id,created_at),
|
||||
CONSTRAINT fk_dropship_task_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order(id),
|
||||
CONSTRAINT chk_dropship_task_status CHECK (task_status IN ('PENDING','CLAIMED','COMPLETED')),
|
||||
CONSTRAINT chk_dropship_task_cartons CHECK (allocated_cartons<=total_cartons)
|
||||
CONSTRAINT chk_dropship_task_cartons CHECK (allocated_cartons<=counted_cartons AND counted_cartons<=total_cartons)
|
||||
) ENGINE=InnoDB COMMENT='一件代发测量后PDA合托任务';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
|
||||
@@ -17415,8 +17363,8 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '一件代发入仓单ID',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
||||
pallet_no VARCHAR(32) NULL COMMENT '封托时生成的DP托盘号',
|
||||
pallet_status VARCHAR(16) NOT NULL COMMENT '托盘状态:OPEN合托中、COMPLETED已封托',
|
||||
pallet_no VARCHAR(32) NOT NULL COMMENT '现场扫描的系统DP托盘码',
|
||||
pallet_status VARCHAR(16) NOT NULL COMMENT '托盘状态:OPEN组托中、SEALED已封托',
|
||||
open_operation_id VARCHAR(128) NOT NULL COMMENT '创建托盘幂等操作ID',
|
||||
seal_operation_id VARCHAR(128) NULL COMMENT '封托幂等操作ID',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '托盘创建人ID',
|
||||
@@ -17433,7 +17381,7 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
|
||||
UNIQUE KEY uk_dropship_pallet_seal_op (seal_operation_id),
|
||||
KEY idx_dropship_pallet_inbound (tenant_id,inbound_id,pallet_status),
|
||||
CONSTRAINT fk_dropship_pallet_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
|
||||
CONSTRAINT chk_dropship_pallet_status CHECK (pallet_status IN ('OPEN','COMPLETED'))
|
||||
CONSTRAINT chk_dropship_pallet_status CHECK (pallet_status IN ('OPEN','SEALED'))
|
||||
) ENGINE=InnoDB COMMENT='一件代发合托形成的可打印托盘';
|
||||
|
||||
CALL queue_column_comment('baiyue_firstleg','fl_loading_cabinet_evidence','id','柜次证据记录ID');
|
||||
@@ -17500,23 +17448,33 @@ DROP TEMPORARY TABLE IF EXISTS tmp_table_comment_target;
|
||||
CREATE TABLE IF NOT EXISTS fl_dropship_pallet_carton (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '托盘箱明细ID',
|
||||
task_id BIGINT UNSIGNED NOT NULL COMMENT '所属合托任务ID',
|
||||
pallet_id BIGINT UNSIGNED NOT NULL COMMENT '所属合托托盘ID',
|
||||
pallet_id BIGINT UNSIGNED NULL COMMENT '所属组托托盘ID;已清点待绑托时为空',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '一件代发入仓单ID',
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '入仓货物明细ID',
|
||||
carton_label_id BIGINT UNSIGNED NOT NULL COMMENT '稳定箱唛记录ID',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '箱唛冻结SKU编码',
|
||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||
box_code VARCHAR(64) NOT NULL COMMENT '稳定箱码',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '上托幂等操作ID',
|
||||
bound_by BIGINT UNSIGNED NOT NULL COMMENT '上托操作人ID',
|
||||
box_code VARCHAR(80) NOT NULL COMMENT '稳定箱码',
|
||||
expected_sku_quantity INT UNSIGNED NOT NULL COMMENT '箱唛冻结的应收SKU数量',
|
||||
actual_sku_quantity INT UNSIGNED NOT NULL COMMENT 'PDA确认的实收SKU数量',
|
||||
discrepancy_status VARCHAR(24) NOT NULL COMMENT '差异状态:MATCH或ACCEPTED_VARIANCE',
|
||||
discrepancy_remark VARCHAR(500) NULL COMMENT '差异接受说明',
|
||||
count_operation_id VARCHAR(128) NOT NULL COMMENT '逐箱清点幂等操作ID',
|
||||
bind_operation_id VARCHAR(128) NULL COMMENT '批量绑托幂等操作ID',
|
||||
last_operation_id VARCHAR(128) NOT NULL COMMENT '最后一次绑托或换托操作幂等号',
|
||||
counted_by BIGINT UNSIGNED NOT NULL COMMENT '清点操作人ID',
|
||||
counted_by_name VARCHAR(64) NULL COMMENT '清点操作人姓名快照',
|
||||
counted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '清点时间',
|
||||
bound_by BIGINT UNSIGNED NULL COMMENT '上托操作人ID',
|
||||
bound_by_name VARCHAR(64) NULL COMMENT '上托操作人姓名快照',
|
||||
bound_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '上托时间',
|
||||
bound_at DATETIME(3) NULL COMMENT '上托时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_dropship_carton_task_label (task_id,carton_label_id),
|
||||
KEY idx_dropship_carton_pallet_sku (pallet_id,sku_code,id),
|
||||
KEY idx_dropship_carton_inbound (tenant_id,inbound_id,pallet_id),
|
||||
KEY idx_dropship_carton_operation (operation_id),
|
||||
UNIQUE KEY uk_dropship_carton_count_operation (count_operation_id),
|
||||
KEY idx_dropship_carton_bind_operation (task_id,bind_operation_id),
|
||||
CONSTRAINT fk_dropship_carton_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
|
||||
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)
|
||||
|
||||
@@ -63,12 +63,13 @@ public interface OrderCargoClient {
|
||||
}
|
||||
|
||||
record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
|
||||
String skuNo, Integer packageCount, Integer unitsPerCarton,
|
||||
BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
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, lengthCm, widthCm, heightCm);
|
||||
this(cargoId, orderId, itemNo, cargoName, null, packageCount, null, lengthCm, widthCm, heightCm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-10
@@ -62,23 +62,44 @@ public class PlatformDropshipPalletizationController {
|
||||
return ApiResponse.success(service.claim(taskId, request.warehouseId(), user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/pallets/open")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Pallet> open(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@GetMapping("/tasks/{taskId}/cartons/resolve")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ApiResponse<DropshipPalletizationDtos.Carton> resolveCarton(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@Valid @RequestBody DropshipPalletizationDtos.OpenPalletRequest request) {
|
||||
@RequestParam @Positive long warehouseId,
|
||||
@RequestParam @Size(max = 80) String boxCode) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.open(taskId, request, user.userId(), user.username()));
|
||||
return ApiResponse.success(service.resolveCarton(taskId, warehouseId, boxCode, user.userId()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/pallets/{palletId}/scan")
|
||||
@PostMapping("/tasks/{taskId}/cartons/count")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<DropshipPalletizationDtos.ScanResult> scan(
|
||||
public ApiResponse<DropshipPalletizationDtos.CountResult> countCarton(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId, @PathVariable @Positive long palletId,
|
||||
@Valid @RequestBody DropshipPalletizationDtos.ScanRequest request) {
|
||||
@PathVariable @Positive long taskId,
|
||||
@Valid @RequestBody DropshipPalletizationDtos.CountCartonRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.scan(taskId, palletId, request, user.userId(), user.username()));
|
||||
return ApiResponse.success(service.countCarton(taskId, request, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/pallets/bind-pending")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Task> bindPending(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@Valid @RequestBody DropshipPalletizationDtos.BindPendingRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.bindPending(taskId, request, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/cartons/{cartonId}/move")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Task> moveCarton(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@PathVariable @Positive long cartonId,
|
||||
@Valid @RequestBody DropshipPalletizationDtos.MoveCartonRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.moveCarton(taskId, cartonId, request, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/pallets/{palletId}/seal")
|
||||
|
||||
+10
-47
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PalletPickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
|
||||
@@ -24,11 +25,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import com.baiyue.firstleg.service.LoadingWorkPalletService;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.Pallet;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@@ -56,8 +52,6 @@ public class PlatformLoadingPdaController {
|
||||
private LoadingPickService service;
|
||||
@Resource
|
||||
private CabinetLoadingEvidenceService evidenceService;
|
||||
@Resource
|
||||
private LoadingWorkPalletService palletService;
|
||||
|
||||
@GetMapping("/tasks")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
@@ -168,6 +162,15 @@ public class PlatformLoadingPdaController {
|
||||
return ApiResponse.success(service.pick(taskId, itemId, request, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/pallets/pick")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Task> pickPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@Valid @RequestBody PalletPickRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.pickPallet(taskId, request, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/items/{itemId}/partial")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Task> partial(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@@ -226,44 +229,4 @@ public class PlatformLoadingPdaController {
|
||||
return ApiResponse.success(service.operations(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}/pallets")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||
public ApiResponse<List<Pallet>> pallets(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@RequestParam @Positive long warehouseId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(palletService.list(taskId, warehouseId));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/pallets/open")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Pallet> openPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@Valid @RequestBody OpenRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(palletService.open(taskId, request, user.userId(), user.username()));
|
||||
}
|
||||
|
||||
/** 扫到其他未封托托盘上的箱唛时,同时完成按箱移托。 */
|
||||
@PostMapping("/tasks/{taskId}/pallets/{palletId}/cartons/scan")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Pallet> scanCarton(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@PathVariable @Positive long palletId,
|
||||
@Valid @RequestBody ScanRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(palletService.scan(taskId, palletId, request,
|
||||
user.userId(), user.username()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/pallets/{palletId}/seal")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ApiResponse<Pallet> sealPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@PathVariable @Positive long palletId,
|
||||
@Valid @RequestBody SealRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(palletService.seal(taskId, palletId, request,
|
||||
user.userId(), user.username()));
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -24,6 +24,10 @@ public class PlatformSmartLoadingController {
|
||||
public ApiResponse<Plan> assign(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assign(planId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/inbound-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Plan> assignInbound(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignInboundRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignInbound(planId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/pallet-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Plan> assignPallet(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignPalletRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignPallet(planId,r,u.userId(),u.username()));}
|
||||
@DeleteMapping("/plans/{planId}/pallet-assignments/{palletId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
|
||||
public ApiResponse<Plan> removePallet(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long palletId,@Valid @RequestBody RemovePalletRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.removePallet(planId,palletId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/pending-review-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Plan> assignPendingReview(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignPendingReviewRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignPendingReview(planId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/group-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
@@ -14,13 +15,23 @@ public final class DropshipPalletizationDtos {
|
||||
|
||||
public record ClaimRequest(@Positive long warehouseId) {}
|
||||
|
||||
public record OpenPalletRequest(@Positive long warehouseId,
|
||||
@NotBlank @Size(max = 128) String operationId) {}
|
||||
public enum DiscrepancyAction { MATCH, ACCEPT_VARIANCE }
|
||||
|
||||
public record ScanRequest(@Positive long warehouseId,
|
||||
@NotBlank @Size(max = 64) String boxCode,
|
||||
@Positive @Max(2000) int cartonCount,
|
||||
@NotBlank @Size(max = 128) String operationId) {}
|
||||
public record CountCartonRequest(@Positive long warehouseId,
|
||||
@NotBlank @Size(max = 80) String boxCode,
|
||||
@Positive int actualSkuQuantity,
|
||||
@NotNull DiscrepancyAction discrepancyAction,
|
||||
@Size(max = 500) String discrepancyRemark,
|
||||
@Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
|
||||
@NotBlank @Size(max = 128) String operationId) {}
|
||||
|
||||
public record BindPendingRequest(@Positive long warehouseId,
|
||||
@NotBlank @Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
|
||||
@NotBlank @Size(max = 128) String operationId) {}
|
||||
|
||||
public record MoveCartonRequest(@Positive long warehouseId,
|
||||
@Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String targetPalletNo,
|
||||
@NotBlank @Size(max = 128) String operationId) {}
|
||||
|
||||
public record SealPalletRequest(@Positive long warehouseId,
|
||||
@NotBlank @Size(max = 128) String operationId,
|
||||
@@ -28,24 +39,34 @@ public final class DropshipPalletizationDtos {
|
||||
|
||||
public record PendingTask(Long taskId, Long inboundId, String inboundCode, Long tenantId,
|
||||
String customerName, Long warehouseId, String warehouseName,
|
||||
Integer totalCartons, Integer allocatedCartons, String taskStatus,
|
||||
Integer totalCartons, Integer countedCartons, Integer boundCartons,
|
||||
Integer unboundCartons, Integer discrepancyCartons, String taskStatus,
|
||||
LocalDateTime createdAt) {}
|
||||
|
||||
public record Task(Long taskId, Long inboundId, String inboundCode, Long tenantId,
|
||||
String customerName, Long warehouseId, String warehouseName,
|
||||
Integer totalCartons, Integer allocatedCartons, String taskStatus,
|
||||
Integer totalCartons, Integer countedCartons, Integer boundCartons,
|
||||
Integer unboundCartons, Integer discrepancyCartons, String taskStatus,
|
||||
Long assigneeId, String assigneeName, Integer version,
|
||||
List<Pallet> pallets) {}
|
||||
List<Pallet> pallets, List<Carton> unbound) {}
|
||||
|
||||
public record Pallet(Long palletId, String palletNo, String palletStatus,
|
||||
Integer cartonCount, Integer skuCount, Integer version,
|
||||
List<SkuItem> items, LocalDateTime completedAt) {}
|
||||
public record Pallet(Long palletId, String palletNo, String status,
|
||||
Integer cartonCount, Integer skuCount, Integer discrepancyCartons,
|
||||
java.math.BigDecimal weightKg, java.math.BigDecimal volumeCbm,
|
||||
Integer version, List<SkuItem> items, List<Carton> cartons,
|
||||
LocalDateTime sealedAt) {}
|
||||
|
||||
public record SkuItem(Long inboundId, String inboundCode, Long inboundDetailId,
|
||||
String skuCode, String cargoName, Integer cartonCount,
|
||||
String skuCode, String cargoName, Integer cartonCount, Integer skuQuantity,
|
||||
List<String> cartonMarks) {}
|
||||
|
||||
public record ScanResult(String skuCode, String cargoName, String inboundCode,
|
||||
int addedCartons, int allocatedCartons, int remainingCartons,
|
||||
Pallet pallet) {}
|
||||
public record Carton(Long cartonId, String boxCode, Long inboundId, String inboundCode,
|
||||
Long inboundDetailId, String skuCode, String cargoName,
|
||||
Integer expectedSkuQuantity, Integer actualSkuQuantity,
|
||||
String discrepancyStatus, String discrepancyRemark,
|
||||
String countStatus, Long palletId, String palletNo,
|
||||
Boolean canCount, String blockReason) {}
|
||||
|
||||
public record CountResult(Carton carton, Integer countedCartons, Integer boundCartons,
|
||||
Integer unboundCartons, Integer remainingCartons, Pallet pallet) {}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,10 @@ public final class LoadingListDtos {
|
||||
String chineseName,
|
||||
String englishName,
|
||||
String goodsImageUrl,
|
||||
String unitType,
|
||||
Long palletId,
|
||||
String palletNo,
|
||||
Boolean indivisible,
|
||||
int packageCount,
|
||||
int palletCount,
|
||||
BigDecimal grossWeightKg,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 一件代发PDA现场打托接口模型。 */
|
||||
public final class LoadingPalletDtos {
|
||||
private LoadingPalletDtos() {}
|
||||
|
||||
public record OpenRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@Min(1) Long warehousePalletId,
|
||||
@NotBlank @Size(max = 64) String palletCode,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {}
|
||||
|
||||
public record ScanRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotBlank @Size(max = 80) String boxCode,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {}
|
||||
|
||||
public record SealRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {}
|
||||
|
||||
public record Carton(Long id, String boxCode, String skuCode, Long tenantId,
|
||||
Long inboundId, Long inboundDetailId, Integer unitsPerCarton) {}
|
||||
|
||||
public record SkuSummary(String skuCode, Integer cartonCount, Long productQuantity) {}
|
||||
|
||||
public record Pallet(Long id, String palletCode, Long warehousePalletId, String status,
|
||||
Integer version, Integer cartonCount, Integer skuCount,
|
||||
Integer customerCount, List<SkuSummary> skus, List<Carton> cartons) {}
|
||||
}
|
||||
@@ -23,6 +23,13 @@ public final class LoadingPickDtos {
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {}
|
||||
|
||||
public record PalletPickRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotBlank @Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
|
||||
@NotNull @Min(0) Integer expectedTaskVersion,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {}
|
||||
|
||||
/** 部分拣货为该 PDA 明细的最终实际拣出箱数,不是增量。 */
|
||||
public record PartialPickRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
|
||||
@@ -22,11 +22,14 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
public record GroupMemberCandidate(Long accountId, Long inboundId, Long orderId, String orderNo,
|
||||
String inboundCode, String customerName, Integer availablePackageCount,
|
||||
String orderStatusSnapshot, LocalDateTime expectedArrivalAt) {}
|
||||
public record PalletCandidate(Long palletId,String palletNo,String unitType,Boolean indivisible,
|
||||
Integer cartonCount,Integer skuCount,BigDecimal weightKg,BigDecimal volumeCbm) {}
|
||||
public record InboundCandidate(Long accountId, Long inboundId, Long orderId, String orderNo, String inboundCode,
|
||||
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsSummary,
|
||||
Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg,
|
||||
BigDecimal averageUnitVolumeCbm, BigDecimal availableVolumeCbm,
|
||||
String unitType,Long palletId,String palletNo,Boolean indivisible,
|
||||
Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes,
|
||||
Boolean locked, Boolean canAssign, String blockReason,
|
||||
String orderStatusSnapshot, LocalDateTime expectedArrivalAt,
|
||||
@@ -66,6 +69,10 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
public record AssignGroupRequest(@NotNull @Positive Long inboundId,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record AssignPalletRequest(@NotNull @Positive Long palletId,@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record RemovePalletRequest(@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record RemoveGroupRequest(@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record RecommendRequest(@NotNull @Min(0) Integer expectedVersion,
|
||||
|
||||
@@ -23,6 +23,7 @@ public class InboundCartonLabelEntity {
|
||||
private Integer cargoItemNo;
|
||||
private String cargoName;
|
||||
private String skuCode;
|
||||
private Integer unitsPerCarton;
|
||||
private Integer detailPackageCount;
|
||||
private String inboundCode;
|
||||
private String boxCode;
|
||||
|
||||
@@ -6,6 +6,7 @@ public class LoadingPlanItemEntity {
|
||||
private Long inboundId; private Long inboundDetailId; private Long orderId; private String orderNo; private String inboundCode; private Long destinationWarehouseId;
|
||||
private String destinationWarehouseCode; private String destinationWarehouseName; private String skuCode; private String productNameZh; private String productNameEn;
|
||||
private Long productImageFileId; private String productImageUrl; private Integer packageCount; private Integer productQuantity; private Integer unitsPerCarton;
|
||||
private String unitType; private Long palletId; private String palletNo; private Boolean indivisible;
|
||||
private Integer palletCount; private BigDecimal grossWeightKg; private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
|
||||
private String fbaTrackingNo; private String shipmentNo; private String shippingMark; private Integer loadSequence; private String positionSnapshot;
|
||||
private String internalRemark; private String customerRemark; private String warehouseRemark; private LocalDateTime frozenAt; private Long frozenBy;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 一件代发箱唛与现场实际托盘的当前绑定事实。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_loading_work_pallet_carton")
|
||||
public class LoadingWorkPalletCartonEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long pickTaskId;
|
||||
private Long workPalletId;
|
||||
private Long loadingPlanItemId;
|
||||
private Long pickItemId;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long inboundDetailId;
|
||||
private Long cartonLabelId;
|
||||
private String boxCode;
|
||||
private String skuCode;
|
||||
private Integer unitsPerCarton;
|
||||
private Long boundBy;
|
||||
private String boundByName;
|
||||
private String operationId;
|
||||
@Version
|
||||
private Integer version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 一件代发装柜现场的实际托盘。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_loading_work_pallet")
|
||||
public class LoadingWorkPalletEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long pickTaskId;
|
||||
private Long loadingPlanId;
|
||||
private Long warehouseId;
|
||||
private Long warehousePalletId;
|
||||
private String palletCode;
|
||||
private String openOperationId;
|
||||
private String sealOperationId;
|
||||
private String palletStatus;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
private Long sealedBy;
|
||||
private String sealedByName;
|
||||
private LocalDateTime sealedAt;
|
||||
@Version
|
||||
private Integer version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+158
-179
@@ -1,203 +1,182 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.PendingTask;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.PalletCandidate;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** 一件代发合托持久化;任务、托盘和逐箱绑定均独立于装柜拣货。 */
|
||||
/** 一件代发逐箱清点、待绑托与封托事实。 */
|
||||
@Mapper
|
||||
public interface DropshipPalletizationMapper {
|
||||
@Insert("""
|
||||
INSERT IGNORE INTO fl_dropship_pallet_task
|
||||
(id,tenant_id,inbound_id,warehouse_id,task_status,total_cartons,allocated_cartons,version,created_at,updated_at)
|
||||
VALUES(#{id},#{tenantId},#{inboundId},#{warehouseId},'PENDING',#{totalCartons},0,1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertTask(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("warehouseId") long warehouseId,
|
||||
@Param("totalCartons") int totalCartons);
|
||||
@Insert("""
|
||||
INSERT IGNORE INTO fl_dropship_pallet_task
|
||||
(id,tenant_id,inbound_id,warehouse_id,task_status,total_cartons,counted_cartons,allocated_cartons,discrepancy_cartons,version,created_at,updated_at)
|
||||
VALUES(#{id},#{tenantId},#{inboundId},#{warehouseId},'PENDING',#{totalCartons},0,0,0,1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))""")
|
||||
int insertTask(@Param("id") long id,@Param("tenantId") long tenantId,@Param("inboundId") long inboundId,
|
||||
@Param("warehouseId") long warehouseId,@Param("totalCartons") int totalCartons);
|
||||
|
||||
@Select("""
|
||||
SELECT t.id task_id,t.inbound_id,o.inbound_code,t.tenant_id,o.customer_name,t.warehouse_id,
|
||||
o.warehouse_name,t.total_cartons,t.allocated_cartons,t.task_status,t.created_at
|
||||
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
|
||||
WHERE t.task_status IN ('PENDING','CLAIMED')
|
||||
AND (t.assignee_id IS NULL OR t.assignee_id=#{operatorId})
|
||||
AND (#{keyword} IS NULL OR o.inbound_code LIKE CONCAT('%',#{keyword},'%')
|
||||
OR o.customer_name LIKE CONCAT('%',#{keyword},'%'))
|
||||
ORDER BY t.created_at,t.id
|
||||
""")
|
||||
List<PendingTask> pending(@Param("operatorId") long operatorId, @Param("keyword") String keyword);
|
||||
@Select("""
|
||||
SELECT t.id task_id,t.inbound_id,o.inbound_code,t.tenant_id,o.customer_name,t.warehouse_id,
|
||||
o.warehouse_name,t.total_cartons,t.counted_cartons,t.allocated_cartons bound_cartons,
|
||||
t.counted_cartons-t.allocated_cartons unbound_cartons,t.discrepancy_cartons,t.task_status,t.created_at
|
||||
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
|
||||
WHERE t.task_status IN ('PENDING','CLAIMED') AND (t.assignee_id IS NULL OR t.assignee_id=#{operatorId})
|
||||
AND (#{keyword} IS NULL OR o.inbound_code LIKE CONCAT('%',#{keyword},'%') OR o.customer_name LIKE CONCAT('%',#{keyword},'%'))
|
||||
ORDER BY t.created_at,t.id""")
|
||||
List<PendingTask> pending(@Param("operatorId") long operatorId,@Param("keyword") String keyword);
|
||||
|
||||
@Select("""
|
||||
SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type
|
||||
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
|
||||
WHERE t.id=#{taskId} AND t.warehouse_id=#{warehouseId}
|
||||
""")
|
||||
TaskRow task(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId);
|
||||
@Select("""
|
||||
SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type
|
||||
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
|
||||
WHERE t.id=#{taskId} AND t.warehouse_id=#{warehouseId}""")
|
||||
TaskRow task(@Param("taskId") long taskId,@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_task
|
||||
SET task_status='CLAIMED',assignee_id=#{operatorId},assignee_name=#{operatorName},
|
||||
claimed_at=COALESCE(claimed_at,CURRENT_TIMESTAMP(3)),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{taskId} AND warehouse_id=#{warehouseId} AND task_status IN ('PENDING','CLAIMED')
|
||||
AND (assignee_id IS NULL OR assignee_id=#{operatorId})
|
||||
""")
|
||||
int claim(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_task SET task_status='CLAIMED',assignee_id=#{operatorId},assignee_name=#{operatorName},
|
||||
claimed_at=COALESCE(claimed_at,CURRENT_TIMESTAMP(3)),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{taskId} AND warehouse_id=#{warehouseId} AND task_status IN ('PENDING','CLAIMED')
|
||||
AND (assignee_id IS NULL OR assignee_id=#{operatorId})""")
|
||||
int claim(@Param("taskId") long taskId,@Param("warehouseId") long warehouseId,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_dropship_pallet
|
||||
(id,task_id,tenant_id,inbound_id,warehouse_id,pallet_status,open_operation_id,version,created_by,created_by_name,created_at,updated_at)
|
||||
VALUES(#{id},#{taskId},#{tenantId},#{inboundId},#{warehouseId},'OPEN',#{operationId},1,
|
||||
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertPallet(@Param("id") long id, @Param("taskId") long taskId,
|
||||
@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("warehouseId") long warehouseId, @Param("operationId") String operationId,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
@Insert("""
|
||||
INSERT INTO fl_dropship_pallet
|
||||
(id,task_id,tenant_id,inbound_id,warehouse_id,pallet_no,pallet_status,open_operation_id,version,created_by,created_by_name,created_at,updated_at)
|
||||
VALUES(#{id},#{taskId},#{tenantId},#{inboundId},#{warehouseId},#{palletNo},'OPEN',#{operationId},1,
|
||||
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))""")
|
||||
int insertPallet(@Param("id") long id,@Param("taskId") long taskId,@Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId,@Param("warehouseId") long warehouseId,@Param("palletNo") String palletNo,
|
||||
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE open_operation_id=#{operationId}")
|
||||
PalletRow palletByOpenOperation(@Param("operationId") String operationId);
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE pallet_no=#{palletNo}") PalletRow palletByNo(@Param("palletNo") String palletNo);
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE seal_operation_id=#{operationId}") PalletRow palletBySealOperation(@Param("operationId") String operationId);
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE id=#{palletId} AND task_id=#{taskId} AND warehouse_id=#{warehouseId}")
|
||||
PalletRow pallet(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("warehouseId") long warehouseId);
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE task_id=#{taskId} ORDER BY id") List<PalletRow> pallets(@Param("taskId") long taskId);
|
||||
@Select("SELECT p.* FROM fl_dropship_pallet p JOIN fl_dropship_pallet_task t ON t.id=p.task_id WHERE t.tenant_id=#{tenantId} AND t.inbound_id=#{inboundId} ORDER BY p.id")
|
||||
List<PalletRow> palletsByInbound(@Param("tenantId") long tenantId,@Param("inboundId") long inboundId);
|
||||
@Select("SELECT inbound_code FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
|
||||
String inboundCode(@Param("tenantId") long tenantId,@Param("inboundId") long inboundId);
|
||||
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE seal_operation_id=#{operationId}")
|
||||
PalletRow palletBySealOperation(@Param("operationId") String operationId);
|
||||
@Select("""
|
||||
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.units_per_carton,l.inbound_code,l.box_code
|
||||
FROM fl_inbound_carton_label l JOIN fl_dropship_pallet_task t ON t.inbound_id=l.inbound_id AND t.tenant_id=l.tenant_id
|
||||
WHERE t.id=#{taskId} AND l.box_code=#{boxCode} AND l.batch_status='ACTIVE'""")
|
||||
LabelRow label(@Param("taskId") long taskId,@Param("boxCode") String boxCode);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.count_operation_id=#{operationId}") CartonRow cartonByCountOperation(@Param("operationId") String operationId);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.id=#{cartonId}") CartonRow carton(@Param("taskId") long taskId,@Param("cartonId") long cartonId);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.carton_label_id=#{labelId}")
|
||||
CartonRow countedByLabel(@Param("taskId") long taskId,@Param("labelId") long labelId);
|
||||
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE id=#{palletId} AND task_id=#{taskId} AND warehouse_id=#{warehouseId}")
|
||||
PalletRow pallet(@Param("taskId") long taskId, @Param("palletId") long palletId,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
@Insert("""
|
||||
INSERT INTO fl_dropship_pallet_carton
|
||||
(id,task_id,pallet_id,tenant_id,inbound_id,inbound_detail_id,carton_label_id,sku_code,cargo_name,box_code,
|
||||
expected_sku_quantity,actual_sku_quantity,discrepancy_status,discrepancy_remark,count_operation_id,last_operation_id,
|
||||
counted_by,counted_by_name,counted_at,bound_by,bound_by_name,bound_at)
|
||||
VALUES(#{id},#{taskId},#{palletId},#{tenantId},#{inboundId},#{detailId},#{labelId},#{skuCode},#{cargoName},#{boxCode},
|
||||
#{expectedQuantity},#{actualQuantity},#{discrepancyStatus},#{discrepancyRemark},#{operationId},#{operationId},
|
||||
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CASE WHEN #{palletId} IS NULL THEN NULL ELSE #{operatorId} END,
|
||||
CASE WHEN #{palletId} IS NULL THEN NULL ELSE #{operatorName} END,CASE WHEN #{palletId} IS NULL THEN NULL ELSE CURRENT_TIMESTAMP(3) END)""")
|
||||
int insertCarton(@Param("id") long id,@Param("taskId") long taskId,@Param("palletId") Long palletId,
|
||||
@Param("tenantId") long tenantId,@Param("inboundId") long inboundId,@Param("detailId") long detailId,
|
||||
@Param("labelId") long labelId,@Param("skuCode") String skuCode,@Param("cargoName") String cargoName,
|
||||
@Param("boxCode") String boxCode,@Param("expectedQuantity") int expectedQuantity,@Param("actualQuantity") int actualQuantity,
|
||||
@Param("discrepancyStatus") String discrepancyStatus,@Param("discrepancyRemark") String discrepancyRemark,
|
||||
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("SELECT * FROM fl_dropship_pallet WHERE task_id=#{taskId} ORDER BY id")
|
||||
List<PalletRow> pallets(@Param("taskId") long taskId);
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_task SET counted_cartons=counted_cartons+1,allocated_cartons=allocated_cartons+#{boundCount},
|
||||
discrepancy_cartons=discrepancy_cartons+#{discrepancyCount},version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{taskId} AND task_status='CLAIMED' AND assignee_id=#{operatorId} AND counted_cartons<total_cartons
|
||||
AND allocated_cartons+#{boundCount}<=total_cartons""")
|
||||
int addCounted(@Param("taskId") long taskId,@Param("operatorId") long operatorId,
|
||||
@Param("boundCount") int boundCount,@Param("discrepancyCount") int discrepancyCount);
|
||||
@Select("SELECT COUNT(*) FROM fl_dropship_pallet_carton WHERE task_id=#{taskId} AND pallet_id IS NULL") int unboundCount(@Param("taskId") long taskId);
|
||||
@Select("SELECT COUNT(*) FROM fl_dropship_pallet_carton WHERE task_id=#{taskId} AND bind_operation_id=#{operationId}")
|
||||
int bindOperationCount(@Param("taskId") long taskId,@Param("operationId") String operationId);
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_carton SET pallet_id=#{palletId},bind_operation_id=#{operationId},last_operation_id=#{operationId},
|
||||
bound_by=#{operatorId},bound_by_name=#{operatorName},bound_at=CURRENT_TIMESTAMP(3) WHERE task_id=#{taskId} AND pallet_id IS NULL""")
|
||||
int bindPending(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("operationId") String operationId,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Update("UPDATE fl_dropship_pallet_task SET allocated_cartons=allocated_cartons+#{delta},version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{taskId} AND assignee_id=#{operatorId} AND allocated_cartons+#{delta} BETWEEN 0 AND counted_cartons")
|
||||
int adjustBound(@Param("taskId") long taskId,@Param("operatorId") long operatorId,@Param("delta") int delta);
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_carton SET pallet_id=#{targetPalletId},last_operation_id=#{operationId},
|
||||
bound_by=CASE WHEN #{targetPalletId} IS NULL THEN NULL ELSE #{operatorId} END,
|
||||
bound_by_name=CASE WHEN #{targetPalletId} IS NULL THEN NULL ELSE #{operatorName} END,
|
||||
bound_at=CASE WHEN #{targetPalletId} IS NULL THEN NULL ELSE CURRENT_TIMESTAMP(3) END
|
||||
WHERE id=#{cartonId} AND task_id=#{taskId} AND last_operation_id<>#{operationId}""")
|
||||
int moveCarton(@Param("taskId") long taskId,@Param("cartonId") long cartonId,@Param("targetPalletId") Long targetPalletId,
|
||||
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("""
|
||||
SELECT p.* FROM fl_dropship_pallet p JOIN fl_dropship_pallet_task t ON t.id=p.task_id
|
||||
WHERE t.tenant_id=#{tenantId} AND t.inbound_id=#{inboundId} ORDER BY p.id
|
||||
""")
|
||||
List<PalletRow> palletsByInbound(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet SET pallet_status='SEALED',seal_operation_id=#{operationId},completed_by=#{operatorId},
|
||||
completed_by_name=#{operatorName},completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{palletId} AND task_id=#{taskId} AND pallet_status='OPEN' AND version=#{expectedVersion}
|
||||
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId})
|
||||
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.task_id=#{taskId} AND c.pallet_id IS NULL)""")
|
||||
int seal(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("expectedVersion") int expectedVersion,
|
||||
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Select("SELECT COUNT(*) FROM fl_dropship_pallet WHERE task_id=#{taskId} AND pallet_status='OPEN'") int openPalletCount(@Param("taskId") long taskId);
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_task SET task_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{taskId} AND task_status='CLAIMED' AND counted_cartons=total_cartons AND allocated_cartons=total_cartons
|
||||
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet p WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN')""") int completeTask(@Param("taskId") long taskId);
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order o SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(*) FROM fl_dropship_pallet p
|
||||
WHERE p.inbound_id=o.id AND p.pallet_status='SEALED'),o.updated_by=#{operatorId},o.version=o.version+1
|
||||
WHERE o.id=#{inboundId} AND o.tenant_id=#{tenantId} AND o.process_status='WAIT_PALLETIZE'
|
||||
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_task t WHERE t.id=#{taskId} AND t.task_status='COMPLETED')""")
|
||||
int completeInbound(@Param("taskId") long taskId,@Param("tenantId") long tenantId,@Param("inboundId") long inboundId,@Param("operatorId") long operatorId);
|
||||
|
||||
@Select("SELECT inbound_code FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
|
||||
String inboundCode(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.pallet_id=#{palletId} ORDER BY c.sku_code,c.inbound_detail_id,c.id") List<CartonRow> cartons(@Param("palletId") long palletId);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.pallet_id IS NULL ORDER BY c.counted_at,c.id") List<CartonRow> unboundCartons(@Param("taskId") long taskId);
|
||||
@Select("""
|
||||
SELECT COALESCE(o.actual_gross_weight_kg*COUNT(c.id)/NULLIF(o.total_package_count,0),0) weight_kg,
|
||||
COALESCE(o.actual_volume_cbm*COUNT(c.id)/NULLIF(o.total_package_count,0),0) volume_cbm
|
||||
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
|
||||
LEFT JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id WHERE p.id=#{palletId}
|
||||
GROUP BY p.id,o.actual_gross_weight_kg,o.actual_volume_cbm,o.total_package_count""")
|
||||
PalletMetrics metrics(@Param("palletId") long palletId);
|
||||
@Select("""
|
||||
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(c.id) carton_count,
|
||||
COUNT(DISTINCT c.sku_code) sku_count,
|
||||
COALESCE(o.actual_gross_weight_kg*COUNT(c.id)/NULLIF(o.total_package_count,0),0) weight_kg,
|
||||
COALESCE(o.actual_volume_cbm*COUNT(c.id)/NULLIF(o.total_package_count,0),0) volume_cbm
|
||||
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
|
||||
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
|
||||
WHERE p.inbound_id=#{inboundId} AND p.pallet_status='SEALED'
|
||||
AND NOT EXISTS(SELECT 1 FROM fl_loading_plan_item i WHERE i.pallet_id=p.id AND i.deleted=0)
|
||||
GROUP BY p.id,p.pallet_no,o.actual_gross_weight_kg,o.actual_volume_cbm,o.total_package_count ORDER BY p.id
|
||||
""") List<PalletCandidate> palletCandidates(@Param("inboundId") long inboundId);
|
||||
|
||||
@Select("""
|
||||
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.detail_package_count,
|
||||
l.inbound_code,l.box_code
|
||||
FROM fl_inbound_carton_label l
|
||||
JOIN fl_dropship_pallet_task t ON t.inbound_id=l.inbound_id AND t.tenant_id=l.tenant_id
|
||||
WHERE t.id=#{taskId} AND l.box_code=#{boxCode} AND l.batch_status='ACTIVE'
|
||||
""")
|
||||
LabelRow label(@Param("taskId") long taskId, @Param("boxCode") String boxCode);
|
||||
@Select("""
|
||||
SELECT p.id pallet_id,p.pallet_no,p.pallet_status,p.tenant_id account_id,p.inbound_id,o.order_id,o.order_no,
|
||||
o.inbound_code,o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,
|
||||
o.destination_code destination_warehouse_code,o.destination_name destination_warehouse_name,o.customer_name,
|
||||
c.inbound_detail_id,c.sku_code,MAX(c.cargo_name) cargo_name,COUNT(c.id) carton_count,
|
||||
SUM(c.actual_sku_quantity) actual_sku_quantity,
|
||||
COALESCE(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0),0) unit_gross_weight_kg,
|
||||
COALESCE(o.actual_net_weight_kg/NULLIF(o.total_package_count,0),0) unit_net_weight_kg,
|
||||
COALESCE(o.actual_volume_cbm/NULLIF(o.total_package_count,0),0) unit_volume_cbm
|
||||
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
|
||||
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id WHERE p.id=#{palletId}
|
||||
GROUP BY p.id,p.pallet_no,p.pallet_status,p.tenant_id,p.inbound_id,o.order_id,o.order_no,o.inbound_code,
|
||||
o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,o.destination_code,o.destination_name,
|
||||
o.customer_name,c.inbound_detail_id,c.sku_code,o.actual_gross_weight_kg,o.actual_net_weight_kg,o.actual_volume_cbm,o.total_package_count
|
||||
ORDER BY c.inbound_detail_id
|
||||
""") List<PalletLine> sealedPalletLines(@Param("palletId") long palletId);
|
||||
|
||||
@Select("""
|
||||
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.detail_package_count,
|
||||
l.inbound_code,l.box_code
|
||||
FROM fl_inbound_carton_label l
|
||||
WHERE l.tenant_id=#{tenantId} AND l.inbound_id=#{inboundId}
|
||||
AND l.inbound_detail_id=#{detailId} AND l.batch_status='ACTIVE'
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_dropship_pallet_carton c
|
||||
WHERE c.task_id=#{taskId} AND c.carton_label_id=l.id)
|
||||
ORDER BY CASE WHEN l.box_code=#{preferredBoxCode} THEN 0 ELSE 1 END,l.carton_index,l.id
|
||||
LIMIT #{count}
|
||||
""")
|
||||
List<LabelRow> unboundLabels(@Param("taskId") long taskId, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("detailId") long detailId,
|
||||
@Param("preferredBoxCode") String preferredBoxCode,
|
||||
@Param("count") int count);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM fl_dropship_pallet_carton WHERE operation_id=#{operationId}")
|
||||
int operationCount(@Param("operationId") String operationId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_dropship_pallet_carton
|
||||
(id,task_id,pallet_id,tenant_id,inbound_id,inbound_detail_id,carton_label_id,sku_code,cargo_name,
|
||||
box_code,operation_id,bound_by,bound_by_name,bound_at)
|
||||
VALUES(#{id},#{taskId},#{palletId},#{tenantId},#{inboundId},#{detailId},#{labelId},#{skuCode},#{cargoName},
|
||||
#{boxCode},#{operationId},#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertCarton(@Param("id") long id, @Param("taskId") long taskId,
|
||||
@Param("palletId") long palletId, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("detailId") long detailId,
|
||||
@Param("labelId") long labelId, @Param("skuCode") String skuCode,
|
||||
@Param("cargoName") String cargoName, @Param("boxCode") String boxCode,
|
||||
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_task
|
||||
SET allocated_cartons=allocated_cartons+#{count},version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{taskId} AND task_status='CLAIMED' AND assignee_id=#{operatorId}
|
||||
AND allocated_cartons+#{count}<=total_cartons
|
||||
""")
|
||||
int addAllocated(@Param("taskId") long taskId, @Param("operatorId") long operatorId,
|
||||
@Param("count") int count);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet
|
||||
SET pallet_no=#{palletNo},pallet_status='COMPLETED',seal_operation_id=#{operationId},
|
||||
completed_by=#{operatorId},completed_by_name=#{operatorName},completed_at=CURRENT_TIMESTAMP(3),
|
||||
version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{palletId} AND task_id=#{taskId} AND pallet_status='OPEN' AND version=#{expectedVersion}
|
||||
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId})
|
||||
""")
|
||||
int seal(@Param("taskId") long taskId, @Param("palletId") long palletId,
|
||||
@Param("expectedVersion") int expectedVersion, @Param("palletNo") String palletNo,
|
||||
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM fl_dropship_pallet WHERE task_id=#{taskId} AND pallet_status='OPEN'")
|
||||
int openPalletCount(@Param("taskId") long taskId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_task
|
||||
SET task_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{taskId} AND task_status='CLAIMED' AND allocated_cartons=total_cartons
|
||||
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet p WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN')
|
||||
""")
|
||||
int completeTask(@Param("taskId") long taskId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order o
|
||||
SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(*) FROM fl_dropship_pallet p
|
||||
WHERE p.inbound_id=o.id AND p.pallet_status='COMPLETED'),o.updated_by=#{operatorId},o.version=o.version+1
|
||||
WHERE o.id=#{inboundId} AND o.tenant_id=#{tenantId} AND o.process_status='WAIT_PALLETIZE'
|
||||
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_task t WHERE t.id=#{taskId} AND t.task_status='COMPLETED')
|
||||
""")
|
||||
int completeInbound(@Param("taskId") long taskId, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("operatorId") long operatorId);
|
||||
|
||||
@Select("""
|
||||
SELECT c.* FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId}
|
||||
ORDER BY c.sku_code,c.inbound_detail_id,c.id
|
||||
""")
|
||||
List<CartonRow> cartons(@Param("palletId") long palletId);
|
||||
|
||||
@Getter @Setter class TaskRow {
|
||||
private Long id; private Long tenantId; private Long inboundId; private Long warehouseId;
|
||||
private String inboundCode; private String customerName; private String warehouseName;
|
||||
private String taskStatus; private Integer totalCartons; private Integer allocatedCartons;
|
||||
private Long assigneeId; private String assigneeName; private Integer version;
|
||||
private String processStatus; private String fulfillmentType;
|
||||
}
|
||||
@Getter @Setter class PalletRow {
|
||||
private Long id; private Long taskId; private Long tenantId; private Long inboundId;
|
||||
private Long warehouseId; private String palletNo; private String palletStatus;
|
||||
private String openOperationId; private String sealOperationId; private Integer version;
|
||||
private LocalDateTime completedAt;
|
||||
}
|
||||
@Getter @Setter class LabelRow {
|
||||
private Long cartonLabelId; private Long inboundDetailId; private String skuCode;
|
||||
private String cargoName; private Integer detailPackageCount; private String inboundCode;
|
||||
private String boxCode;
|
||||
}
|
||||
@Getter @Setter class CartonRow {
|
||||
private Long inboundId; private Long inboundDetailId; private String skuCode;
|
||||
private String cargoName; private String boxCode;
|
||||
}
|
||||
@Getter @Setter class TaskRow { private Long id,tenantId,inboundId,warehouseId,assigneeId; private String inboundCode,customerName,warehouseName,taskStatus,assigneeName,processStatus,fulfillmentType; private Integer totalCartons,countedCartons,allocatedCartons,discrepancyCartons,version; }
|
||||
@Getter @Setter class PalletRow { private Long id,taskId,tenantId,inboundId,warehouseId; private String palletNo,palletStatus,sealOperationId; private Integer version; private LocalDateTime completedAt; }
|
||||
@Getter @Setter class LabelRow { private Long cartonLabelId,inboundDetailId; private String skuCode,cargoName,inboundCode,boxCode; private Integer unitsPerCarton; }
|
||||
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
|
||||
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
|
||||
@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; }
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public interface LoadingCompletedEventMapper {
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=#{planId}
|
||||
AND item.account_id=carton.tenant_id AND item.inbound_id=carton.inbound_id
|
||||
AND item.inbound_detail_id=carton.inbound_detail_id AND item.deleted=0
|
||||
WHERE pallet.pallet_status='COMPLETED'
|
||||
WHERE pallet.pallet_status='SEALED'
|
||||
ORDER BY carton.tenant_id,carton.inbound_id,pallet.id,item.inbound_detail_id,carton.id
|
||||
""")
|
||||
List<PalletRow> actualPalletRows(@Param("planId") long planId);
|
||||
@@ -99,7 +99,7 @@ public interface LoadingCompletedEventMapper {
|
||||
@Select("""
|
||||
SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet p
|
||||
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=p.tenant_id
|
||||
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='COMPLETED'
|
||||
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='SEALED'
|
||||
""")
|
||||
int workPalletCount(@Param("planId") long planId);
|
||||
|
||||
|
||||
@@ -171,7 +171,8 @@ public interface LoadingListQueryMapper {
|
||||
SELECT i.id AS item_id, i.account_id, i.customer_name, i.order_id, i.order_no,
|
||||
i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no,
|
||||
i.sku_code, i.destination_warehouse_name, i.product_name_zh,
|
||||
product_name_en, product_image_url, i.package_count,
|
||||
product_name_en, product_image_url, i.unit_type, i.pallet_id,
|
||||
i.pallet_no, i.indivisible, i.package_count,
|
||||
COALESCE(NULLIF(i.pallet_count,0),
|
||||
CEIL(COALESCE(NULLIF(inbound.pallet_count,0),
|
||||
(SELECT COUNT(*) FROM fl_inbound_weight_result weight
|
||||
@@ -420,6 +421,10 @@ public interface LoadingListQueryMapper {
|
||||
private String productNameZh;
|
||||
private String productNameEn;
|
||||
private String productImageUrl;
|
||||
private String unitType;
|
||||
private Long palletId;
|
||||
private String palletNo;
|
||||
private Boolean indivisible;
|
||||
private Integer packageCount;
|
||||
private Integer palletCount;
|
||||
private BigDecimal grossWeightKg;
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingWorkPalletCartonMapper extends BaseMapper<LoadingWorkPalletCartonEntity> {
|
||||
@Select("""
|
||||
SELECT label.id carton_label_id,label.tenant_id,label.inbound_id,label.inbound_detail_id,
|
||||
label.box_code,plan_item.id loading_plan_item_id,pick_item.id pick_item_id,
|
||||
plan_item.sku_code,plan_item.units_per_carton
|
||||
FROM fl_inbound_carton_label label
|
||||
JOIN fl_loading_plan_item plan_item
|
||||
ON plan_item.account_id=label.tenant_id AND plan_item.inbound_id=label.inbound_id
|
||||
AND plan_item.inbound_detail_id=label.inbound_detail_id AND plan_item.deleted=0
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id=plan_item.id
|
||||
JOIN fl_loading_pick_task task ON task.id=pick_item.pick_task_id
|
||||
AND task.loading_plan_id=plan_item.loading_plan_id
|
||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
||||
JOIN fl_inbound_order inbound_order ON inbound_order.id=plan_item.inbound_id
|
||||
AND inbound_order.tenant_id=plan_item.account_id AND inbound_order.deleted=0
|
||||
WHERE task.id=#{taskId} AND task.task_status='LOADING' AND booking.warehouse_id=#{warehouseId}
|
||||
AND label.box_code=#{boxCode} AND label.batch_status='ACTIVE'
|
||||
AND inbound_order.fulfillment_type='ONE_CLICK_DROPSHIP'
|
||||
AND pick_item.item_status<>'SKIPPED'
|
||||
ORDER BY label.tenant_id,label.id
|
||||
""")
|
||||
List<CartonCandidate> selectCandidates(@Param("taskId") long taskId,
|
||||
@Param("warehouseId") long warehouseId,
|
||||
@Param("boxCode") String boxCode);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE pick_task_id=#{taskId} AND carton_label_id=#{labelId}")
|
||||
LoadingWorkPalletCartonEntity selectBinding(@Param("taskId") long taskId,
|
||||
@Param("labelId") long labelId);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE operation_id=#{operationId}")
|
||||
LoadingWorkPalletCartonEntity selectByOperation(@Param("operationId") String operationId);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE work_pallet_id=#{palletId} ORDER BY id")
|
||||
List<LoadingWorkPalletCartonEntity> selectPalletCartons(@Param("palletId") long palletId);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_work_pallet_carton WHERE work_pallet_id=#{palletId}")
|
||||
int countPalletCartons(@Param("palletId") long palletId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT carton.pick_item_id
|
||||
FROM fl_loading_work_pallet_carton carton
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.id=carton.pick_item_id
|
||||
WHERE carton.pick_task_id=#{taskId}
|
||||
GROUP BY carton.pick_item_id,pick_item.picked_package_count
|
||||
HAVING COUNT(*)>pick_item.picked_package_count
|
||||
) unreconciled
|
||||
""")
|
||||
int countUnreconciled(@Param("taskId") long taskId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_work_pallet_carton
|
||||
SET work_pallet_id=#{targetPalletId},bound_by=#{operatorId},bound_by_name=#{operatorName},
|
||||
operation_id=#{operationId},version=version+1
|
||||
WHERE id=#{id} AND version=#{version}
|
||||
""")
|
||||
int move(@Param("id") long id, @Param("version") int version,
|
||||
@Param("targetPalletId") long targetPalletId,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName,
|
||||
@Param("operationId") String operationId);
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
class CartonCandidate {
|
||||
private Long cartonLabelId;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long inboundDetailId;
|
||||
private String boxCode;
|
||||
private Long loadingPlanItemId;
|
||||
private Long pickItemId;
|
||||
private String skuCode;
|
||||
private Integer unitsPerCarton;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingWorkPalletMapper extends BaseMapper<LoadingWorkPalletEntity> {
|
||||
@Select("""
|
||||
SELECT task.id task_id,task.loading_plan_id,task.task_status,booking.warehouse_id,
|
||||
assignment.assignee_id,
|
||||
SUM(CASE WHEN inbound_order.fulfillment_type='ONE_CLICK_DROPSHIP' THEN 1 ELSE 0 END) dropship_item_count
|
||||
FROM fl_loading_pick_task task
|
||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id=task.id
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id=pick_item.loading_plan_item_id AND plan_item.deleted=0
|
||||
JOIN fl_inbound_order inbound_order ON inbound_order.id=plan_item.inbound_id
|
||||
AND inbound_order.tenant_id=plan_item.account_id AND inbound_order.deleted=0
|
||||
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
|
||||
WHERE task.id=#{taskId} AND booking.warehouse_id=#{warehouseId}
|
||||
GROUP BY task.id,task.loading_plan_id,task.task_status,booking.warehouse_id,assignment.assignee_id
|
||||
""")
|
||||
TaskContext selectTaskContext(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_work_pallet WHERE open_operation_id=#{operationId}")
|
||||
LoadingWorkPalletEntity selectByOpenOperation(@Param("operationId") String operationId);
|
||||
|
||||
@Select("SELECT * FROM fl_loading_work_pallet WHERE seal_operation_id=#{operationId}")
|
||||
LoadingWorkPalletEntity selectBySealOperation(@Param("operationId") String operationId);
|
||||
|
||||
@Select("""
|
||||
SELECT pallet.*
|
||||
FROM fl_loading_work_pallet pallet
|
||||
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
|
||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
||||
WHERE pallet.pick_task_id=#{taskId} AND pallet.id=#{palletId}
|
||||
AND booking.warehouse_id=#{warehouseId}
|
||||
""")
|
||||
LoadingWorkPalletEntity selectScoped(@Param("taskId") long taskId,
|
||||
@Param("palletId") long palletId,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("""
|
||||
SELECT pallet.*
|
||||
FROM fl_loading_work_pallet pallet
|
||||
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
|
||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
||||
WHERE pallet.pick_task_id=#{taskId} AND booking.warehouse_id=#{warehouseId}
|
||||
AND pallet.pallet_status<>'CANCELLED'
|
||||
ORDER BY pallet.id
|
||||
""")
|
||||
List<LoadingWorkPalletEntity> selectTaskPallets(@Param("taskId") long taskId,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_work_pallet
|
||||
SET pallet_status='SEALED',seal_operation_id=#{operationId},sealed_by=#{operatorId},
|
||||
sealed_by_name=#{operatorName},sealed_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{palletId} AND pick_task_id=#{taskId} AND warehouse_id=#{warehouseId}
|
||||
AND pallet_status='OPEN' AND version=#{expectedVersion}
|
||||
""")
|
||||
int seal(@Param("taskId") long taskId, @Param("palletId") long palletId,
|
||||
@Param("warehouseId") long warehouseId, @Param("expectedVersion") int expectedVersion,
|
||||
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
record TaskContext(Long taskId, Long loadingPlanId, String taskStatus, Long warehouseId,
|
||||
Long assigneeId, Integer dropshipItemCount) {}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
SELECT carton.tenant_id,carton.inbound_id,batch.id,carton.inbound_detail_id,COUNT(*),
|
||||
COALESCE(batch.total_volume_cbm/NULLIF(source.total_package_count,0),0)
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='COMPLETED'
|
||||
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='SEALED'
|
||||
JOIN fl_inbound_order source ON source.id=carton.inbound_id AND source.tenant_id=carton.tenant_id
|
||||
AND source.fulfillment_type='ONE_CLICK_DROPSHIP'
|
||||
JOIN fl_inbound_measurement batch ON batch.inbound_id=source.id AND batch.tenant_id=source.tenant_id
|
||||
@@ -399,7 +399,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
SELECT carton.tenant_id,carton.inbound_id,batch.id,carton.inbound_detail_id,COUNT(*),
|
||||
COALESCE(batch.total_volume_cbm/NULLIF(source.total_package_count,0),0)
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='COMPLETED'
|
||||
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='SEALED'
|
||||
JOIN fl_inbound_order source ON source.id=carton.inbound_id AND source.tenant_id=carton.tenant_id
|
||||
AND source.fulfillment_type='ONE_CLICK_DROPSHIP'
|
||||
JOIN fl_inbound_measurement batch ON batch.inbound_id=source.id AND batch.tenant_id=source.tenant_id
|
||||
|
||||
@@ -187,7 +187,8 @@ public class LoadingListRepository {
|
||||
row.getOrderId(), row.getOrderNo(), row.getInboundId(), row.getInboundDetailId(),
|
||||
row.getInboundCode(), row.getContainerNo(), row.getSkuCode(),
|
||||
row.getDestinationWarehouseName(), row.getProductNameZh(), row.getProductNameEn(),
|
||||
row.getProductImageUrl(), integer(row.getPackageCount()), integer(row.getPalletCount()),
|
||||
row.getProductImageUrl(), row.getUnitType(), row.getPalletId(), row.getPalletNo(),
|
||||
Boolean.TRUE.equals(row.getIndivisible()), integer(row.getPackageCount()), integer(row.getPalletCount()),
|
||||
decimal(row.getGrossWeightKg()), decimal(row.getNetWeightKg()), decimal(row.getVolumeCbm()),
|
||||
decimal(row.getDimensionalWeightKg()), value(row.getProductQuantity()),
|
||||
integer(row.getUnitsPerCarton()), row.getFbaTrackingNo(), row.getShipmentNo(), row.getShippingMark(),
|
||||
|
||||
@@ -97,6 +97,11 @@ public class SmartLoadingRepository {
|
||||
.eq(LoadingPlanItemEntity::getInboundId, inboundId)
|
||||
.eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
}
|
||||
public List<LoadingPlanItemEntity> activeItemsByPallet(long palletId) {
|
||||
return items.selectList(new LambdaQueryWrapper<LoadingPlanItemEntity>()
|
||||
.eq(LoadingPlanItemEntity::getPalletId, palletId)
|
||||
.eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
}
|
||||
public LoadingPlanEntity plan(long planId) { return plans.selectById(planId); }
|
||||
public LoadingPlanEntity planByBooking(long bookingId) {
|
||||
return plans.selectOne(new LambdaQueryWrapper<LoadingPlanEntity>()
|
||||
@@ -121,6 +126,13 @@ public class SmartLoadingRepository {
|
||||
.eq(LoadingPlanItemEntity::getInboundDetailId, detailId)
|
||||
.eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
}
|
||||
public LoadingPlanItemEntity activePalletItem(long planId, long palletId, long detailId) {
|
||||
return items.selectOne(new LambdaQueryWrapper<LoadingPlanItemEntity>()
|
||||
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
|
||||
.eq(LoadingPlanItemEntity::getPalletId, palletId)
|
||||
.eq(LoadingPlanItemEntity::getInboundDetailId, detailId)
|
||||
.eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
}
|
||||
public LoadingPlanHistoryEntity operation(String operationId) {
|
||||
return histories.selectOne(new LambdaQueryWrapper<LoadingPlanHistoryEntity>()
|
||||
.eq(LoadingPlanHistoryEntity::getOperationId, operationId));
|
||||
|
||||
+164
-165
@@ -4,194 +4,193 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.ScanResult;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.SkuItem;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Task;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.*;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.TaskRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
/** 测量完成即下发的一件代发合托,不创建国内仓库托盘、库位或库存。 */
|
||||
/** 一件代发测量后的逐箱清点、待绑托和封托,不创建第二套装柜工作托盘。 */
|
||||
@Service
|
||||
public class DropshipPalletizationService {
|
||||
@Resource private DropshipPalletizationMapper mapper;
|
||||
@Resource private FirstlegDocumentNumberGenerator numbers;
|
||||
@Resource private DropshipPalletizationMapper mapper;
|
||||
|
||||
public void createTask(InboundOrderEntity inbound) {
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) return;
|
||||
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
|
||||
throw invalid("一件代发测量完成箱数无效,不能下发合托任务");
|
||||
}
|
||||
mapper.insertTask(IdWorker.getId(), inbound.getTenantId(), inbound.getId(),
|
||||
inbound.getWarehouseId(), inbound.getTotalPackageCount());
|
||||
}
|
||||
public void createTask(InboundOrderEntity inbound) {
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) return;
|
||||
if (inbound.getTotalPackageCount()==null || inbound.getTotalPackageCount()<1) throw invalid("一件代发测量完成箱数无效,不能下发清点组托任务");
|
||||
mapper.insertTask(IdWorker.getId(),inbound.getTenantId(),inbound.getId(),inbound.getWarehouseId(),inbound.getTotalPackageCount());
|
||||
}
|
||||
|
||||
public List<DropshipPalletizationDtos.PendingTask> pending(long operatorId, String keyword) {
|
||||
return mapper.pending(operatorId, normalize(keyword));
|
||||
}
|
||||
public List<PendingTask> pending(long operatorId,String keyword){ return mapper.pending(operatorId,normalize(keyword)); }
|
||||
|
||||
@Transactional
|
||||
public Task claim(long taskId, long warehouseId, long operatorId, String operatorName) {
|
||||
if (mapper.claim(taskId, warehouseId, operatorId, operatorName) != 1) {
|
||||
throw conflict("任务已被其他人员领取或状态已变化");
|
||||
}
|
||||
return task(taskId, warehouseId, operatorId);
|
||||
}
|
||||
@Transactional
|
||||
public Task claim(long taskId,long warehouseId,long operatorId,String operatorName){
|
||||
if(mapper.claim(taskId,warehouseId,operatorId,operatorName)!=1) throw conflict("任务已被其他人员领取或状态已变化");
|
||||
return task(taskId,warehouseId,operatorId);
|
||||
}
|
||||
|
||||
public Task task(long taskId, long warehouseId, long operatorId) {
|
||||
TaskRow row = requireTask(taskId, warehouseId);
|
||||
if (row.getAssigneeId() != null && row.getAssigneeId() != operatorId) {
|
||||
throw conflict("任务已由其他人员领取");
|
||||
}
|
||||
return view(row);
|
||||
}
|
||||
public Task task(long taskId,long warehouseId,long operatorId){
|
||||
TaskRow row=requireTask(taskId,warehouseId);
|
||||
if(row.getAssigneeId()!=null && row.getAssigneeId()!=operatorId) throw conflict("任务已由其他人员领取");
|
||||
return view(row);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Pallet open(long taskId, DropshipPalletizationDtos.OpenPalletRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
|
||||
String operationId = request.operationId().trim();
|
||||
PalletRow replay = mapper.palletByOpenOperation(operationId);
|
||||
if (replay != null) {
|
||||
if (replay.getTaskId() != taskId) throw conflict("开托幂等号已用于其他任务");
|
||||
return palletView(replay, task.getInboundCode());
|
||||
}
|
||||
long id = IdWorker.getId();
|
||||
mapper.insertPallet(id, taskId, task.getTenantId(), task.getInboundId(), task.getWarehouseId(),
|
||||
operationId, operatorId, operatorName);
|
||||
return palletView(mapper.pallet(taskId, id, request.warehouseId()), task.getInboundCode());
|
||||
}
|
||||
public Carton resolveCarton(long taskId,long warehouseId,String boxCode,long operatorId){
|
||||
TaskRow task=requireTask(taskId,warehouseId);
|
||||
if(task.getAssigneeId()!=null && task.getAssigneeId()!=operatorId) throw conflict("任务已由其他人员领取");
|
||||
LabelRow label=requireLabel(taskId,boxCode);
|
||||
CartonRow counted=mapper.countedByLabel(taskId,label.getCartonLabelId());
|
||||
if(counted!=null) return cartonView(counted,task.getInboundCode(),false,"箱码已清点");
|
||||
String block=null;
|
||||
if(!"CLAIMED".equals(task.getTaskStatus())) block="请先领取任务";
|
||||
else if(!Objects.equals(task.getAssigneeId(),operatorId)) block="任务不属于当前操作员";
|
||||
else if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) block="入仓单已不在待清点组托状态";
|
||||
return new Carton(null,label.getBoxCode(),task.getInboundId(),label.getInboundCode(),label.getInboundDetailId(),
|
||||
label.getSkuCode(),label.getCargoName(),label.getUnitsPerCarton(),null,null,null,"UNCOUNTED",null,null,
|
||||
block==null,block);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ScanResult scan(long taskId, long palletId, DropshipPalletizationDtos.ScanRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
|
||||
PalletRow pallet = requireOpenPallet(taskId, palletId, request.warehouseId());
|
||||
String operationId = request.operationId().trim();
|
||||
LabelRow scanned = mapper.label(taskId, request.boxCode().trim());
|
||||
if (scanned == null) throw new BizException(ResultCode.NOT_FOUND, "箱唛不属于当前一件代发入仓单");
|
||||
if (!StringUtils.hasText(scanned.getSkuCode())) throw conflict("箱唛缺少SKU,不能合托");
|
||||
if (mapper.operationCount(operationId) > 0) {
|
||||
return scanResult(scanned, request.cartonCount(), task, pallet);
|
||||
}
|
||||
List<LabelRow> labels = mapper.unboundLabels(taskId, task.getTenantId(), task.getInboundId(),
|
||||
scanned.getInboundDetailId(), scanned.getBoxCode(), request.cartonCount());
|
||||
if (labels.size() != request.cartonCount()) {
|
||||
throw conflict("本SKU剩余未上托箱数不足,请刷新任务后重试");
|
||||
}
|
||||
for (LabelRow label : labels) {
|
||||
mapper.insertCarton(IdWorker.getId(), taskId, palletId, task.getTenantId(), task.getInboundId(),
|
||||
label.getInboundDetailId(), label.getCartonLabelId(), label.getSkuCode(), label.getCargoName(),
|
||||
label.getBoxCode(), operationId, operatorId, operatorName);
|
||||
}
|
||||
if (mapper.addAllocated(taskId, operatorId, labels.size()) != 1) {
|
||||
throw conflict("任务箱数已变化,请刷新后重试");
|
||||
}
|
||||
TaskRow refreshed = requireTask(taskId, request.warehouseId());
|
||||
return scanResult(scanned, labels.size(), refreshed,
|
||||
mapper.pallet(taskId, palletId, request.warehouseId()));
|
||||
}
|
||||
@Transactional
|
||||
public CountResult countCarton(long taskId,CountCartonRequest request,long operatorId,String operatorName){
|
||||
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
||||
String operation=operation(request.operationId());
|
||||
CartonRow replay=mapper.cartonByCountOperation(operation);
|
||||
if(replay!=null){
|
||||
if(!Objects.equals(replay.getTaskId(),taskId) || !Objects.equals(replay.getBoxCode(),request.boxCode().trim())
|
||||
|| !Objects.equals(replay.getActualSkuQuantity(),request.actualSkuQuantity())) throw conflict("清点幂等号已用于不同操作");
|
||||
return countResult(task,replay);
|
||||
}
|
||||
LabelRow label=requireLabel(taskId,request.boxCode());
|
||||
if(mapper.countedByLabel(taskId,label.getCartonLabelId())!=null) throw conflict("箱码已清点,不能重复清点");
|
||||
int expected=requireExpectedQuantity(label);
|
||||
boolean mismatch=expected!=request.actualSkuQuantity();
|
||||
String remark=normalize(request.discrepancyRemark());
|
||||
if(!mismatch && request.discrepancyAction()!=DiscrepancyAction.MATCH) throw invalid("实收数量一致时差异动作必须为 MATCH");
|
||||
if(mismatch && request.discrepancyAction()!=DiscrepancyAction.ACCEPT_VARIANCE) throw conflict("实收数量存在差异,请确认差异处理结果");
|
||||
if(mismatch && !StringUtils.hasText(remark)) throw invalid("接受数量差异时必须填写处理说明");
|
||||
PalletRow pallet=StringUtils.hasText(request.palletNo())
|
||||
? getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName):null;
|
||||
long cartonId=IdWorker.getId();
|
||||
mapper.insertCarton(cartonId,taskId,pallet==null?null:pallet.getId(),task.getTenantId(),task.getInboundId(),
|
||||
label.getInboundDetailId(),label.getCartonLabelId(),label.getSkuCode(),label.getCargoName(),label.getBoxCode(),
|
||||
expected,request.actualSkuQuantity(),mismatch?"ACCEPTED_VARIANCE":"MATCH",remark,operation,operatorId,operatorName);
|
||||
if(mapper.addCounted(taskId,operatorId,pallet==null?0:1,mismatch?1:0)!=1) throw conflict("任务清点数量已变化,请刷新后重试");
|
||||
TaskRow refreshed=requireTask(taskId,request.warehouseId());
|
||||
return countResult(refreshed,mapper.carton(taskId,cartonId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Task seal(long taskId, long palletId, DropshipPalletizationDtos.SealPalletRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
|
||||
PalletRow replay = mapper.palletBySealOperation(request.operationId().trim());
|
||||
if (replay != null) {
|
||||
if (replay.getTaskId() != taskId || replay.getId() != palletId) {
|
||||
throw conflict("封托幂等号已用于其他托盘");
|
||||
}
|
||||
return view(requireTask(taskId, request.warehouseId()));
|
||||
}
|
||||
requireOpenPallet(taskId, palletId, request.warehouseId());
|
||||
String palletNo = numbers.next("DP");
|
||||
if (mapper.seal(taskId, palletId, request.expectedVersion(), palletNo, request.operationId().trim(),
|
||||
operatorId, operatorName) != 1) {
|
||||
throw conflict("空托盘、托盘状态或版本已变化,请刷新后重试");
|
||||
}
|
||||
TaskRow refreshed = requireTask(taskId, request.warehouseId());
|
||||
if (refreshed.getAllocatedCartons().equals(refreshed.getTotalCartons())
|
||||
&& mapper.openPalletCount(taskId) == 0) {
|
||||
if (mapper.completeTask(taskId) != 1
|
||||
|| mapper.completeInbound(taskId, refreshed.getTenantId(), refreshed.getInboundId(), operatorId) != 1) {
|
||||
throw conflict("合托完成状态已变化,请刷新后重试");
|
||||
}
|
||||
}
|
||||
return view(requireTask(taskId, request.warehouseId()));
|
||||
}
|
||||
@Transactional
|
||||
public Task bindPending(long taskId,BindPendingRequest request,long operatorId,String operatorName){
|
||||
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
||||
String operation=operation(request.operationId());
|
||||
if(mapper.bindOperationCount(taskId,operation)>0) return view(requireTask(taskId,request.warehouseId()));
|
||||
PalletRow pallet=getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName);
|
||||
int expected=mapper.unboundCount(taskId);
|
||||
if(expected==0) throw conflict("当前没有待绑托箱");
|
||||
int bound=mapper.bindPending(taskId,pallet.getId(),operation,operatorId,operatorName);
|
||||
if(bound!=expected || mapper.adjustBound(taskId,operatorId,bound)!=1) throw conflict("待绑托箱已变化,请刷新后重试");
|
||||
return view(requireTask(taskId,request.warehouseId()));
|
||||
}
|
||||
|
||||
public List<Pallet> inboundPallets(long tenantId, long inboundId) {
|
||||
String inboundCode = mapper.inboundCode(tenantId, inboundId);
|
||||
if (inboundCode == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
return mapper.palletsByInbound(tenantId, inboundId).stream()
|
||||
.map(row -> palletView(row, inboundCode)).toList();
|
||||
}
|
||||
@Transactional
|
||||
public Task moveCarton(long taskId,long cartonId,MoveCartonRequest request,long operatorId,String operatorName){
|
||||
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
||||
CartonRow carton=mapper.carton(taskId,cartonId);
|
||||
if(carton==null) throw new BizException(ResultCode.NOT_FOUND,"清点箱不存在");
|
||||
if(carton.getPalletId()!=null) requireOpenPallet(taskId,carton.getPalletId(),request.warehouseId());
|
||||
PalletRow target=StringUtils.hasText(request.targetPalletNo())
|
||||
? getOrCreatePallet(task,request.targetPalletNo().trim(),operation(request.operationId()),operatorId,operatorName):null;
|
||||
if(Objects.equals(carton.getPalletId(),target==null?null:target.getId())) return view(task);
|
||||
if(mapper.moveCarton(taskId,cartonId,target==null?null:target.getId(),operation(request.operationId()),operatorId,operatorName)!=1)
|
||||
throw conflict("清点箱已被其他操作修改,请刷新后重试");
|
||||
int delta=carton.getPalletId()==null?1:(target==null?-1:0);
|
||||
if(delta!=0 && mapper.adjustBound(taskId,operatorId,delta)!=1) throw conflict("任务绑托数量已变化,请刷新后重试");
|
||||
return view(requireTask(taskId,request.warehouseId()));
|
||||
}
|
||||
|
||||
private ScanResult scanResult(LabelRow label, int added, TaskRow task, PalletRow pallet) {
|
||||
return new ScanResult(label.getSkuCode(), label.getCargoName(), label.getInboundCode(), added,
|
||||
task.getAllocatedCartons(), task.getTotalCartons() - task.getAllocatedCartons(),
|
||||
palletView(pallet, task.getInboundCode()));
|
||||
}
|
||||
@Transactional
|
||||
public Task seal(long taskId,long palletId,SealPalletRequest request,long operatorId,String operatorName){
|
||||
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
||||
PalletRow replay=mapper.palletBySealOperation(operation(request.operationId()));
|
||||
if(replay!=null){ if(!Objects.equals(replay.getTaskId(),taskId)||!Objects.equals(replay.getId(),palletId)) throw conflict("封托幂等号已用于其他托盘"); return view(requireTask(taskId,request.warehouseId())); }
|
||||
requireOpenPallet(taskId,palletId,request.warehouseId());
|
||||
if(mapper.unboundCount(taskId)>0) throw conflict("仍有已清点待绑托箱,不能封托");
|
||||
if(mapper.seal(taskId,palletId,request.expectedVersion(),operation(request.operationId()),operatorId,operatorName)!=1)
|
||||
throw conflict("空托盘、托盘状态或版本已变化,请刷新后重试");
|
||||
TaskRow refreshed=requireTask(taskId,request.warehouseId());
|
||||
if(Objects.equals(refreshed.getCountedCartons(),refreshed.getTotalCartons()) &&
|
||||
Objects.equals(refreshed.getAllocatedCartons(),refreshed.getTotalCartons()) && mapper.openPalletCount(taskId)==0){
|
||||
if(mapper.completeTask(taskId)!=1 || mapper.completeInbound(taskId,refreshed.getTenantId(),refreshed.getInboundId(),operatorId)!=1)
|
||||
throw conflict("组托完成状态已变化,请刷新后重试");
|
||||
}
|
||||
return view(requireTask(taskId,request.warehouseId()));
|
||||
}
|
||||
|
||||
private Task view(TaskRow row) {
|
||||
return new Task(row.getId(), row.getInboundId(), row.getInboundCode(), row.getTenantId(),
|
||||
row.getCustomerName(), row.getWarehouseId(), row.getWarehouseName(), row.getTotalCartons(),
|
||||
row.getAllocatedCartons(), row.getTaskStatus(), row.getAssigneeId(), row.getAssigneeName(),
|
||||
row.getVersion(), mapper.pallets(row.getId()).stream()
|
||||
.map(pallet -> palletView(pallet, row.getInboundCode())).toList());
|
||||
}
|
||||
public List<Pallet> inboundPallets(long tenantId,long inboundId){
|
||||
String code=mapper.inboundCode(tenantId,inboundId);
|
||||
if(code==null) throw new BizException(ResultCode.NOT_FOUND,"入库单不存在");
|
||||
return mapper.palletsByInbound(tenantId,inboundId).stream().map(p->palletView(p,code)).toList();
|
||||
}
|
||||
|
||||
private Pallet palletView(PalletRow row, String inboundCode) {
|
||||
List<CartonRow> cartons = mapper.cartons(row.getId());
|
||||
Map<String, List<CartonRow>> groups = new LinkedHashMap<>();
|
||||
for (CartonRow carton : cartons) {
|
||||
groups.computeIfAbsent(carton.getInboundDetailId() + "|" + carton.getSkuCode(), ignored -> new ArrayList<>())
|
||||
.add(carton);
|
||||
}
|
||||
List<SkuItem> items = groups.values().stream().map(values -> {
|
||||
CartonRow first = values.get(0);
|
||||
return new SkuItem(first.getInboundId(), inboundCode, first.getInboundDetailId(), first.getSkuCode(),
|
||||
first.getCargoName(), values.size(), values.stream().map(CartonRow::getBoxCode).toList());
|
||||
}).toList();
|
||||
return new Pallet(row.getId(), row.getPalletNo(), row.getPalletStatus(), cartons.size(), items.size(),
|
||||
row.getVersion(), items, row.getCompletedAt());
|
||||
}
|
||||
private PalletRow getOrCreatePallet(TaskRow task,String palletNo,String operation,long operatorId,String operatorName){
|
||||
PalletRow existing=mapper.palletByNo(palletNo);
|
||||
if(existing!=null){
|
||||
if(!Objects.equals(existing.getTaskId(),task.getId()) || !Objects.equals(existing.getTenantId(),task.getTenantId())) throw conflict("托盘码已被其他客户或入仓任务占用");
|
||||
if(!"OPEN".equals(existing.getPalletStatus())) throw conflict("托盘已封托,不能继续绑定箱");
|
||||
return existing;
|
||||
}
|
||||
try{ mapper.insertPallet(IdWorker.getId(),task.getId(),task.getTenantId(),task.getInboundId(),task.getWarehouseId(),palletNo,operation,operatorId,operatorName); }
|
||||
catch(DuplicateKeyException e){ existing=mapper.palletByNo(palletNo); if(existing==null) throw conflict("托盘码已被并发占用"); }
|
||||
existing=mapper.palletByNo(palletNo);
|
||||
if(existing==null || !Objects.equals(existing.getTaskId(),task.getId())) throw conflict("托盘码已被其他任务占用");
|
||||
return existing;
|
||||
}
|
||||
|
||||
private TaskRow requireWorkingTask(long taskId, long warehouseId, long operatorId) {
|
||||
TaskRow task = requireTask(taskId, warehouseId);
|
||||
if (!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待合托任务");
|
||||
if (task.getAssigneeId() == null || task.getAssigneeId() != operatorId) throw conflict("任务不属于当前操作员");
|
||||
if (!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待合托状态");
|
||||
return task;
|
||||
}
|
||||
private CountResult countResult(TaskRow task,CartonRow carton){
|
||||
Pallet p=carton.getPalletId()==null?null:palletView(mapper.pallet(task.getId(),carton.getPalletId(),task.getWarehouseId()),task.getInboundCode());
|
||||
return new CountResult(cartonView(carton,task.getInboundCode(),false,null),task.getCountedCartons(),task.getAllocatedCartons(),
|
||||
task.getCountedCartons()-task.getAllocatedCartons(),task.getTotalCartons()-task.getCountedCartons(),p);
|
||||
}
|
||||
|
||||
private TaskRow requireTask(long taskId, long warehouseId) {
|
||||
TaskRow task = mapper.task(taskId, warehouseId);
|
||||
if (task == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的一件代发合托任务");
|
||||
return task;
|
||||
}
|
||||
private Task view(TaskRow row){
|
||||
return new Task(row.getId(),row.getInboundId(),row.getInboundCode(),row.getTenantId(),row.getCustomerName(),row.getWarehouseId(),row.getWarehouseName(),
|
||||
row.getTotalCartons(),row.getCountedCartons(),row.getAllocatedCartons(),row.getCountedCartons()-row.getAllocatedCartons(),row.getDiscrepancyCartons(),
|
||||
row.getTaskStatus(),row.getAssigneeId(),row.getAssigneeName(),row.getVersion(),mapper.pallets(row.getId()).stream().map(p->palletView(p,row.getInboundCode())).toList(),
|
||||
mapper.unboundCartons(row.getId()).stream().map(c->cartonView(c,row.getInboundCode(),false,null)).toList());
|
||||
}
|
||||
|
||||
private PalletRow requireOpenPallet(long taskId, long palletId, long warehouseId) {
|
||||
PalletRow pallet = mapper.pallet(taskId, palletId, warehouseId);
|
||||
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
|
||||
if (!"OPEN".equals(pallet.getPalletStatus())) throw conflict("托盘已完成,不能继续上托");
|
||||
return pallet;
|
||||
}
|
||||
private Pallet palletView(PalletRow row,String inboundCode){
|
||||
List<CartonRow> cartons=mapper.cartons(row.getId()); Map<String,List<CartonRow>> groups=new LinkedHashMap<>();
|
||||
for(CartonRow c:cartons) groups.computeIfAbsent(c.getInboundDetailId()+"|"+c.getSkuCode(),ignored->new ArrayList<>()).add(c);
|
||||
List<SkuItem> items=groups.values().stream().map(values->{CartonRow first=values.get(0);return new SkuItem(first.getInboundId(),inboundCode,
|
||||
first.getInboundDetailId(),first.getSkuCode(),first.getCargoName(),values.size(),values.stream().mapToInt(v->v.getActualSkuQuantity()==null?0:v.getActualSkuQuantity()).sum(),
|
||||
values.stream().map(CartonRow::getBoxCode).toList());}).toList();
|
||||
int discrepancies=(int)cartons.stream().filter(c->"ACCEPTED_VARIANCE".equals(c.getDiscrepancyStatus())).count();
|
||||
PalletMetrics metrics=mapper.metrics(row.getId());
|
||||
return new Pallet(row.getId(),row.getPalletNo(),row.getPalletStatus(),cartons.size(),items.size(),discrepancies,
|
||||
metrics==null?BigDecimal.ZERO:metrics.weightKg(),metrics==null?BigDecimal.ZERO:metrics.volumeCbm(),row.getVersion(),items,
|
||||
cartons.stream().map(c->cartonView(c,inboundCode,false,null)).toList(),row.getCompletedAt());
|
||||
}
|
||||
|
||||
private static String normalize(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
private static BizException invalid(String message) { return new BizException(ResultCode.VALIDATION_FAILED, message); }
|
||||
private Carton cartonView(CartonRow row,String inboundCode,boolean canCount,String blockReason){
|
||||
return new Carton(row.getId(),row.getBoxCode(),row.getInboundId(),inboundCode,row.getInboundDetailId(),row.getSkuCode(),row.getCargoName(),
|
||||
row.getExpectedSkuQuantity(),row.getActualSkuQuantity(),row.getDiscrepancyStatus(),row.getDiscrepancyRemark(),
|
||||
row.getPalletId()==null?"COUNTED_UNBOUND":"BOUND",row.getPalletId(),row.getPalletNo(),canCount,blockReason);
|
||||
}
|
||||
private LabelRow requireLabel(long taskId,String boxCode){ LabelRow label=mapper.label(taskId,boxCode.trim()); if(label==null) throw new BizException(ResultCode.NOT_FOUND,"箱码不属于当前一件代发入仓单"); if(!StringUtils.hasText(label.getSkuCode())) throw conflict("箱码缺少SKU快照,不能清点"); requireExpectedQuantity(label); return label; }
|
||||
private int requireExpectedQuantity(LabelRow label){ if(label.getUnitsPerCarton()==null||label.getUnitsPerCarton()<1) throw conflict("箱码缺少每箱SKU数量快照,不能清点"); return label.getUnitsPerCarton(); }
|
||||
private TaskRow requireWorkingTask(long taskId,long warehouseId,long operatorId){ TaskRow task=requireTask(taskId,warehouseId); if(!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待清点组托任务"); if(!Objects.equals(task.getAssigneeId(),operatorId)) throw conflict("任务不属于当前操作员"); if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待清点组托状态"); return task; }
|
||||
private TaskRow requireTask(long taskId,long warehouseId){ TaskRow task=mapper.task(taskId,warehouseId); if(task==null) throw new BizException(ResultCode.NOT_FOUND,"未找到当前仓库的一件代发清点组托任务"); return task; }
|
||||
private PalletRow requireOpenPallet(long taskId,long palletId,long warehouseId){ PalletRow p=mapper.pallet(taskId,palletId,warehouseId); if(p==null) throw new BizException(ResultCode.NOT_FOUND,"托盘不存在"); if(!"OPEN".equals(p.getPalletStatus())) throw conflict("托盘已封托,不能修改"); return p; }
|
||||
private static String operation(String v){String n=normalize(v);if(!StringUtils.hasText(n))throw invalid("操作幂等号不能为空");return n;}
|
||||
private static String normalize(String v){return StringUtils.hasText(v)?v.trim():null;}
|
||||
private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);}
|
||||
private static BizException invalid(String m){return new BizException(ResultCode.VALIDATION_FAILED,m);}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
&& cargoes.stream().anyMatch(cargo -> cargo.skuNo() == null || cargo.skuNo().isBlank())) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "一件代发货物明细缺少SKU,不能生成箱唛");
|
||||
}
|
||||
if ("ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())
|
||||
&& cargoes.stream().anyMatch(cargo -> cargo.unitsPerCarton() == null || cargo.unitsPerCarton() < 1)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "一件代发货物明细缺少每箱SKU数量,不能生成箱唛");
|
||||
}
|
||||
existing.forEach(label -> cargoes.stream()
|
||||
.filter(cargo -> java.util.Objects.equals(cargo.cargoId(), label.getInboundDetailId()))
|
||||
.findFirst().ifPresent(cargo -> repository.initializeLegacySku(tenantId, inboundId,
|
||||
@@ -211,6 +215,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
label.setCargoItemNo(cargo.itemNo());
|
||||
label.setCargoName(cargo.cargoName());
|
||||
label.setSkuCode(cargo.skuNo());
|
||||
label.setUnitsPerCarton(cargo.unitsPerCarton());
|
||||
label.setDetailPackageCount(cargo.packageCount());
|
||||
label.setInboundCode(inbound.getInboundCode());
|
||||
label.setBoxCode(inbound.getInboundCode() + "#" + total + "-" + index);
|
||||
@@ -267,6 +272,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
String source = cargoes.stream().map(cargo -> String.join("|", String.valueOf(cargo.itemNo()),
|
||||
cargo.cargoName() == null ? "" : cargo.cargoName().trim(),
|
||||
cargo.skuNo() == null ? "" : cargo.skuNo().trim(), String.valueOf(cargo.packageCount()),
|
||||
String.valueOf(cargo.unitsPerCarton()),
|
||||
decimalOrEmpty(cargo.lengthCm()), decimalOrEmpty(cargo.widthCm()), decimalOrEmpty(cargo.heightCm())))
|
||||
.collect(java.util.stream.Collectors.joining("\n"));
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PalletPickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||
@@ -106,6 +107,8 @@ public class LoadingPickService {
|
||||
private InboundPalletAllocationRepository palletAllocations;
|
||||
@Resource
|
||||
private InboundHoldGuard inboundHoldGuard;
|
||||
@Resource
|
||||
private com.baiyue.firstleg.mapper.DropshipPalletizationMapper dropshipPalletizationMapper;
|
||||
|
||||
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
||||
@Transactional
|
||||
@@ -326,6 +329,7 @@ public class LoadingPickService {
|
||||
if (item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
|
||||
}
|
||||
requirePackageUnit(item);
|
||||
if (repository.pick(itemId, request.pickedPackageCount(), request.version(), request.warehouseId()) != 1) {
|
||||
throw conflict("拣货数量超过计划箱数或明细版本已变化");
|
||||
}
|
||||
@@ -344,6 +348,41 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
|
||||
/** 一件代发只扫描已封托托盘码,并在同一事务把该托全部计划行结算为已拣。 */
|
||||
@Transactional
|
||||
public Task pickPallet(long taskId,PalletPickRequest request,long operatorId,String operatorName){
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId=operation(request.operationId());
|
||||
String requestHash=hash("PICK_PALLET|"+taskId+'|'+request.warehouseId()+'|'+request.palletNo()
|
||||
+'|'+request.expectedTaskVersion());
|
||||
Task replay=replayTaskOperation(taskId,operationId,requestHash,operatorId); if(replay!=null)return replay;
|
||||
LoadingPickTaskEntity task=requiredAssignedLoadingTaskForUpdate(taskId,operatorId);
|
||||
replay=replayTaskOperationForUpdate(taskId,operationId,requestHash,operatorId); if(replay!=null)return replay;
|
||||
if(!Objects.equals(task.getVersion(),request.expectedTaskVersion()))throw conflict("PDA任务版本已变化,请刷新后重试");
|
||||
List<LoadingPlanItemEntity> planItems=smartLoadingRepository.items(task.getLoadingPlanId()).stream()
|
||||
.filter(item->request.palletNo().equals(item.getPalletNo()) && item.getPalletId()!=null
|
||||
&& Boolean.TRUE.equals(item.getIndivisible())).toList();
|
||||
if(planItems.isEmpty())throw new BizException(ResultCode.NOT_FOUND,"托盘不属于当前装柜任务");
|
||||
Long palletId=planItems.get(0).getPalletId();
|
||||
if(planItems.stream().anyMatch(item->!Objects.equals(item.getPalletId(),palletId)))throw conflict("托盘装柜快照不一致");
|
||||
var physical=dropshipPalletizationMapper.sealedPalletLines(palletId);
|
||||
if(physical.isEmpty()||physical.stream().anyMatch(line->!"SEALED".equals(line.getPalletStatus())))throw conflict("托盘已失效或尚未封托");
|
||||
for(LoadingPlanItemEntity planItem:planItems){
|
||||
LoadingPickItemEntity pickItem=repository.itemByPlanItem(planItem.getId());
|
||||
if(pickItem==null||!"PENDING".equals(pickItem.getItemStatus())||count(pickItem.getPickedPackageCount())!=0)
|
||||
throw conflict("托盘已有部分作业或明细已变化,禁止拆托处理");
|
||||
if(repository.pick(pickItem.getId(),pickItem.getPlannedPackageCount(),pickItem.getVersion(),request.warehouseId())!=1)
|
||||
throw conflict("托盘明细版本已变化,请刷新后重试");
|
||||
insertHistory(taskId,pickItem.getId(),"PAL-"+hash(operationId+'|'+pickItem.getId()).substring(0,48),
|
||||
requestHash,"PICK_PALLET",pickItem.getPlannedPackageCount(),null,operatorId,operatorName);
|
||||
}
|
||||
startPickingIfNeeded(task,operatorId,operatorName);
|
||||
if(repository.touchTask(taskId,request.expectedTaskVersion())!=1)throw conflict("PDA任务版本已变化,请刷新后重试");
|
||||
insertOperation(taskId,operationId,requestHash,"PICK_PALLET",operatorId,operatorName);
|
||||
settleAndFinish(task,operatorId,operatorName); repository.syncInboundProjections(taskId,operatorId,operatorName);
|
||||
return view(requiredTask(taskId),request.warehouseId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Task partial(long taskId, long itemId, PartialPickRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
@@ -364,7 +403,8 @@ public class LoadingPickService {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
requirePackageUnit(item);
|
||||
if (repository.partial(itemId, request.finalPickedPackageCount(), request.version(),
|
||||
request.warehouseId()) != 1) {
|
||||
throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化");
|
||||
@@ -397,6 +437,7 @@ public class LoadingPickService {
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
requirePackageUnit(item);
|
||||
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
|
||||
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
|
||||
}
|
||||
@@ -443,6 +484,7 @@ public class LoadingPickService {
|
||||
|
||||
List<BatchLine> lines = validateBatchLines(task, request);
|
||||
validateInventorySnapshot(task, request, lines);
|
||||
lines.forEach(line -> requirePackageUnit(line.item()));
|
||||
for (BatchLine line : lines) {
|
||||
applyBatchLine(taskId, line, operationId, operatorId, operatorName);
|
||||
}
|
||||
@@ -1213,6 +1255,16 @@ public class LoadingPickService {
|
||||
/**
|
||||
* 逐项拣货后仅更新任务版本与投影,不能自动结算或推进装柜计划。
|
||||
*/
|
||||
private void requirePackageUnit(LoadingPickItemEntity item) {
|
||||
if (item.getLoadingPlanItemId() == null) return;
|
||||
LoadingPlanItemEntity planItem = smartLoadingRepository.item(
|
||||
requiredTask(item.getPickTaskId()).getLoadingPlanId(), item.getLoadingPlanItemId());
|
||||
if (planItem != null && (Boolean.TRUE.equals(planItem.getIndivisible())
|
||||
|| planItem.getPalletId() != null)) {
|
||||
throw conflict("一件代发已封托货物必须扫描托盘码整体拣货,不能按箱数处理");
|
||||
}
|
||||
}
|
||||
|
||||
private void touchSettleFinishAndSync(LoadingPickTaskEntity task, long operatorId, String operatorName) {
|
||||
if (repository.touchTask(task.getId(), task.getVersion()) != 1) {
|
||||
throw conflict("PDA任务版本已变化");
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.Carton;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.Pallet;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.SkuSummary;
|
||||
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper.CartonCandidate;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper.TaskContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** 一件代发现场打托旁路;不改变既有拣货数量和仓库资源结算。 */
|
||||
@Service
|
||||
public class LoadingWorkPalletService {
|
||||
@Resource private LoadingWorkPalletMapper pallets;
|
||||
@Resource private LoadingWorkPalletCartonMapper cartons;
|
||||
@Resource private InboundHoldGuard inboundHoldGuard;
|
||||
|
||||
@Transactional
|
||||
public Pallet open(long taskId, OpenRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
TaskContext context = requireWorkTask(taskId, request.warehouseId(), operatorId);
|
||||
String code = request.palletCode().trim();
|
||||
String operationId = request.operationId().trim();
|
||||
LoadingWorkPalletEntity replay = pallets.selectByOpenOperation(operationId);
|
||||
if (replay != null) {
|
||||
if (replay.getPickTaskId() != taskId || !replay.getPalletCode().equals(code)) {
|
||||
throw conflict("开托幂等号已用于其他请求");
|
||||
}
|
||||
return view(replay);
|
||||
}
|
||||
LoadingWorkPalletEntity value = new LoadingWorkPalletEntity();
|
||||
value.setPickTaskId(taskId);
|
||||
value.setLoadingPlanId(context.loadingPlanId());
|
||||
value.setWarehouseId(request.warehouseId());
|
||||
value.setWarehousePalletId(request.warehousePalletId());
|
||||
value.setPalletCode(code);
|
||||
value.setOpenOperationId(operationId);
|
||||
value.setPalletStatus("OPEN");
|
||||
value.setCreatedBy(operatorId);
|
||||
value.setCreatedByName(operatorName);
|
||||
value.setVersion(0);
|
||||
try {
|
||||
pallets.insert(value);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
replay = pallets.selectByOpenOperation(operationId);
|
||||
if (replay == null || replay.getPickTaskId() != taskId || !replay.getPalletCode().equals(code)) {
|
||||
throw conflict("托盘号已存在或开托操作重复");
|
||||
}
|
||||
value = replay;
|
||||
}
|
||||
return view(value);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Pallet scan(long taskId, long palletId, ScanRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
requireWorkTask(taskId, request.warehouseId(), operatorId);
|
||||
LoadingWorkPalletEntity target = requireOpenPallet(taskId, palletId, request.warehouseId());
|
||||
String operationId = request.operationId().trim();
|
||||
LoadingWorkPalletCartonEntity replay = cartons.selectByOperation(operationId);
|
||||
if (replay != null) {
|
||||
if (replay.getPickTaskId() != taskId || replay.getWorkPalletId() != palletId
|
||||
|| !replay.getBoxCode().equals(request.boxCode().trim())) {
|
||||
throw conflict("箱唛操作幂等号已用于其他请求");
|
||||
}
|
||||
return view(target);
|
||||
}
|
||||
List<CartonCandidate> candidates = cartons.selectCandidates(taskId, request.warehouseId(),
|
||||
request.boxCode().trim());
|
||||
if (candidates.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "箱唛不属于当前任务的一件代发货物");
|
||||
if (candidates.size() > 1) throw conflict("箱唛在当前多客户任务中不唯一,请检查箱唛数据");
|
||||
CartonCandidate candidate = candidates.get(0);
|
||||
if (candidate.getSkuCode() == null || candidate.getSkuCode().isBlank()) {
|
||||
throw conflict("一件代发箱唛缺少SKU,不能上托");
|
||||
}
|
||||
LoadingWorkPalletCartonEntity current = cartons.selectBinding(taskId, candidate.getCartonLabelId());
|
||||
if (current == null) {
|
||||
current = binding(taskId, palletId, candidate, operationId, operatorId, operatorName);
|
||||
try {
|
||||
cartons.insert(current);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
LoadingWorkPalletCartonEntity concurrent = cartons.selectBinding(taskId, candidate.getCartonLabelId());
|
||||
if (concurrent == null || concurrent.getWorkPalletId() != palletId) {
|
||||
throw conflict("箱唛已被其他托盘绑定");
|
||||
}
|
||||
}
|
||||
return view(target);
|
||||
}
|
||||
if (current.getWorkPalletId() == palletId) return view(target);
|
||||
LoadingWorkPalletEntity source = pallets.selectScoped(taskId, current.getWorkPalletId(), request.warehouseId());
|
||||
if (source == null || !"OPEN".equals(source.getPalletStatus())) {
|
||||
throw conflict("来源托盘已封托,不能直接移箱");
|
||||
}
|
||||
if (cartons.move(current.getId(), current.getVersion(), palletId, operatorId, operatorName, operationId) != 1) {
|
||||
throw conflict("箱唛绑定已变化,请刷新后重试");
|
||||
}
|
||||
return view(target);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Pallet seal(long taskId, long palletId, SealRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
requireWorkTask(taskId, request.warehouseId(), operatorId);
|
||||
LoadingWorkPalletEntity replay = pallets.selectBySealOperation(request.operationId().trim());
|
||||
if (replay != null) {
|
||||
if (replay.getId() != palletId || replay.getPickTaskId() != taskId) {
|
||||
throw conflict("封托幂等号已用于其他请求");
|
||||
}
|
||||
return view(replay);
|
||||
}
|
||||
requireOpenPallet(taskId, palletId, request.warehouseId());
|
||||
if (cartons.countPalletCartons(palletId) == 0) throw conflict("空托盘不能封托");
|
||||
if (cartons.countUnreconciled(taskId) > 0) {
|
||||
throw conflict("现场上托箱数超过已提交拣货箱数,请先完成原PDA拣货提交");
|
||||
}
|
||||
if (pallets.seal(taskId, palletId, request.warehouseId(), request.expectedVersion(),
|
||||
request.operationId().trim(), operatorId, operatorName) != 1) {
|
||||
throw conflict("托盘状态或版本已变化,请刷新后重试");
|
||||
}
|
||||
return view(pallets.selectScoped(taskId, palletId, request.warehouseId()));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Pallet> list(long taskId, long warehouseId) {
|
||||
TaskContext context = pallets.selectTaskContext(taskId, warehouseId);
|
||||
if (context == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的PDA任务");
|
||||
return pallets.selectTaskPallets(taskId, warehouseId).stream().map(this::view).toList();
|
||||
}
|
||||
|
||||
private TaskContext requireWorkTask(long taskId, long warehouseId, long operatorId) {
|
||||
TaskContext context = pallets.selectTaskContext(taskId, warehouseId);
|
||||
if (context == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的PDA任务");
|
||||
if (!"LOADING".equals(context.taskStatus())) throw conflict("PDA任务已完成,不能继续打托");
|
||||
if (context.dropshipItemCount() == null || context.dropshipItemCount() == 0) {
|
||||
throw conflict("当前任务没有一件代发货物");
|
||||
}
|
||||
if (context.assigneeId() == null || context.assigneeId() != operatorId) {
|
||||
throw conflict("请先领取PDA任务后再执行打托");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private LoadingWorkPalletEntity requireOpenPallet(long taskId, long palletId, long warehouseId) {
|
||||
LoadingWorkPalletEntity pallet = pallets.selectScoped(taskId, palletId, warehouseId);
|
||||
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "现场托盘不存在");
|
||||
if (!"OPEN".equals(pallet.getPalletStatus())) throw conflict("托盘不是打托中状态");
|
||||
return pallet;
|
||||
}
|
||||
|
||||
private static LoadingWorkPalletCartonEntity binding(long taskId, long palletId,
|
||||
CartonCandidate source, String operationId,
|
||||
long operatorId, String operatorName) {
|
||||
LoadingWorkPalletCartonEntity value = new LoadingWorkPalletCartonEntity();
|
||||
value.setPickTaskId(taskId);
|
||||
value.setWorkPalletId(palletId);
|
||||
value.setLoadingPlanItemId(source.getLoadingPlanItemId());
|
||||
value.setPickItemId(source.getPickItemId());
|
||||
value.setTenantId(source.getTenantId());
|
||||
value.setInboundId(source.getInboundId());
|
||||
value.setInboundDetailId(source.getInboundDetailId());
|
||||
value.setCartonLabelId(source.getCartonLabelId());
|
||||
value.setBoxCode(source.getBoxCode());
|
||||
value.setSkuCode(source.getSkuCode());
|
||||
value.setUnitsPerCarton(source.getUnitsPerCarton());
|
||||
value.setBoundBy(operatorId);
|
||||
value.setBoundByName(operatorName);
|
||||
value.setOperationId(operationId);
|
||||
value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
|
||||
private Pallet view(LoadingWorkPalletEntity pallet) {
|
||||
List<LoadingWorkPalletCartonEntity> values = cartons.selectPalletCartons(pallet.getId());
|
||||
Map<String, List<LoadingWorkPalletCartonEntity>> bySku = values.stream().collect(
|
||||
java.util.stream.Collectors.groupingBy(LoadingWorkPalletCartonEntity::getSkuCode,
|
||||
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||
List<SkuSummary> skus = bySku.entrySet().stream().map(entry -> new SkuSummary(entry.getKey(),
|
||||
entry.getValue().size(), entry.getValue().stream().map(LoadingWorkPalletCartonEntity::getUnitsPerCarton)
|
||||
.filter(Objects::nonNull).mapToLong(Integer::longValue).sum())).toList();
|
||||
List<Carton> cartonViews = values.stream().map(value -> new Carton(value.getId(), value.getBoxCode(),
|
||||
value.getSkuCode(), value.getTenantId(), value.getInboundId(), value.getInboundDetailId(),
|
||||
value.getUnitsPerCarton())).toList();
|
||||
return new Pallet(pallet.getId(), pallet.getPalletCode(), pallet.getWarehousePalletId(),
|
||||
pallet.getPalletStatus(), pallet.getVersion(), values.size(), bySku.size(),
|
||||
(int) values.stream().map(LoadingWorkPalletCartonEntity::getTenantId).distinct().count(),
|
||||
skus, cartonViews);
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
|
||||
@Service
|
||||
public class MerchantInboundBillingService {
|
||||
private static final Set<String> PUBLISHED_BILLING_PROCESS_STATUSES = Set.of(
|
||||
InboundProcessStatus.WAIT_PALLETIZE,
|
||||
InboundProcessStatus.WAIT_SPLIT,
|
||||
InboundProcessStatus.LOADING,
|
||||
InboundProcessStatus.LOADED,
|
||||
|
||||
@@ -54,6 +54,7 @@ public class SmartLoadingService {
|
||||
"OA_NEW_YORK_TRUCK", Set.of("OA_NEW_YORK_TRUCK")
|
||||
);
|
||||
@Resource private SmartLoadingRepository repository;
|
||||
@Resource private com.baiyue.firstleg.mapper.DropshipPalletizationMapper dropshipPallets;
|
||||
@Resource private OrderCargoClient orderCargoClient;
|
||||
@Resource private PendingReviewOrderClient pendingReviewOrders;
|
||||
@Resource private InboundOrderCreationService inboundOrderCreationService;
|
||||
@@ -111,7 +112,7 @@ public class SmartLoadingService {
|
||||
Map<Long, List<Candidate>> grouped = candidates(bookingId, warehouseId).stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
|
||||
java.util.stream.Collectors.toList()));
|
||||
return grouped.values().stream().map(this::inboundCandidate).toList();
|
||||
return grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
|
||||
}
|
||||
|
||||
private List<InboundCandidate> recommendationCandidates(ContainerBookingEntity booking) {
|
||||
@@ -121,7 +122,7 @@ public class SmartLoadingService {
|
||||
repository.planByBooking(booking.getId()), values).stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
|
||||
java.util.stream.Collectors.toList()));
|
||||
return grouped.values().stream().map(this::inboundCandidate).toList();
|
||||
return grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
|
||||
}
|
||||
|
||||
/** Global workbench candidates. A co-load group is one paging unit and can never be split across pages. */
|
||||
@@ -175,7 +176,7 @@ public class SmartLoadingService {
|
||||
List<InboundCandidate> all = repository.globalCandidatesByUnits(unitPage.records()).stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
|
||||
java.util.stream.Collectors.toList()))
|
||||
.values().stream().map(this::inboundCandidate).toList();
|
||||
.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
|
||||
Map<String, InboundCandidate> units = new HashMap<>();
|
||||
Map<Long, List<InboundCandidate>> grouped = all.stream()
|
||||
.filter(candidate -> candidate.coLoadGroupId() != null)
|
||||
@@ -216,6 +217,7 @@ public class SmartLoadingService {
|
||||
new InboundCandidate(row.customerId(), null, row.orderId(), row.orderNo(), row.inboundCode(),
|
||||
row.warehouseId(), row.channelCode(), row.channelName(), null, null, null,
|
||||
row.customerName(), null, row.totalPackageCount(), null, null, null, null,
|
||||
"PACKAGE", null, null, false,
|
||||
null, null, null, null, false, true, null,
|
||||
"PENDING_REVIEW", row.expectedArrivalAt(), List.of())).toList();
|
||||
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages());
|
||||
@@ -266,7 +268,7 @@ public class SmartLoadingService {
|
||||
return repository.workbenchContainers(query, actualChannels(query.channelGroupCodes()));
|
||||
}
|
||||
|
||||
private InboundCandidate inboundCandidate(List<Candidate> details) {
|
||||
private List<InboundCandidate> inboundCandidates(List<Candidate> details) {
|
||||
Candidate first = details.get(0);
|
||||
int packages = details.stream().map(Candidate::availablePackageCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
@@ -279,13 +281,23 @@ public class SmartLoadingService {
|
||||
BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
|
||||
String goods = details.stream().map(Candidate::goodsName).filter(StringUtils::hasText)
|
||||
.map(String::trim).distinct().collect(java.util.stream.Collectors.joining("、"));
|
||||
return new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
|
||||
List<PalletCandidate> pallets=dropshipPallets.palletCandidates(first.inboundId());
|
||||
if(!pallets.isEmpty()) return pallets.stream().map(pallet -> new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
|
||||
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
|
||||
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
|
||||
first.customerName(), goods, pallet.cartonCount(),
|
||||
pallet.cartonCount()==null||pallet.cartonCount()==0?null:pallet.weightKg().divide(BigDecimal.valueOf(pallet.cartonCount()),6,RoundingMode.HALF_UP),
|
||||
first.unitNetWeightKg(), pallet.cartonCount()==null||pallet.cartonCount()==0?null:pallet.volumeCbm().divide(BigDecimal.valueOf(pallet.cartonCount()),6,RoundingMode.HALF_UP),
|
||||
pallet.volumeCbm(),"SEALED_PALLET",pallet.palletId(),pallet.palletNo(),true,
|
||||
first.coLoadGroupId(), first.groupNo(), first.memberCount(),first.groupTotalBoxes(), first.locked(), canAssign(first), capabilityBlock(first),
|
||||
first.orderStatusSnapshot(),first.expectedArrivalAt(),List.of())).toList();
|
||||
return List.of(new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
|
||||
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
|
||||
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
|
||||
first.customerName(), goods, packages, first.unitGrossWeightKg(), first.unitNetWeightKg(),
|
||||
averageVolume, volume, first.coLoadGroupId(), first.groupNo(), first.memberCount(),
|
||||
averageVolume, volume,"PACKAGE",null,null,false, first.coLoadGroupId(), first.groupNo(), first.memberCount(),
|
||||
first.groupTotalBoxes(), first.locked(), canAssign(first), capabilityBlock(first), first.orderStatusSnapshot(),
|
||||
first.expectedArrivalAt(), List.of());
|
||||
first.expectedArrivalAt(), List.of()));
|
||||
}
|
||||
|
||||
private InboundCandidate globalGroupCandidate(List<InboundCandidate> availableMembers) {
|
||||
@@ -317,7 +329,7 @@ public class SmartLoadingService {
|
||||
return new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
|
||||
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
|
||||
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
|
||||
customers, goods, packages, gross, net, averageVolume, volume, metadata.coLoadGroupId(), metadata.groupNo(),
|
||||
customers, goods, packages, gross, net, averageVolume, volume,"PACKAGE",null,null,false, metadata.coLoadGroupId(), metadata.groupNo(),
|
||||
metadata.memberCount(), metadata.groupTotalBoxes(),
|
||||
occupied,
|
||||
ordered.stream().allMatch(candidate -> Boolean.TRUE.equals(candidate.canAssign())),
|
||||
@@ -418,11 +430,52 @@ public class SmartLoadingService {
|
||||
@BusinessAudit(event = "SMART_LOADING_ITEM_ASSIGN", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "分配智能装柜明细")
|
||||
public Plan assign(long planId, AssignRequest request, long operatorId, String operatorName) {
|
||||
return assign(planId, request, operatorId, operatorName, false);
|
||||
return assign(planId, request, operatorId, operatorName, false, null, null, null, false, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_PALLET_ASSIGN", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "按已封托托盘分配智能装柜明细")
|
||||
public Plan assignPallet(long planId, AssignPalletRequest request, long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("ASSIGN_PALLET|" + planId + '|' + request.palletId() + '|'
|
||||
+ request.expectedVersion());
|
||||
Plan replay = replay(operation, planId, "ADD_PALLET", null, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity original = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
List<com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletLine> lines =
|
||||
dropshipPallets.sealedPalletLines(request.palletId());
|
||||
if (lines.isEmpty() || lines.stream().anyMatch(line -> !"SEALED".equals(line.getPalletStatus()))) {
|
||||
throw conflict("托盘不存在或尚未封托");
|
||||
}
|
||||
var first = lines.get(0);
|
||||
inboundHoldGuard.requireInboundNotHeld(first.getAccountId(), first.getInboundId());
|
||||
if (!repository.activeItemsByPallet(request.palletId()).isEmpty()) throw conflict("托盘已被有效柜次占用");
|
||||
int sequence = 0;
|
||||
for (var line : lines) {
|
||||
LoadingPlanEntity current = repository.plan(planId);
|
||||
Candidate candidate = new Candidate(line.getAccountId(), line.getInboundId(), line.getInboundDetailId(),
|
||||
line.getOrderId(), line.getOrderNo(), line.getInboundCode(), line.getWarehouseId(), line.getChannelCode(),
|
||||
line.getChannelName(), line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(),
|
||||
line.getDestinationWarehouseName(), line.getCustomerName(), line.getCargoName(), line.getCartonCount(),
|
||||
line.getUnitGrossWeightKg(), line.getUnitNetWeightKg(), line.getUnitVolumeCbm(), null, null, null, null,
|
||||
false, null, "WAIT_SPLIT", null);
|
||||
String childOperation = "PAL-" + sha256(operation + '|' + line.getInboundDetailId()).substring(0, 48);
|
||||
assign(planId, new AssignRequest(line.getInboundId(), line.getInboundDetailId(), line.getCartonCount(),
|
||||
current.getVersion(), childOperation), operatorId, operatorName, true, request.palletId(),
|
||||
line.getPalletNo(), line.getActualSkuQuantity(), sequence++ == 0, candidate);
|
||||
}
|
||||
LoadingPlanEntity assigned = repository.plan(planId);
|
||||
history(planId, null, "ADD_PALLET", original.getPlanStatus(), assigned.getPlanStatus(), operation,
|
||||
json(values("palletId", request.palletId(), "version", request.expectedVersion())),
|
||||
json(values("requestHash", requestHash, "palletNo", first.getPalletNo(), "lineCount", lines.size())),
|
||||
operatorId, operatorName);
|
||||
return view(assigned, requireBooking(assigned.getContainerBookingId()));
|
||||
}
|
||||
|
||||
private Plan assign(long planId, AssignRequest request, long operatorId, String operatorName,
|
||||
boolean wholeGroupAssignment) {
|
||||
boolean wholeGroupAssignment, Long palletId, String palletNo,
|
||||
Integer actualSkuQuantity, boolean primaryPalletLine, Candidate frozenCandidate) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("ASSIGN|" + planId + '|'
|
||||
+ request.inboundId() + '|' + request.inboundDetailId() + '|' + request.packageCount()
|
||||
@@ -431,8 +484,14 @@ public class SmartLoadingService {
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
Candidate candidate = repository.candidate(planId, request.inboundId(), request.inboundDetailId());
|
||||
Candidate candidate = frozenCandidate == null
|
||||
? repository.candidate(planId, request.inboundId(), request.inboundDetailId()) : frozenCandidate;
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
InboundOrderEntity candidateInbound = repository.inbound(candidate.accountId(), candidate.inboundId());
|
||||
if (!wholeGroupAssignment && candidateInbound != null
|
||||
&& "ONE_CLICK_DROPSHIP".equals(candidateInbound.getFulfillmentType())) {
|
||||
throw conflict("一件代发必须按已封托托盘整体加入,不能按箱数拆分");
|
||||
}
|
||||
inboundHoldGuard.requireInboundNotHeld(candidate.accountId(), candidate.inboundId());
|
||||
if (!wholeGroupAssignment) requireStandalone(candidate.accountId(), candidate.inboundId());
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
@@ -445,7 +504,9 @@ public class SmartLoadingService {
|
||||
operatorId, operatorName);
|
||||
}
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
|
||||
LoadingPlanItemEntity existing = repository.activeItem(planId, candidate.accountId(), request.inboundDetailId());
|
||||
LoadingPlanItemEntity existing = palletId == null
|
||||
? repository.activeItem(planId, candidate.accountId(), request.inboundDetailId())
|
||||
: repository.activePalletItem(planId, palletId, request.inboundDetailId());
|
||||
int total = request.packageCount() + (existing == null ? 0 : existing.getPackageCount());
|
||||
int availableIncludingCurrent = candidate.availablePackageCount()
|
||||
+ (existing == null ? 0 : existing.getPackageCount());
|
||||
@@ -457,6 +518,14 @@ public class SmartLoadingService {
|
||||
LoadingPlanItemEntity changed;
|
||||
if (existing == null) {
|
||||
LoadingPlanItemEntity item = snapshot(planId, candidate, cargo, request.packageCount(), operatorId, operatorName, lines.size() + 1);
|
||||
if (palletId != null) {
|
||||
item.setUnitType("SEALED_PALLET");
|
||||
item.setPalletId(palletId);
|
||||
item.setPalletNo(palletNo);
|
||||
item.setIndivisible(true);
|
||||
item.setPalletCount(primaryPalletLine ? 1 : 0);
|
||||
item.setProductQuantity(actualSkuQuantity);
|
||||
}
|
||||
try { repository.insert(item); } catch (DuplicateKeyException e) { throw conflict("货物明细已被并发锁定"); }
|
||||
changed = item;
|
||||
} else {
|
||||
@@ -617,7 +686,7 @@ public class SmartLoadingService {
|
||||
.substring(0, 48);
|
||||
assign(planId, new AssignRequest(request.inboundId(), allocation.candidate().inboundDetailId(),
|
||||
allocation.packageCount(), current.getVersion(), childOperation), operatorId, operatorName,
|
||||
wholeGroupAssignment);
|
||||
wholeGroupAssignment, null, null, null, false, null);
|
||||
sequence++;
|
||||
}
|
||||
LoadingPlanEntity assigned = repository.plan(planId);
|
||||
@@ -859,6 +928,9 @@ public class SmartLoadingService {
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||
if (Boolean.TRUE.equals(item.getIndivisible()) || item.getPalletId() != null) {
|
||||
throw conflict("已封托托盘不可调整箱数,只能整托移除");
|
||||
}
|
||||
inboundHoldGuard.requireInboundNotHeld(item.getAccountId(), item.getInboundId());
|
||||
requireStandalone(item.getAccountId(), item.getInboundId());
|
||||
if (!Objects.equals(item.getVersion(), request.expectedItemVersion())) throw conflict("装柜明细版本冲突");
|
||||
@@ -971,6 +1043,9 @@ public class SmartLoadingService {
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||
if (Boolean.TRUE.equals(item.getIndivisible()) || item.getPalletId() != null) {
|
||||
throw conflict("已封托托盘不可删除单条明细,只能整托移除");
|
||||
}
|
||||
requireStandalone(item.getAccountId(), item.getInboundId());
|
||||
loadingPickService.remove(itemId);
|
||||
touch(plan, operatorId, operatorName);
|
||||
@@ -982,6 +1057,34 @@ public class SmartLoadingService {
|
||||
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_PALLET_REMOVE", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "整托移除一件代发装柜明细")
|
||||
public Plan removePallet(long planId,long palletId,RemovePalletRequest request,long operatorId,String operatorName){
|
||||
String operation=operation(request.operationId());
|
||||
String requestHash=sha256("REMOVE_PALLET|"+planId+'|'+palletId+'|'+request.expectedPlanVersion());
|
||||
Plan replay=replay(operation,planId,"REMOVE_PALLET",null,requestHash); if(replay!=null)return replay;
|
||||
LoadingPlanEntity plan=editing(planId,request.expectedPlanVersion(),operatorId,operatorName);
|
||||
List<LoadingPlanItemEntity> palletItems=repository.activeItemsByPallet(palletId).stream()
|
||||
.filter(item->Objects.equals(item.getLoadingPlanId(),planId)).toList();
|
||||
if(palletItems.isEmpty())throw new BizException(ResultCode.NOT_FOUND,"装柜计划中不存在该托盘");
|
||||
LoadingPickService.MutationSnapshot picks=loadingPickService.mutationSnapshot(planId);
|
||||
if(palletItems.stream().anyMatch(item->picks.pickedByPlanItem().getOrDefault(item.getId(),0)>0))
|
||||
throw conflict("托盘已发生实物拣货,不能移除");
|
||||
for(LoadingPlanItemEntity item:palletItems)loadingPickService.remove(item.getId());
|
||||
touch(plan,operatorId,operatorName);
|
||||
for(LoadingPlanItemEntity item:palletItems){
|
||||
if(repository.remove(planId,item.getId(),item.getVersion(),operatorId,operatorName)!=1)
|
||||
throw conflict("整托装柜明细已被并发修改");
|
||||
}
|
||||
LoadingPlanItemEntity first=palletItems.get(0);
|
||||
loadingModelBridge.removeIfUnallocated(planId,first.getAccountId(),first.getInboundId());
|
||||
history(planId,null,"REMOVE_PALLET",plan.getPlanStatus(),plan.getPlanStatus(),operation,
|
||||
json(values("palletId",palletId,"version",request.expectedPlanVersion())),
|
||||
json(values("requestHash",requestHash,"removedLineCount",palletItems.size())),operatorId,operatorName);
|
||||
return view(repository.plan(planId),requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_UNPICKED_REMOVE", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "批量移除智能装柜未拣明细")
|
||||
@@ -1461,7 +1564,40 @@ public class SmartLoadingService {
|
||||
: "COMPLETED".equals(task.taskStatus()) ? "COMPLETED" : "IN_PROGRESS";
|
||||
return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(),
|
||||
b.getContainerNo(),b.getVoyageNo(),p.getVersion(),mutable,mutable,blockedReason,finalStatus,
|
||||
task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),items);
|
||||
task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),collapsePalletUnits(items));
|
||||
}
|
||||
private List<LoadingPlanItemEntity> collapsePalletUnits(List<LoadingPlanItemEntity> items) {
|
||||
List<LoadingPlanItemEntity> result = new ArrayList<>();
|
||||
Map<Long,List<LoadingPlanItemEntity>> pallets = items.stream().filter(i -> i.getPalletId() != null)
|
||||
.collect(java.util.stream.Collectors.groupingBy(LoadingPlanItemEntity::getPalletId,
|
||||
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||
items.stream().filter(i -> i.getPalletId() == null).forEach(result::add);
|
||||
for (List<LoadingPlanItemEntity> lines : pallets.values()) {
|
||||
LoadingPlanItemEntity unit = lines.get(0);
|
||||
unit.setPackageCount(lines.stream().map(LoadingPlanItemEntity::getPackageCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
|
||||
unit.setProductQuantity(lines.stream().map(LoadingPlanItemEntity::getProductQuantity)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
|
||||
unit.setPalletCount(1);
|
||||
unit.setGrossWeightKg(sumDecimal(lines, LoadingPlanItemEntity::getGrossWeightKg));
|
||||
unit.setNetWeightKg(sumDecimal(lines, LoadingPlanItemEntity::getNetWeightKg));
|
||||
unit.setVolumeCbm(sumDecimal(lines, LoadingPlanItemEntity::getVolumeCbm));
|
||||
unit.setPickedPackageCount(lines.stream().map(LoadingPlanItemEntity::getPickedPackageCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
|
||||
unit.setTotalPackageCount(unit.getPackageCount());
|
||||
unit.setRemainingPackageCount(0);
|
||||
unit.setCanAdjust(false);
|
||||
unit.setCanRemove(lines.stream().allMatch(i -> Boolean.TRUE.equals(i.getCanRemove())));
|
||||
result.add(unit);
|
||||
}
|
||||
return result.stream().sorted(Comparator
|
||||
.comparing(LoadingPlanItemEntity::getLoadSequence, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(LoadingPlanItemEntity::getId, Comparator.nullsLast(Long::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
private static BigDecimal sumDecimal(List<LoadingPlanItemEntity> lines,
|
||||
java.util.function.Function<LoadingPlanItemEntity,BigDecimal> getter) {
|
||||
return lines.stream().map(getter).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
private int adjustmentTotal(LoadingPlanItemEntity item) {
|
||||
int current = item.getPackageCount() == null ? 0 : item.getPackageCount();
|
||||
|
||||
@@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
||||
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||
sku_code VARCHAR(128) NULL COMMENT '箱唛绑定的SKU快照;二维码内容不变',
|
||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱SKU数量快照;一件代发必须为正数',
|
||||
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
||||
|
||||
@@ -209,6 +209,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
||||
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||
sku_code VARCHAR(128) NULL COMMENT '箱唛绑定的SKU快照;二维码内容不变',
|
||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱SKU数量快照;一件代发必须为正数',
|
||||
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
||||
@@ -920,7 +921,9 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet_task (
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
||||
task_status VARCHAR(24) NOT NULL COMMENT '任务状态:PENDING待领取、CLAIMED已领取、COMPLETED已完成',
|
||||
total_cartons INT UNSIGNED NOT NULL COMMENT '测量确认总箱数',
|
||||
counted_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已逐箱确认实收数量的箱数',
|
||||
allocated_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已上托箱数',
|
||||
discrepancy_cartons INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已确认差异箱数',
|
||||
assignee_id BIGINT UNSIGNED NULL COMMENT '任务领取人ID',
|
||||
assignee_name VARCHAR(64) NULL COMMENT '任务领取人姓名快照',
|
||||
claimed_at DATETIME(3) NULL COMMENT '任务领取时间',
|
||||
@@ -933,7 +936,7 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet_task (
|
||||
KEY idx_dropship_task_operator (task_status,assignee_id,warehouse_id,created_at),
|
||||
CONSTRAINT fk_dropship_task_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order(id),
|
||||
CONSTRAINT chk_dropship_task_status CHECK (task_status IN ('PENDING','CLAIMED','COMPLETED')),
|
||||
CONSTRAINT chk_dropship_task_cartons CHECK (allocated_cartons<=total_cartons)
|
||||
CONSTRAINT chk_dropship_task_cartons CHECK (allocated_cartons<=counted_cartons AND counted_cartons<=total_cartons)
|
||||
) ENGINE=InnoDB COMMENT='一件代发测量后PDA合托任务';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
|
||||
@@ -942,8 +945,8 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '一件代发入仓单ID',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
||||
pallet_no VARCHAR(32) NULL COMMENT '封托时生成的DP托盘号',
|
||||
pallet_status VARCHAR(16) NOT NULL COMMENT '托盘状态:OPEN合托中、COMPLETED已封托',
|
||||
pallet_no VARCHAR(32) NOT NULL COMMENT '现场扫描的系统DP托盘码',
|
||||
pallet_status VARCHAR(16) NOT NULL COMMENT '托盘状态:OPEN组托中、SEALED已封托',
|
||||
open_operation_id VARCHAR(128) NOT NULL COMMENT '创建托盘幂等操作ID',
|
||||
seal_operation_id VARCHAR(128) NULL COMMENT '封托幂等操作ID',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '托盘创建人ID',
|
||||
@@ -960,29 +963,39 @@ CREATE TABLE IF NOT EXISTS fl_dropship_pallet (
|
||||
UNIQUE KEY uk_dropship_pallet_seal_op (seal_operation_id),
|
||||
KEY idx_dropship_pallet_inbound (tenant_id,inbound_id,pallet_status),
|
||||
CONSTRAINT fk_dropship_pallet_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
|
||||
CONSTRAINT chk_dropship_pallet_status CHECK (pallet_status IN ('OPEN','COMPLETED'))
|
||||
CONSTRAINT chk_dropship_pallet_status CHECK (pallet_status IN ('OPEN','SEALED'))
|
||||
) ENGINE=InnoDB COMMENT='一件代发合托形成的可打印托盘';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_dropship_pallet_carton (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '托盘箱明细ID',
|
||||
task_id BIGINT UNSIGNED NOT NULL COMMENT '所属合托任务ID',
|
||||
pallet_id BIGINT UNSIGNED NOT NULL COMMENT '所属合托托盘ID',
|
||||
pallet_id BIGINT UNSIGNED NULL COMMENT '所属组托托盘ID;已清点待绑托时为空',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离键',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '一件代发入仓单ID',
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '入仓货物明细ID',
|
||||
carton_label_id BIGINT UNSIGNED NOT NULL COMMENT '稳定箱唛记录ID',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT '箱唛冻结SKU编码',
|
||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||
box_code VARCHAR(64) NOT NULL COMMENT '稳定箱码',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '上托幂等操作ID',
|
||||
bound_by BIGINT UNSIGNED NOT NULL COMMENT '上托操作人ID',
|
||||
box_code VARCHAR(80) NOT NULL COMMENT '稳定箱码',
|
||||
expected_sku_quantity INT UNSIGNED NOT NULL COMMENT '箱唛冻结的应收SKU数量',
|
||||
actual_sku_quantity INT UNSIGNED NOT NULL COMMENT 'PDA确认的实收SKU数量',
|
||||
discrepancy_status VARCHAR(24) NOT NULL COMMENT '差异状态:MATCH或ACCEPTED_VARIANCE',
|
||||
discrepancy_remark VARCHAR(500) NULL COMMENT '差异接受说明',
|
||||
count_operation_id VARCHAR(128) NOT NULL COMMENT '逐箱清点幂等操作ID',
|
||||
bind_operation_id VARCHAR(128) NULL COMMENT '批量绑托幂等操作ID',
|
||||
last_operation_id VARCHAR(128) NOT NULL COMMENT '最后一次绑托或换托操作幂等号',
|
||||
counted_by BIGINT UNSIGNED NOT NULL COMMENT '清点操作人ID',
|
||||
counted_by_name VARCHAR(64) NULL COMMENT '清点操作人姓名快照',
|
||||
counted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '清点时间',
|
||||
bound_by BIGINT UNSIGNED NULL COMMENT '上托操作人ID',
|
||||
bound_by_name VARCHAR(64) NULL COMMENT '上托操作人姓名快照',
|
||||
bound_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '上托时间',
|
||||
bound_at DATETIME(3) NULL COMMENT '上托时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_dropship_carton_task_label (task_id,carton_label_id),
|
||||
KEY idx_dropship_carton_pallet_sku (pallet_id,sku_code,id),
|
||||
KEY idx_dropship_carton_inbound (tenant_id,inbound_id,pallet_id),
|
||||
KEY idx_dropship_carton_operation (operation_id),
|
||||
UNIQUE KEY uk_dropship_carton_count_operation (count_operation_id),
|
||||
KEY idx_dropship_carton_bind_operation (task_id,bind_operation_id),
|
||||
CONSTRAINT fk_dropship_carton_task FOREIGN KEY (task_id) REFERENCES fl_dropship_pallet_task(id),
|
||||
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)
|
||||
|
||||
@@ -267,6 +267,10 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
product_image_url VARCHAR(1000) NULL COMMENT '产品图片访问地址快照',
|
||||
package_count INT UNSIGNED NOT NULL COMMENT '本次装柜箱数',
|
||||
product_quantity INT UNSIGNED NULL COMMENT '产品总数量',
|
||||
unit_type VARCHAR(24) NOT NULL DEFAULT 'PACKAGE' COMMENT '分配单元:PACKAGE普通箱、SEALED_PALLET一件代发封托',
|
||||
pallet_id BIGINT UNSIGNED NULL COMMENT '一件代发封托ID;普通箱为空',
|
||||
pallet_no VARCHAR(32) NULL COMMENT '一件代发系统托盘码快照',
|
||||
indivisible TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为不可拆整托单元',
|
||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱产品数',
|
||||
pallet_count INT UNSIGNED NULL COMMENT '托盘数',
|
||||
gross_weight_kg DECIMAL(18,3) NULL COMMENT '本次装柜毛重kg',
|
||||
@@ -287,7 +291,8 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
removed_by_name VARCHAR(64) NULL COMMENT '推送前逻辑删除操作人名称快照',
|
||||
removed_at DATETIME(3) NULL COMMENT '推送前逻辑删除时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '推送前逻辑删除标记;冻结后不可修改',
|
||||
active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END) STORED COMMENT '未删除明细唯一锁;删除后返回NULL释放分配资格',
|
||||
active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END) STORED COMMENT '未删除明细查询键;删除后返回NULL',
|
||||
active_assignment_key VARCHAR(96) GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN CONCAT(CASE WHEN pallet_id IS NULL THEN 'D:0' ELSE CONCAT('P:', pallet_id) END, ':', inbound_detail_id) ELSE NULL END) STORED COMMENT '普通明细或已封托托盘明细的有效唯一锁',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
@@ -296,12 +301,13 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '装柜明细最后更新时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_item_plan_detail (loading_plan_id, active_inbound_detail_id),
|
||||
UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id, active_assignment_key),
|
||||
KEY idx_loading_item_active_detail (account_id, active_inbound_detail_id),
|
||||
KEY idx_loading_item_plan (loading_plan_id, deleted, load_sequence, id),
|
||||
KEY idx_loading_item_account (account_id, created_at, id),
|
||||
KEY idx_loading_item_inbound (account_id, inbound_id, deleted),
|
||||
KEY idx_loading_item_candidate (account_id, inbound_id, inbound_detail_id, deleted),
|
||||
KEY idx_loading_item_pallet (pallet_id, deleted),
|
||||
KEY idx_loading_item_order (account_id, order_id, deleted),
|
||||
CONSTRAINT fk_loading_item_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||
CONSTRAINT chk_loading_item_account CHECK (account_id > 0),
|
||||
@@ -669,66 +675,6 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_history (
|
||||
CONSTRAINT fk_loading_pick_history_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id)
|
||||
) ENGINE=InnoDB COMMENT='PDA拣货幂等和操作审计历史';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场作业托盘ID',
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID快照',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
||||
warehouse_pallet_id BIGINT UNSIGNED NULL COMMENT '仓库实物托盘ID;未绑定仓库资源时为空',
|
||||
pallet_code VARCHAR(64) NOT NULL COMMENT '现场托盘号',
|
||||
open_operation_id VARCHAR(128) NOT NULL COMMENT '开托幂等操作号',
|
||||
seal_operation_id VARCHAR(128) NULL COMMENT '封托幂等操作号',
|
||||
pallet_status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT '托盘状态:OPEN打托中、SEALED已封托、CANCELLED已取消',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID',
|
||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人名称快照',
|
||||
sealed_by BIGINT UNSIGNED NULL COMMENT '封托操作人ID',
|
||||
sealed_by_name VARCHAR(64) NULL COMMENT '封托操作人名称快照',
|
||||
sealed_at DATETIME(3) NULL COMMENT '封托时间',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_work_pallet_code (pick_task_id, pallet_code),
|
||||
UNIQUE KEY uk_loading_work_pallet_open_operation (open_operation_id),
|
||||
UNIQUE KEY uk_loading_work_pallet_seal_operation (seal_operation_id),
|
||||
KEY idx_loading_work_pallet_plan (loading_plan_id, pallet_status, id),
|
||||
CONSTRAINT fk_loading_work_pallet_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
||||
CONSTRAINT fk_loading_work_pallet_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||
CONSTRAINT chk_loading_work_pallet_status CHECK (pallet_status IN ('OPEN','SEALED','CANCELLED'))
|
||||
) ENGINE=InnoDB COMMENT='一件代发PDA现场实际托盘;允许同托盘承载多个客户订单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet_carton (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场托盘箱唛绑定ID',
|
||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
||||
work_pallet_id BIGINT UNSIGNED NOT NULL COMMENT '现场作业托盘ID',
|
||||
loading_plan_item_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划货物明细ID',
|
||||
pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '箱唛所属客户ID',
|
||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓单ID',
|
||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓货物明细ID',
|
||||
carton_label_id BIGINT UNSIGNED NOT NULL COMMENT '稳定箱唛记录ID',
|
||||
box_code VARCHAR(80) NOT NULL COMMENT '稳定箱唛箱码快照',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照',
|
||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱产品数量快照',
|
||||
bound_by BIGINT UNSIGNED NOT NULL COMMENT '最后绑定操作人ID',
|
||||
bound_by_name VARCHAR(64) NULL COMMENT '最后绑定操作人名称快照',
|
||||
operation_id VARCHAR(128) NOT NULL COMMENT '最近一次绑定或移托幂等操作号',
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次上托时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后移托时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_work_carton_task_label (pick_task_id, carton_label_id),
|
||||
UNIQUE KEY uk_loading_work_carton_operation (operation_id),
|
||||
KEY idx_loading_work_carton_pallet (work_pallet_id, sku_code, id),
|
||||
KEY idx_loading_work_carton_plan_item (loading_plan_item_id, id),
|
||||
CONSTRAINT fk_loading_work_carton_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
||||
CONSTRAINT fk_loading_work_carton_pallet FOREIGN KEY (work_pallet_id) REFERENCES fl_loading_work_pallet (id),
|
||||
CONSTRAINT fk_loading_work_carton_plan_item FOREIGN KEY (loading_plan_item_id) REFERENCES fl_loading_plan_item (id),
|
||||
CONSTRAINT fk_loading_work_carton_pick_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
|
||||
CONSTRAINT fk_loading_work_carton_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label (id),
|
||||
CONSTRAINT chk_loading_work_carton_units CHECK (units_per_carton IS NULL OR units_per_carton > 0)
|
||||
) ENGINE=InnoDB COMMENT='一件代发PDA逐箱上托事实;客户订单不合并但实际托盘可混托';
|
||||
|
||||
DROP PROCEDURE IF EXISTS repair_phase3_loading_pick_task_foreign_keys;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE repair_phase3_loading_pick_task_foreign_keys()
|
||||
@@ -1478,12 +1424,13 @@ BEGIN
|
||||
'destination_warehouse_name', 'product_name_zh', 'product_name_en',
|
||||
'product_image_file_id', 'pallet_count', 'net_weight_kg',
|
||||
'dimensional_weight_kg', 'product_quantity', 'units_per_carton',
|
||||
'fba_tracking_no', 'shipment_no', 'shipping_mark', 'version')) <> 17 THEN
|
||||
'unit_type', 'pallet_id', 'pallet_no', 'indivisible', 'active_assignment_key',
|
||||
'fba_tracking_no', 'shipment_no', 'shipping_mark', 'version')) <> 22 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '智能装柜迁移失败:明细归属、清单快照或冻结字段缺失';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_plan_item'
|
||||
AND index_name = 'uk_loading_item_plan_detail' AND non_unique = 0) <> 2 THEN
|
||||
AND index_name = 'uk_loading_item_plan_assignment' AND non_unique = 0) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '智能装柜迁移失败:计划内唯一活动明细锁缺失';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
|
||||
@@ -1,54 +1,153 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 一件代发 PDA 合托 API
|
||||
version: 1.0.0
|
||||
description: 测量完成后独立下发;不创建国内库存。PDA 扫箱唛识别 SKU,并按箱数合托。
|
||||
title: 一件代发国内清点组托 API
|
||||
version: 2.0.0
|
||||
description: 测量、计费后独立下发。箱码逐箱确认 SKU 实收数量;托盘码可先扫或后扫,封托后作为不可拆分柜单元。
|
||||
paths:
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/pending:
|
||||
get:
|
||||
summary: 查询待领取或本人已领取的一件代发合托任务
|
||||
responses: { '200': { description: OK } }
|
||||
summary: 查询待领取或本人已领取任务
|
||||
x-required-authority: platform:firstleg:pda:view
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}:
|
||||
get:
|
||||
summary: 查询任务、托盘、入仓单、SKU 和箱数明细
|
||||
summary: 查询任务、已清点待绑箱及托盘结果
|
||||
x-required-authority: platform:firstleg:pda:view
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/WarehouseId'}]
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/cartons/resolve:
|
||||
get:
|
||||
summary: 扫箱码解析入仓码、SKU 和应收数量,不返回全部未清点箱
|
||||
x-required-authority: platform:firstleg:pda:view
|
||||
parameters:
|
||||
- { name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
- { name: warehouseId, in: query, required: true, schema: { type: integer, format: int64 } }
|
||||
responses: { '200': { description: OK } }
|
||||
- {$ref: '#/components/parameters/TaskId'}
|
||||
- {$ref: '#/components/parameters/WarehouseId'}
|
||||
- {name: boxCode, in: query, required: true, schema: {type: string, maxLength: 80}}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/claim:
|
||||
post:
|
||||
summary: 领取合托任务
|
||||
parameters: [{ name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }]
|
||||
responses: { '200': { description: OK } }
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/open:
|
||||
summary: 领取任务
|
||||
x-required-authority: platform:firstleg:pda:work
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ClaimRequest'}}}}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/cartons/count:
|
||||
post:
|
||||
summary: 开启一个现场托盘
|
||||
parameters: [{ name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }]
|
||||
responses: { '200': { description: OK } }
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/scan:
|
||||
summary: 逐箱确认 SKU 实收数量,可同时绑定当前托盘
|
||||
description: palletNo 为空时进入 COUNTED_UNBOUND;不为空时幂等获取或创建 OPEN 托盘并绑定。数量不一致必须 ACCEPT_VARIANCE 且填写说明。
|
||||
x-required-authority: platform:firstleg:pda:work
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CountCartonRequest'}}}}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/bind-pending:
|
||||
post:
|
||||
summary: 扫箱唛识别 SKU 并填写上托箱数
|
||||
summary: 扫托盘码并把当前任务全部待绑箱绑定到该托盘
|
||||
x-required-authority: platform:firstleg:pda:work
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/BindPendingRequest'}}}}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/cartons/{cartonId}/move:
|
||||
post:
|
||||
summary: 换托,targetPalletNo 为空时退回待绑区
|
||||
x-required-authority: platform:firstleg:pda:work
|
||||
parameters:
|
||||
- { name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
- { name: palletId, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
responses: { '200': { description: OK } }
|
||||
- {$ref: '#/components/parameters/TaskId'}
|
||||
- {name: cartonId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/MoveCartonRequest'}}}}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/tasks/{taskId}/pallets/{palletId}/seal:
|
||||
post:
|
||||
summary: 完成合托并生成 DP 托盘号
|
||||
summary: 封托;禁止空托、待绑箱、版本冲突和重复归托
|
||||
x-required-authority: platform:firstleg:pda:work
|
||||
parameters:
|
||||
- { name: taskId, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
- { name: palletId, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
responses: { '200': { description: OK } }
|
||||
- {$ref: '#/components/parameters/TaskId'}
|
||||
- {name: palletId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/SealPalletRequest'}}}}
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets:
|
||||
get:
|
||||
summary: 入仓单查看绑定的一个或多个托盘及各自箱数
|
||||
parameters: [{ name: inboundId, in: path, required: true, schema: { type: integer, format: int64 } }]
|
||||
responses: { '200': { description: OK } }
|
||||
summary: Admin 查询入仓单权威托盘结果
|
||||
x-required-authority: firstleg:inbound:list
|
||||
responses: {'200': {description: OK}}
|
||||
/api/platform/v1/first-leg/pda-dropship/inbound-orders/{inboundId}/pallets/{palletId}/print:
|
||||
get:
|
||||
summary: 打印托盘号、入仓码和 SKU 箱数托盘唛
|
||||
parameters:
|
||||
- { name: inboundId, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
- { name: palletId, in: path, required: true, schema: { type: integer, format: int64 } }
|
||||
responses:
|
||||
'200': { description: PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
|
||||
summary: 打印托盘码、入仓码和 SKU 实收汇总
|
||||
x-required-authority: firstleg:inbound:list
|
||||
responses: {'200': {description: PDF}}
|
||||
components:
|
||||
parameters:
|
||||
TaskId: {name: taskId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
WarehouseId: {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64}}
|
||||
schemas:
|
||||
Carton:
|
||||
type: object
|
||||
properties:
|
||||
cartonId: {type: integer, format: int64, nullable: true}
|
||||
boxCode: {type: string}
|
||||
inboundId: {type: integer, format: int64}
|
||||
inboundCode: {type: string}
|
||||
inboundDetailId: {type: integer, format: int64}
|
||||
skuCode: {type: string}
|
||||
cargoName: {type: string, nullable: true}
|
||||
expectedSkuQuantity: {type: integer, minimum: 1}
|
||||
actualSkuQuantity: {type: integer, minimum: 1, nullable: true}
|
||||
discrepancyStatus: {type: string, enum: [MATCH, ACCEPTED_VARIANCE], nullable: true}
|
||||
discrepancyRemark: {type: string, nullable: true}
|
||||
countStatus: {type: string, enum: [UNCOUNTED, COUNTED_UNBOUND, BOUND]}
|
||||
palletId: {type: integer, format: int64, nullable: true}
|
||||
palletNo: {$ref: '#/components/schemas/NullablePalletNo'}
|
||||
canCount: {type: boolean}
|
||||
blockReason: {type: string, nullable: true}
|
||||
Pallet:
|
||||
type: object
|
||||
properties:
|
||||
palletId: {type: integer, format: int64}
|
||||
palletNo: {$ref: '#/components/schemas/PalletNo'}
|
||||
status: {type: string, enum: [OPEN, SEALED]}
|
||||
cartonCount: {type: integer, minimum: 0}
|
||||
skuCount: {type: integer, minimum: 0}
|
||||
discrepancyCartons: {type: integer, minimum: 0}
|
||||
weightKg: {type: number, minimum: 0}
|
||||
volumeCbm: {type: number, minimum: 0}
|
||||
version: {type: integer, minimum: 1}
|
||||
items: {type: array, items: {type: object}}
|
||||
cartons: {type: array, items: {$ref: '#/components/schemas/Carton'}, description: 含稳定 cartonId,换托必须使用该 ID}
|
||||
sealedAt: {type: string, format: date-time, nullable: true}
|
||||
ClaimRequest:
|
||||
type: object
|
||||
required: [warehouseId]
|
||||
properties: {warehouseId: {type: integer, format: int64}}
|
||||
CountCartonRequest:
|
||||
type: object
|
||||
required: [warehouseId, boxCode, actualSkuQuantity, discrepancyAction, operationId]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64}
|
||||
boxCode: {type: string, maxLength: 80}
|
||||
actualSkuQuantity: {type: integer, minimum: 1}
|
||||
discrepancyAction: {type: string, enum: [MATCH, ACCEPT_VARIANCE]}
|
||||
discrepancyRemark: {type: string, maxLength: 500, nullable: true}
|
||||
palletNo: {$ref: '#/components/schemas/NullablePalletNo'}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
BindPendingRequest:
|
||||
type: object
|
||||
required: [warehouseId, palletNo, operationId]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64}
|
||||
palletNo: {$ref: '#/components/schemas/PalletNo'}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
MoveCartonRequest:
|
||||
type: object
|
||||
required: [warehouseId, operationId]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64}
|
||||
targetPalletNo: {$ref: '#/components/schemas/NullablePalletNo'}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
SealPalletRequest:
|
||||
type: object
|
||||
required: [warehouseId, expectedVersion, operationId]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64}
|
||||
expectedVersion: {type: integer, minimum: 1}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
PalletNo: {type: string, pattern: '^DP-[0-9]{6}-[0-9]{4}$'}
|
||||
NullablePalletNo: {type: string, pattern: '^DP-[0-9]{6}-[0-9]{4}$', nullable: true}
|
||||
|
||||
@@ -296,7 +296,7 @@ paths:
|
||||
post:
|
||||
summary: 按统一订单条件分页查询履约摘要(Order 内部)
|
||||
description: >-
|
||||
mainStatus 接受六态产品状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态。
|
||||
mainStatus 接受七态产品状态;WAIT_PALLETIZE 为一件代发测量计费后的待清点组托状态,WAIT_SPLIT 为待分柜状态。
|
||||
计费与入库处理中状态在完成前仍归并为 WAIT_MEASURE,不提前开放分柜。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
@@ -1133,7 +1133,7 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/approve:
|
||||
post:
|
||||
summary: 审核通过测量
|
||||
description: 需要 firstleg:inbound:measurement-review 权限。审核通过后立即进入 WAIT_SPLIT,并按该次测量数据尽力生成 CALCULATED 初算费用;价格未覆盖或初算异常只返回非阻断提示,不影响审核结果和待分柜状态。
|
||||
description: 需要 firstleg:inbound:measurement-review 权限。标准头程审核通过后进入 WAIT_SPLIT;一件代发进入 WAIT_PALLETIZE,完成逐箱清点和封托后再进入 WAIT_SPLIT。审核时按该次测量数据尽力生成 CALCULATED 初算费用;价格未覆盖或初算异常只返回非阻断提示。
|
||||
parameters:
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
@@ -1150,7 +1150,7 @@ paths:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
reason: {type: string, maxLength: 1000}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断;审核成功 data.processStatus 固定为 WAIT_SPLIT,initialPricingSucceeded 表示是否同时生成初算费用,初算失败时 feeStatus=UNPRICED 且主流程仍成功}
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断;审核成功后标准头程 data.processStatus=WAIT_SPLIT,一件代发为 WAIT_PALLETIZE;initialPricingSucceeded 表示是否同时生成初算费用,未覆盖时 feeStatus=UNPRICED 且主流程仍成功}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/submit:
|
||||
post:
|
||||
summary: 提交测量完成
|
||||
@@ -1164,7 +1164,7 @@ paths:
|
||||
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
||||
get:
|
||||
summary: 客户查询已完成入库结果和最新已发布费用
|
||||
description: 需要 merchant:order:view 权限;客户仅能按登录态客户范围查看。入库流程达到 WAIT_SPLIT 后,费用在 LOADING、LOADED、DELIVERED 仍持续可见;始终要求 measureReviewStatus=APPROVED、feeStatus=SENT 且存在 PUBLISHED 入库运费明细。latestFee.customerConfirmationStatus 表示客户是否已确认当前费用版本。
|
||||
description: 需要 merchant:order:view 权限;客户仅能按登录态客户范围查看。一件代发在 WAIT_PALLETIZE 清点组托阶段即可查看并确认已发布费用,之后在 WAIT_SPLIT、LOADING、LOADED、DELIVERED 持续可见;始终要求 measureReviewStatus=APPROVED、feeStatus=SENT 且存在 PUBLISHED 入库运费明细。
|
||||
parameters:
|
||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
@@ -1383,8 +1383,8 @@ components:
|
||||
invalidatedFeeId: {type: integer, format: int64, nullable: true}
|
||||
UnifiedMainStatus:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
description: 六态产品主状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
description: 七态产品主状态;WAIT_PALLETIZE 仅表示一件代发待清点组托
|
||||
UnifiedInboundQuery:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1393,7 +1393,7 @@ components:
|
||||
orderNo: {type: string, maxLength: 64, nullable: true}
|
||||
mainStatus:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
nullable: true
|
||||
description: WAIT_SPLIT 为唯一待分柜筛选值
|
||||
customerId: {type: integer, format: int64, nullable: true}
|
||||
@@ -1511,6 +1511,7 @@ components:
|
||||
deliveryWarehouseName: {type: string, nullable: true}
|
||||
businessType: {type: string, nullable: true}
|
||||
fulfillmentType: {type: string, enum: [STANDARD, ONE_CLICK_DROPSHIP], nullable: true}
|
||||
lxCode: {type: string, nullable: true, description: 一件代发线路或履约产品编码快照}
|
||||
channelCode: {type: string, nullable: true}
|
||||
channelName: {type: string, nullable: true}
|
||||
taxMode: {type: string, nullable: true}
|
||||
|
||||
@@ -170,104 +170,11 @@ paths:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
reason: {type: string, minLength: 1, maxLength: 500}
|
||||
responses: {'200': {description: 作废后柜级证据列表}}
|
||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets:
|
||||
get:
|
||||
summary: 查询一件代发现场实际托盘、SKU汇总和逐箱明细
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TaskId'}
|
||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
responses:
|
||||
'200':
|
||||
description: 当前任务未取消的现场托盘
|
||||
content: {application/json: {schema: {type: array, items: {$ref: '#/components/schemas/WorkPallet'}}}}
|
||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets/open:
|
||||
post:
|
||||
summary: 为已领取任务开启一个一件代发现场托盘
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||
requestBody:
|
||||
required: true
|
||||
content: {application/json: {schema: {$ref: '#/components/schemas/OpenPalletRequest'}}}
|
||||
responses:
|
||||
'200': {description: 已开启托盘或相同operationId的幂等结果}
|
||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets/{palletId}/cartons/scan:
|
||||
post:
|
||||
summary: 扫描一件代发箱唛上托;箱唛已在其他未封托托盘时按箱移托
|
||||
description: 本操作只记录实际箱到托关系,不推进既有拣货数量、订单状态或仓库资源结算。
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/PalletId'}]
|
||||
requestBody:
|
||||
required: true
|
||||
content: {application/json: {schema: {$ref: '#/components/schemas/ScanCartonRequest'}}}
|
||||
responses:
|
||||
'200': {description: 包含最新SKU与箱唛汇总的目标托盘}
|
||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets/{palletId}/seal:
|
||||
post:
|
||||
summary: 封托并校验现场上托箱数未超过既有PDA已提交拣货箱数
|
||||
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/PalletId'}]
|
||||
requestBody:
|
||||
required: true
|
||||
content: {application/json: {schema: {$ref: '#/components/schemas/SealPalletRequest'}}}
|
||||
responses:
|
||||
'200': {description: 已封托托盘或A0409数量/版本冲突}
|
||||
components:
|
||||
parameters:
|
||||
TaskId: {name: taskId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
ItemId: {name: itemId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
PalletId: {name: palletId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
schemas:
|
||||
OpenPalletRequest:
|
||||
type: object
|
||||
required: [warehouseId, palletCode, operationId]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
||||
warehousePalletId: {type: integer, format: int64, minimum: 1, nullable: true}
|
||||
palletCode: {type: string, maxLength: 64}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
ScanCartonRequest:
|
||||
type: object
|
||||
required: [warehouseId, boxCode, operationId]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
||||
boxCode: {type: string, maxLength: 80}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
SealPalletRequest:
|
||||
type: object
|
||||
required: [warehouseId, expectedVersion, operationId]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
operationId: {type: string, maxLength: 128}
|
||||
WorkPallet:
|
||||
type: object
|
||||
required: [id, palletCode, status, version, cartonCount, skuCount, customerCount, skus, cartons]
|
||||
properties:
|
||||
id: {type: integer, format: int64}
|
||||
palletCode: {type: string}
|
||||
warehousePalletId: {type: integer, format: int64, nullable: true}
|
||||
status: {type: string, enum: [OPEN, SEALED]}
|
||||
version: {type: integer, minimum: 0}
|
||||
cartonCount: {type: integer, minimum: 0}
|
||||
skuCount: {type: integer, minimum: 0}
|
||||
customerCount: {type: integer, minimum: 0}
|
||||
skus: {type: array, items: {$ref: '#/components/schemas/WorkPalletSku'}}
|
||||
cartons: {type: array, items: {$ref: '#/components/schemas/WorkPalletCarton'}}
|
||||
WorkPalletSku:
|
||||
type: object
|
||||
required: [skuCode, cartonCount, productQuantity]
|
||||
properties:
|
||||
skuCode: {type: string}
|
||||
cartonCount: {type: integer, minimum: 1}
|
||||
productQuantity: {type: integer, format: int64, minimum: 0}
|
||||
WorkPalletCarton:
|
||||
type: object
|
||||
required: [id, boxCode, skuCode, tenantId, inboundId, inboundDetailId]
|
||||
properties:
|
||||
id: {type: integer, format: int64}
|
||||
boxCode: {type: string}
|
||||
skuCode: {type: string}
|
||||
tenantId: {type: integer, format: int64}
|
||||
inboundId: {type: integer, format: int64}
|
||||
inboundDetailId: {type: integer, format: int64}
|
||||
unitsPerCarton: {type: integer, minimum: 1, nullable: true}
|
||||
Task:
|
||||
type: object
|
||||
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, sourceType, recordStatus, taskStatus, workflowStatus, version, items]
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ class InboundMeasurementReviewContractTest {
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/measurements/approve:")
|
||||
.contains("Idempotency-Key")
|
||||
.contains("初算异常只返回非阻断提示")
|
||||
.contains("data.processStatus 固定为 WAIT_SPLIT")
|
||||
.contains("标准头程审核通过后进入 WAIT_SPLIT")
|
||||
.contains("一件代发进入 WAIT_PALLETIZE")
|
||||
.contains("feeStatus=UNPRICED 且主流程仍成功")
|
||||
.contains("name: inboundId")
|
||||
.contains("schema: {type: string, pattern:")
|
||||
|
||||
+2
-7
@@ -24,13 +24,8 @@ class PlatformLoadingPdaControllerContractTest {
|
||||
authority("review", "platform:firstleg:pda:review", AuthenticatedUser.class, long.class, ReviewRequest.class);
|
||||
authority("complete", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, CompleteLoadingRequest.class);
|
||||
authority("operations", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class);
|
||||
authority("pallets", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("openPallet", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class,
|
||||
com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest.class);
|
||||
authority("scanCarton", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, long.class,
|
||||
com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest.class);
|
||||
authority("sealPallet", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, long.class,
|
||||
com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest.class);
|
||||
authority("pickPallet", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class,
|
||||
PalletPickRequest.class);
|
||||
}
|
||||
|
||||
private void authority(String name, String expected, Class<?>... parameterTypes) throws Exception {
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class UnifiedInboundOpenApiContractTest {
|
||||
@Test
|
||||
void documentsInternalUnifiedOrderEndpointsAndSixStatuses() throws Exception {
|
||||
void documentsInternalUnifiedOrderEndpointsAndSevenStatuses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
||||
@@ -25,8 +25,8 @@ class UnifiedInboundOpenApiContractTest {
|
||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
|
||||
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜状态");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_PALLETIZE", "待清点组托");
|
||||
|
||||
Map<String, Object> query = stringMap(schemas.get("UnifiedInboundQuery"));
|
||||
Map<String, Object> queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus"));
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DropshipPalletizationSqlContractTest {
|
||||
@Test
|
||||
void moduleAndUnifiedSchemasExposeCountingPalletAndWholePalletAssignmentFacts() throws Exception {
|
||||
String inbound = new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String loading = new ClassPathResource("db/phase3-smart-loading-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
for (String sql : new String[]{inbound, unified}) {
|
||||
assertThat(sql)
|
||||
.contains("counted_cartons INT UNSIGNED NOT NULL DEFAULT 0")
|
||||
.contains("discrepancy_cartons INT UNSIGNED NOT NULL DEFAULT 0")
|
||||
.contains("pallet_status VARCHAR(16) NOT NULL COMMENT '托盘状态:OPEN组托中、SEALED已封托'")
|
||||
.contains("expected_sku_quantity INT UNSIGNED NOT NULL")
|
||||
.contains("actual_sku_quantity INT UNSIGNED NOT NULL")
|
||||
.contains("count_operation_id VARCHAR(128) NOT NULL")
|
||||
.contains("bind_operation_id VARCHAR(128) NULL");
|
||||
}
|
||||
for (String sql : new String[]{loading, unified}) {
|
||||
assertThat(sql)
|
||||
.contains("unit_type VARCHAR(24) NOT NULL DEFAULT 'PACKAGE'")
|
||||
.contains("pallet_id BIGINT UNSIGNED NULL")
|
||||
.contains("indivisible TINYINT(1) NOT NULL DEFAULT 0")
|
||||
.contains("active_assignment_key VARCHAR(96) GENERATED ALWAYS AS")
|
||||
.contains("UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id, active_assignment_key)")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_work_pallet (")
|
||||
.doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_work_pallet_carton (");
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -18,16 +18,19 @@ class InboundCartonLabelSqlContractTest {
|
||||
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
assertThat(schema).contains("CREATE TABLE IF NOT EXISTS fl_inbound_carton_label")
|
||||
.contains("units_per_carton INT UNSIGNED NULL")
|
||||
.contains("UNIQUE KEY uk_carton_label_box_code (tenant_id, box_code)")
|
||||
.contains("UNIQUE KEY uk_carton_label_index (tenant_id, inbound_id, label_version, carton_index)")
|
||||
.contains("inbound_detail_id BIGINT UNSIGNED NOT NULL");
|
||||
assertThat(migration).contains("CREATE TABLE IF NOT EXISTS fl_inbound_carton_label")
|
||||
.contains("units_per_carton INT UNSIGNED NULL")
|
||||
.contains("固定二维码内容:入仓码#总箱数-箱下标")
|
||||
.contains("idx_carton_label_detail (tenant_id, inbound_id, inbound_detail_id)");
|
||||
assertThat(versioningMigration).contains("phase2_carton_label_add_column('label_version'")
|
||||
.contains("phase2_carton_label_add_column('batch_status'")
|
||||
.contains("idx_carton_label_active (tenant_id, inbound_id, batch_status, label_version)");
|
||||
assertThat(unified).contains("CREATE TABLE IF NOT EXISTS fl_inbound_carton_label")
|
||||
.contains("units_per_carton INT UNSIGNED NULL")
|
||||
.contains("unified_carton_label_add_column('label_version'")
|
||||
.contains("unified_carton_label_add_column('batch_status'")
|
||||
.contains("unified_carton_label_add_column('cargo_fingerprint'")
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.contains("SUM(package_count) package_count")
|
||||
.contains("GROUP BY tenant_id,measurement_id,inbound_detail_id")
|
||||
.contains("fl_dropship_pallet_carton carton")
|
||||
.contains("pallet.pallet_status='COMPLETED'")
|
||||
.contains("pallet.pallet_status='SEALED'")
|
||||
.contains("source.fulfillment_type='ONE_CLICK_DROPSHIP'")
|
||||
.contains("i.inbound_id=o.id")
|
||||
.contains("o.warehouse_id=#{warehouseId}")
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ class SmartLoadingSqlContractTest {
|
||||
assertThat(migration())
|
||||
.contains("active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS")
|
||||
.contains("CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END")
|
||||
.contains("UNIQUE KEY uk_loading_item_plan_detail (loading_plan_id, active_inbound_detail_id)")
|
||||
.contains("active_assignment_key VARCHAR(96) GENERATED ALWAYS AS")
|
||||
.contains("UNIQUE KEY uk_loading_item_plan_assignment (loading_plan_id, active_assignment_key)")
|
||||
.contains("KEY idx_loading_item_active_detail (account_id, active_inbound_detail_id)")
|
||||
.contains("UNIQUE KEY uk_container_booking_no (booking_no)")
|
||||
.contains("UNIQUE KEY uk_container_booking_cabinet (cabinet_no)")
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.CountCartonRequest;
|
||||
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.DiscrepancyAction;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.TaskRow;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DropshipPalletizationServiceTest {
|
||||
private final DropshipPalletizationMapper mapper=mock(DropshipPalletizationMapper.class);
|
||||
private final DropshipPalletizationService service=new DropshipPalletizationService();
|
||||
|
||||
DropshipPalletizationServiceTest(){ ReflectionTestUtils.setField(service,"mapper",mapper); }
|
||||
|
||||
@Test void resolvesUncountedCartonBeforeQuantityConfirmation(){
|
||||
TaskRow task=task(); LabelRow label=label();
|
||||
when(mapper.task(10L,100L)).thenReturn(task); when(mapper.label(10L,"BOX-1")).thenReturn(label);
|
||||
var result=service.resolveCarton(10L,100L,"BOX-1",7L);
|
||||
assertThat(result.inboundCode()).isEqualTo("IN-1");
|
||||
assertThat(result.skuCode()).isEqualTo("SKU-1");
|
||||
assertThat(result.expectedSkuQuantity()).isEqualTo(12);
|
||||
assertThat(result.countStatus()).isEqualTo("UNCOUNTED");
|
||||
assertThat(result.canCount()).isTrue();
|
||||
}
|
||||
|
||||
@Test void mismatchCannotBeRecordedWithoutAcceptedVarianceAndRemark(){
|
||||
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.label(10L,"BOX-1")).thenReturn(label());
|
||||
var wrongAction=new CountCartonRequest(100L,"BOX-1",11,DiscrepancyAction.MATCH,null,null,"op-1");
|
||||
assertThatThrownBy(()->service.countCarton(10L,wrongAction,7L,"张三"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("确认差异处理结果");
|
||||
var noRemark=new CountCartonRequest(100L,"BOX-1",11,DiscrepancyAction.ACCEPT_VARIANCE,null,null,"op-2");
|
||||
assertThatThrownBy(()->service.countCarton(10L,noRemark,7L,"张三"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("处理说明");
|
||||
verify(mapper,never()).insertCarton(anyLong(),anyLong(),any(),anyLong(),anyLong(),anyLong(),anyLong(),any(),any(),any(),anyInt(),anyInt(),any(),any(),any(),anyLong(),any());
|
||||
}
|
||||
|
||||
@Test void countsOneCartonDirectlyIntoScannedPallet(){
|
||||
TaskRow before=task(),after=task(); after.setCountedCartons(1); after.setAllocatedCartons(1);
|
||||
LabelRow label=label(); PalletRow pallet=pallet(); CartonRow carton=carton();
|
||||
when(mapper.task(10L,100L)).thenReturn(before,after);
|
||||
when(mapper.label(10L,"BOX-1")).thenReturn(label); when(mapper.palletByNo("DP-260908-0001")).thenReturn(pallet);
|
||||
when(mapper.addCounted(10L,7L,1,0)).thenReturn(1); when(mapper.carton(eq(10L),anyLong())).thenReturn(carton);
|
||||
when(mapper.pallet(10L,20L,100L)).thenReturn(pallet); when(mapper.cartons(20L)).thenReturn(java.util.List.of(carton));
|
||||
when(mapper.metrics(20L)).thenReturn(new DropshipPalletizationMapper.PalletMetrics(java.math.BigDecimal.ONE,java.math.BigDecimal.TEN));
|
||||
var request=new CountCartonRequest(100L,"BOX-1",12,DiscrepancyAction.MATCH,null,"DP-260908-0001","op-1");
|
||||
var result=service.countCarton(10L,request,7L,"张三");
|
||||
assertThat(result.carton().countStatus()).isEqualTo("BOUND");
|
||||
assertThat(result.pallet().status()).isEqualTo("OPEN");
|
||||
verify(mapper).insertCarton(anyLong(),eq(10L),eq(20L),eq(1L),eq(30L),eq(40L),eq(50L),eq("SKU-1"),
|
||||
eq("货物"),eq("BOX-1"),eq(12),eq(12),eq("MATCH"),isNull(),eq("op-1"),eq(7L),eq("张三"));
|
||||
}
|
||||
|
||||
private static TaskRow task(){ TaskRow t=new TaskRow();t.setId(10L);t.setTenantId(1L);t.setInboundId(30L);t.setWarehouseId(100L);t.setInboundCode("IN-1");t.setTaskStatus("CLAIMED");t.setAssigneeId(7L);t.setProcessStatus("WAIT_PALLETIZE");t.setTotalCartons(2);t.setCountedCartons(0);t.setAllocatedCartons(0);t.setDiscrepancyCartons(0);t.setVersion(1);return t; }
|
||||
private static LabelRow label(){LabelRow l=new LabelRow();l.setCartonLabelId(50L);l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setUnitsPerCarton(12);l.setInboundCode("IN-1");l.setBoxCode("BOX-1");return l;}
|
||||
private static PalletRow pallet(){PalletRow p=new PalletRow();p.setId(20L);p.setTaskId(10L);p.setTenantId(1L);p.setInboundId(30L);p.setWarehouseId(100L);p.setPalletNo("DP-260908-0001");p.setPalletStatus("OPEN");p.setVersion(1);return p;}
|
||||
private static CartonRow carton(){CartonRow c=new CartonRow();c.setId(60L);c.setTaskId(10L);c.setPalletId(20L);c.setPalletNo("DP-260908-0001");c.setInboundId(30L);c.setInboundDetailId(40L);c.setSkuCode("SKU-1");c.setCargoName("货物");c.setBoxCode("BOX-1");c.setExpectedSkuQuantity(12);c.setActualSkuQuantity(12);c.setDiscrepancyStatus("MATCH");return c;}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
@@ -103,6 +104,8 @@ class LoadingPickServiceTest {
|
||||
private InboundPalletAllocationRepository palletAllocations;
|
||||
@MockBean
|
||||
private InboundHoldGuard inboundHoldGuard;
|
||||
@MockBean
|
||||
private DropshipPalletizationMapper dropshipPallets;
|
||||
@Resource
|
||||
private LoadingPickService service;
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
|
||||
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper.CartonCandidate;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper.TaskContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(LoadingWorkPalletService.class)
|
||||
class LoadingWorkPalletServiceTest {
|
||||
@MockBean private LoadingWorkPalletMapper pallets;
|
||||
@MockBean private LoadingWorkPalletCartonMapper cartons;
|
||||
@MockBean private InboundHoldGuard inboundHoldGuard;
|
||||
@Resource private LoadingWorkPalletService service;
|
||||
|
||||
@Test
|
||||
void scansDropshipCartonOntoMixedCustomerWorkPalletWithoutChangingPickWorkflow() {
|
||||
when(pallets.selectTaskContext(10L, 20L)).thenReturn(context());
|
||||
LoadingWorkPalletEntity pallet = pallet(30L, "OPEN");
|
||||
when(pallets.selectScoped(10L, 30L, 20L)).thenReturn(pallet);
|
||||
CartonCandidate candidate = candidate();
|
||||
when(cartons.selectCandidates(10L, 20L, "BOX-A")).thenReturn(List.of(candidate));
|
||||
doAnswer(invocation -> {
|
||||
LoadingWorkPalletCartonEntity value = invocation.getArgument(0);
|
||||
value.setId(50L);
|
||||
return 1;
|
||||
}).when(cartons).insert(any(LoadingWorkPalletCartonEntity.class));
|
||||
when(cartons.selectPalletCartons(30L)).thenAnswer(invocation -> List.of());
|
||||
|
||||
service.scan(10L, 30L, new ScanRequest(20L, "BOX-A", "SCAN-1"), 99L, "作业员");
|
||||
|
||||
var captor = org.mockito.ArgumentCaptor.forClass(LoadingWorkPalletCartonEntity.class);
|
||||
verify(cartons).insert(captor.capture());
|
||||
assertThat(captor.getValue().getTenantId()).isEqualTo(88L);
|
||||
assertThat(captor.getValue().getLoadingPlanItemId()).isEqualTo(41L);
|
||||
assertThat(captor.getValue().getSkuCode()).isEqualTo("SKU-A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesToSealUntilScannedCartonsAreReconciledByExistingPickSubmission() {
|
||||
when(pallets.selectTaskContext(10L, 20L)).thenReturn(context());
|
||||
when(pallets.selectScoped(10L, 30L, 20L)).thenReturn(pallet(30L, "OPEN"));
|
||||
when(cartons.countPalletCartons(30L)).thenReturn(2);
|
||||
when(cartons.countUnreconciled(10L)).thenReturn(1);
|
||||
|
||||
assertThatThrownBy(() -> service.seal(10L, 30L,
|
||||
new SealRequest(20L, 0, "SEAL-1"), 99L, "作业员"))
|
||||
.hasMessageContaining("原PDA拣货提交");
|
||||
}
|
||||
|
||||
private static TaskContext context() {
|
||||
return new TaskContext(10L, 11L, "LOADING", 20L, 99L, 2);
|
||||
}
|
||||
|
||||
private static LoadingWorkPalletEntity pallet(long id, String status) {
|
||||
LoadingWorkPalletEntity value = new LoadingWorkPalletEntity();
|
||||
value.setId(id); value.setPickTaskId(10L); value.setLoadingPlanId(11L);
|
||||
value.setWarehouseId(20L); value.setPalletCode("PLT-01");
|
||||
value.setPalletStatus(status); value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static CartonCandidate candidate() {
|
||||
CartonCandidate value = new CartonCandidate();
|
||||
value.setCartonLabelId(40L); value.setTenantId(88L); value.setInboundId(81L);
|
||||
value.setInboundDetailId(82L); value.setBoxCode("BOX-A");
|
||||
value.setLoadingPlanItemId(41L); value.setPickItemId(42L);
|
||||
value.setSkuCode("SKU-A"); value.setUnitsPerCarton(20);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
|
||||
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow;
|
||||
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.PlanColoadMemberRow;
|
||||
import com.baiyue.firstleg.split.service.SplitPreloadService;
|
||||
@@ -95,6 +96,7 @@ class SmartLoadingServiceTest {
|
||||
@MockBean private ColoadGroupAssignmentPort coloadGroups;
|
||||
@MockBean private PriceBookQueryUseCase priceBooks;
|
||||
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@MockBean private DropshipPalletizationMapper dropshipPallets;
|
||||
@Resource private SmartLoadingService service;
|
||||
|
||||
@TestConfiguration
|
||||
|
||||
@@ -48,7 +48,8 @@ public interface FirstlegUnifiedOrderClient {
|
||||
Long deliveryWarehouseId, String deliveryWarehouseName, String channelCode, String channelName,
|
||||
Integer totalPackageCount, String locationCodes,
|
||||
LocalDateTime expectedArrivalAt, LocalDateTime requestedPickupAt, String feeReviewStatus,
|
||||
String companyName, String businessType, String taxMode, String containerMode,
|
||||
String companyName, String businessType, String fulfillmentType, String lxCode,
|
||||
String taxMode, String containerMode,
|
||||
Boolean customsInspection, String destinationType, String destinationCode,
|
||||
Boolean holdFlag, String currentHoldReason,
|
||||
BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg, BigDecimal actualVolumeCbm,
|
||||
|
||||
@@ -72,13 +72,27 @@ public class InternalOrderCargoController {
|
||||
if (tenantId <= 0 || orderId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "租户和订单ID必须大于0");
|
||||
}
|
||||
var order = orders.findOwned(tenantId, orderId);
|
||||
if (order == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "订单不存在或不属于当前客户");
|
||||
}
|
||||
boolean oneClickDropship = "ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType());
|
||||
return ApiResponse.success(orders.findCargoInheritedByOrder(tenantId, orderId).stream()
|
||||
.map(cargo -> new CargoLabelBinding(cargo.getId(), orderId, cargo.getItemNo(),
|
||||
cargo.getCargoName(), cargo.getSkuNo(), cargo.getPackageCount(), cargo.getLengthCm(),
|
||||
cargo.getWidthCm(), cargo.getHeightCm()))
|
||||
cargo.getCargoName(), cargo.getSkuNo(), cargo.getPackageCount(),
|
||||
unitsPerCarton(cargo, oneClickDropship), cargo.getLengthCm(), cargo.getWidthCm(),
|
||||
cargo.getHeightCm()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private Integer unitsPerCarton(OrderCargoEntity cargo, boolean oneClickDropship) {
|
||||
Integer value = cargo.getUnitsPerPackage();
|
||||
if (oneClickDropship && (value == null || value <= 0)) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "一件代发货物每箱件数缺失或无效");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/{orderId}/inbound-snapshot")
|
||||
public ApiResponse<FirstlegInboundClient.OrderApprovedInboundCommand> inboundSnapshot(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@@ -155,12 +169,20 @@ public class InternalOrderCargoController {
|
||||
}
|
||||
|
||||
public record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, java.math.BigDecimal lengthCm,
|
||||
String skuNo, Integer packageCount, Integer unitsPerCarton,
|
||||
java.math.BigDecimal lengthCm,
|
||||
java.math.BigDecimal widthCm, java.math.BigDecimal heightCm) {
|
||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer packageCount, java.math.BigDecimal lengthCm,
|
||||
java.math.BigDecimal widthCm, java.math.BigDecimal heightCm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, null, packageCount, lengthCm, widthCm, heightCm);
|
||||
this(cargoId, orderId, itemNo, cargoName, null, packageCount, null,
|
||||
lengthCm, widthCm, heightCm);
|
||||
}
|
||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, java.math.BigDecimal lengthCm,
|
||||
java.math.BigDecimal widthCm, java.math.BigDecimal heightCm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, skuNo, packageCount, null,
|
||||
lengthCm, widthCm, heightCm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public class PlatformUnifiedOrderController {
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
@Operation(summary = "统一订单列表", description = "返回七态产品主状态及测量、预装、装柜、派送摘要。")
|
||||
@Operation(summary = "统一订单列表", description = "返回八态产品主状态及测量、组托、预装、装柜、派送摘要。")
|
||||
public ApiResponse<PageResult<UnifiedOrderResponse.Row>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) Long tenantId,
|
||||
|
||||
@@ -12,8 +12,8 @@ import java.time.LocalDateTime;
|
||||
public record UnifiedOrderQuery(
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED",
|
||||
"LOADING", "LOADED", "DELIVERED"})
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED",
|
||||
"WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
Long receivingWarehouseId,
|
||||
|
||||
@@ -24,7 +24,7 @@ public final class UnifiedOrderResponse {
|
||||
BigDecimal estimatedVolumeCbm, BigDecimal expectedVolumeCbm,
|
||||
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
|
||||
LocalDateTime expectedArrivalAt, String feeReviewStatus,
|
||||
String companyName, String businessType, String pickupMethod,
|
||||
String companyName, String businessType, String fulfillmentType, String lxCode, String pickupMethod,
|
||||
String taxMode, String containerMode, Boolean customsInspection,
|
||||
String destinationType, String destinationCode,
|
||||
Integer totalPalletCount, BigDecimal chargeableWeightKg, BigDecimal chargeableVolumeCbm,
|
||||
|
||||
@@ -38,7 +38,8 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class UnifiedOrderQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||
private static final List<OrderStatus> ACTIVE_ORDER_STATUSES = List.copyOf(EnumSet.of(
|
||||
OrderStatus.SUBMITTED, OrderStatus.APPROVED, OrderStatus.ACCEPTED,
|
||||
@@ -311,7 +312,8 @@ public class UnifiedOrderQueryService {
|
||||
order.getInboundCode(),
|
||||
order.getExpectedShipDate(), null, null, totalPackageCount(cargo), null, null,
|
||||
order.getEstimatedVolumeCbm(), volumeCbm(cargo), null, null,
|
||||
order.getExpectedArrivalAt(), null, null, null, order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
|
||||
order.getExpectedArrivalAt(), null, null, null, order.getFulfillmentType(), order.getLxCode(),
|
||||
order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
|
||||
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
|
||||
null, null, null, null, null, null, null, order.getExpressCompany(), order.getExpressTrackingNo(),
|
||||
StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
|
||||
@@ -334,7 +336,8 @@ public class UnifiedOrderQueryService {
|
||||
order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
||||
order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(),
|
||||
inbound.companyName(), inbound.businessType(), order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.companyName(), inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
||||
order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
||||
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
||||
inbound.actualNetWeightKg(),
|
||||
@@ -361,7 +364,8 @@ public class UnifiedOrderQueryService {
|
||||
order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
||||
order != null && order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(),
|
||||
inbound.businessType(), order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
||||
order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
||||
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
||||
inbound.actualNetWeightKg(),
|
||||
@@ -502,7 +506,7 @@ public class UnifiedOrderQueryService {
|
||||
return switch (status) {
|
||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE";
|
||||
case "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
case "INBOUNDED", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ paths:
|
||||
get:
|
||||
summary: 平台统一订单列表
|
||||
description: >-
|
||||
统一返回七态产品主状态、下一步动作及测量/预装/装柜/派送摘要,需要 platform:order:list。
|
||||
统一返回八态产品主状态、下一步动作及测量/组托/预装/装柜/派送摘要,需要 platform:order:list。
|
||||
入仓码来源由全系统统一模式控制:AUTO 在订单提交时按物流渠道生成,格式为物流前缀+YYMMDD+当天全平台票序;
|
||||
LEGACY_MANUAL 在草稿中录入旧系统入仓码。两种模式下入仓码一经提交均固定;有入库单时同时返回
|
||||
inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion
|
||||
@@ -617,7 +617,7 @@ components:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
keyword: {type: string, maxLength: 64}
|
||||
orderNo: {type: string, maxLength: 64}
|
||||
mainStatus: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]}
|
||||
mainStatus: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]}
|
||||
customerId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
receivingWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
@@ -693,9 +693,10 @@ components:
|
||||
asOf: {type: string, format: date-time}
|
||||
UnifiedMainStatus:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
description: >-
|
||||
统一订单七态产品主状态。WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录。
|
||||
统一订单八态产品主状态。ONE_CLICK_DROPSHIP 测量计费完成后进入 WAIT_PALLETIZE,封托完成后进入 WAIT_SPLIT;
|
||||
STANDARD 保持既有流程,INBOUNDED 仅兼容存量已入库记录。
|
||||
UnifiedOrderPageEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
@@ -766,6 +767,8 @@ components:
|
||||
feeReviewStatus: {type: string, nullable: true, description: Firstleg 入仓费用审核快照;财务状态不在本字段表达}
|
||||
companyName: {type: string, nullable: true}
|
||||
businessType: {type: string, nullable: true}
|
||||
fulfillmentType: {type: string, enum: [STANDARD, ONE_CLICK_DROPSHIP], nullable: true, description: 独立履约类型,不复用计费业务类型}
|
||||
lxCode: {type: string, nullable: true, description: 一件代发可选 LX 编码;普通头程或未填写时为 null}
|
||||
pickupMethod: {type: string, nullable: true}
|
||||
requestedPickupAt: {type: string, format: date-time, nullable: true, description: Customer-requested pickup appointment from the pickup snapshot}
|
||||
taxMode: {type: string, nullable: true}
|
||||
@@ -961,13 +964,13 @@ components:
|
||||
fulfillmentType:
|
||||
type: string
|
||||
enum: [STANDARD, ONE_CLICK_DROPSHIP]
|
||||
default: STANDARD
|
||||
description: 独立履约类型;省略为普通头程,不复用业务/计费 businessType
|
||||
lxCode:
|
||||
type: string
|
||||
nullable: true
|
||||
maxLength: 128
|
||||
description: 一件代发绑定的领星入库单 LX 代码
|
||||
default: STANDARD
|
||||
description: 独立履约类型;省略为普通头程,不复用业务/计费 businessType
|
||||
inboundCode:
|
||||
type: string
|
||||
maxLength: 40
|
||||
@@ -1194,17 +1197,17 @@ components:
|
||||
name: mainStatus
|
||||
in: query
|
||||
required: false
|
||||
description: 统一订单七态主状态筛选;WAIT_SPLIT 与存量兼容状态 INBOUNDED 独立查询
|
||||
description: 统一订单八态主状态筛选;WAIT_PALLETIZE、WAIT_SPLIT 与存量兼容状态 INBOUNDED 独立查询
|
||||
schema:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED, WAIT_SPLIT]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
UnifiedInboundCode:
|
||||
{name: inboundCode, in: query, required: false, description: 订单创建时生成的入仓码,所有订单状态均可查询, schema: {type: string, maxLength: 40}}
|
||||
UnifiedInternalOrderStatus:
|
||||
name: internalOrderStatus
|
||||
in: query
|
||||
required: false
|
||||
description: Order 内部生命周期状态;不是七态产品主状态
|
||||
description: Order 内部生命周期状态;不是八态产品主状态
|
||||
schema: {type: string, enum: [DRAFT, SUBMITTED, APPROVED, ACCEPTED, REJECTED, CANCELLED, PROCESSING, COMPLETED]}
|
||||
UnifiedExpectedArrivalDate:
|
||||
{name: expectedArrivalDate, in: query, required: false, schema: {type: string, format: date}}
|
||||
|
||||
@@ -88,7 +88,10 @@ class InternalOrderCargoControllerTest {
|
||||
@Test
|
||||
void listsCargoInRepositoryOrderForStableCartonAllocation() {
|
||||
OrderCargoEntity first = cargo(31L, 1, 3, "60", "40", "30");
|
||||
first.setUnitsPerPackage(24);
|
||||
OrderCargoEntity second = cargo(32L, 2, 2, "50", "30", "20");
|
||||
OrderEntity order = new OrderEntity(); order.setId(21L); order.setFulfillmentType("STANDARD");
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L)).thenReturn(List.of(first, second));
|
||||
|
||||
var response = controller.listForCartonLabels("internal-token", 11L, 21L);
|
||||
@@ -97,10 +100,68 @@ class InternalOrderCargoControllerTest {
|
||||
assertThat(response.data()).extracting(InternalOrderCargoController.CargoLabelBinding::cargoId)
|
||||
.containsExactly(31L, 32L);
|
||||
assertThat(response.data().get(0).packageCount()).isEqualTo(3);
|
||||
assertThat(response.data().get(0).unitsPerCarton()).isEqualTo(24);
|
||||
assertThat(response.data().get(1).unitsPerCarton()).isNull();
|
||||
assertThat(response.data().get(0).lengthCm()).isEqualByComparingTo("60");
|
||||
verify(orders).findOwned(11L, 21L);
|
||||
verify(orders).findCargoInheritedByOrder(11L, 21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneClickDropshipReturnsAuthoritativePositiveUnitsPerCarton() {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L); order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
OrderCargoEntity cargo = cargo(31L, 1, 3, "60", "40", "30");
|
||||
cargo.setUnitsPerPackage(12);
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L)).thenReturn(List.of(cargo));
|
||||
|
||||
var response = controller.listForCartonLabels("internal-token", 11L, 21L);
|
||||
|
||||
assertThat(response.data()).singleElement()
|
||||
.extracting(InternalOrderCargoController.CargoLabelBinding::unitsPerCarton)
|
||||
.isEqualTo(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneClickDropshipFailsClosedWhenUnitsPerCartonIsMissing() {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L); order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L))
|
||||
.thenReturn(List.of(cargo(31L, 1, 3, "60", "40", "30")));
|
||||
|
||||
assertThatThrownBy(() -> controller.listForCartonLabels("internal-token", 11L, 21L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("每箱件数缺失或无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneClickDropshipFailsClosedWhenUnitsPerCartonIsNotPositive() {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L); order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
OrderCargoEntity cargo = cargo(31L, 1, 3, "60", "40", "30");
|
||||
cargo.setUnitsPerPackage(0);
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L)).thenReturn(List.of(cargo));
|
||||
|
||||
assertThatThrownBy(() -> controller.listForCartonLabels("internal-token", 11L, 21L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("每箱件数缺失或无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonLabelListDoesNotExposeCargoWhenOrderIsOutsideCustomerScope() {
|
||||
when(orders.findOwned(12L, 21L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> controller.listForCartonLabels("internal-token", 12L, 21L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("订单不存在或不属于当前客户");
|
||||
|
||||
verify(orders).findOwned(12L, 21L);
|
||||
verifyNoMoreInteractions(orders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void childOrderCanInheritRootCargoBindingWithoutCrossCustomerLeak() {
|
||||
OrderCargoEntity cargo = cargo(31L, 1, 3, "60", "40", "30");
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.baiyue.order.controller;
|
||||
|
||||
import com.baiyue.order.client.FirstlegUnifiedOrderClient;
|
||||
import com.baiyue.order.client.FirstlegColoadGroupClient;
|
||||
import com.baiyue.order.dto.UnifiedOrderQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
@@ -26,7 +28,7 @@ class UnifiedOrderApiContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentsSevenProductMainStatuses() throws Exception {
|
||||
void documentsEightProductMainStatuses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/order-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
||||
@@ -40,22 +42,32 @@ class UnifiedOrderApiContractTest {
|
||||
Map<String, Object> mainStatus = stringMap(parameters.get("UnifiedMainStatus"));
|
||||
Map<String, Object> schema = stringMap(mainStatus.get("schema"));
|
||||
assertThat(stringList(schema.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED", "WAIT_SPLIT");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(mainStatus.get("description").toString())
|
||||
.contains("WAIT_SPLIT", "INBOUNDED");
|
||||
.contains("WAIT_PALLETIZE", "WAIT_SPLIT", "INBOUNDED");
|
||||
|
||||
Map<String, Object> schemas = stringMap(components.get("schemas"));
|
||||
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
|
||||
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString())
|
||||
.contains("WAIT_PALLETIZE", "WAIT_SPLIT", "INBOUNDED");
|
||||
Schema queryAnnotation = UnifiedOrderQuery.class.getDeclaredField("mainStatus")
|
||||
.getAnnotation(Schema.class);
|
||||
assertThat(queryAnnotation).isNotNull();
|
||||
assertThat(queryAnnotation.allowableValues()).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
|
||||
Map<String, Object> row = stringMap(schemas.get("UnifiedOrderRow"));
|
||||
assertThat(stringMap(row.get("properties"))).containsKeys(
|
||||
"orderId", "mainStatus", "channelCode", "inboundId", "inboundVersion", "measurement", "preload",
|
||||
"loading", "delivery", "transport", "customerRemark", "latestInternalNote",
|
||||
"holdFlag", "currentHoldReason",
|
||||
"expectedArrivalAt", "requestedPickupAt", "feeReviewStatus", "companyName", "businessType", "pickupMethod",
|
||||
"expectedArrivalAt", "requestedPickupAt", "feeReviewStatus", "companyName", "businessType",
|
||||
"fulfillmentType", "lxCode", "pickupMethod",
|
||||
"taxMode", "containerMode", "customsInspection", "destinationType", "destinationCode",
|
||||
"totalPalletCount", "chargeableWeightKg", "chargeableVolumeCbm", "volumetricWeightKg", "totalNetWeightKg",
|
||||
"measurementCompletedAt", "inboundCompletedAt", "expressCompany", "expressTrackingNo",
|
||||
@@ -86,7 +98,7 @@ class UnifiedOrderApiContractTest {
|
||||
assertThat(page.getAnnotation(PostMapping.class).value()).containsExactly("/query");
|
||||
assertThat(List.of(FirstlegUnifiedOrderClient.Inbound.class.getRecordComponents()).stream()
|
||||
.map(component -> component.getName()))
|
||||
.contains("channelCode", "channelName", "holdFlag", "currentHoldReason")
|
||||
.contains("channelCode", "channelName", "holdFlag", "currentHoldReason", "fulfillmentType", "lxCode")
|
||||
.doesNotContain("holdCargo", "holdReason");
|
||||
}
|
||||
|
||||
|
||||
@@ -549,6 +549,70 @@ class UnifiedOrderQueryServiceTest {
|
||||
assertThat(captor.getValue().mainStatus()).isEqualTo("WAIT_SPLIT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void waitPalletizeFilterReturnsDropshipProjectionAndForwardsToFirstleg() {
|
||||
UnifiedOrderQuery query = query("WAIT_PALLETIZE");
|
||||
var summary = summary("WAIT_PALLETIZE", "PALLETIZE_DROPSHIP");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
when(orders.findByIds(eq(10001L), eq(List.of(77L)))).thenReturn(List.of(order(OrderStatus.PROCESSING)));
|
||||
|
||||
var result = service.page(10001L, query);
|
||||
|
||||
assertThat(result.records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.mainStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
assertThat(row.nextAction()).isEqualTo("PALLETIZE_DROPSHIP");
|
||||
assertThat(row.fulfillmentType()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||
assertThat(row.lxCode()).isEqualTo("LX-20260908-01");
|
||||
});
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
|
||||
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg).page(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue().mainStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderOnlyProjectionUsesExistingFulfillmentFieldsWithoutCreatingInboundFacts() {
|
||||
UnifiedOrderQuery query = query("PENDING_REVIEW");
|
||||
OrderEntity order = order(OrderStatus.SUBMITTED);
|
||||
order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
order.setLxCode("LX-ORDER-01");
|
||||
when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED))))
|
||||
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
|
||||
|
||||
assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.mainStatus()).isEqualTo("PENDING_REVIEW");
|
||||
assertThat(row.fulfillmentType()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||
assertThat(row.lxCode()).isEqualTo("LX-ORDER-01");
|
||||
assertThat(row.inboundId()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailMapsWaitPalletizeSummaryAndTimeline() {
|
||||
OrderEntity order = order(OrderStatus.PROCESSING);
|
||||
when(orders.findOwned(10001L, 77L)).thenReturn(order);
|
||||
when(orders.findCargo(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findParties(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findHistory(10001L, 77L)).thenReturn(List.of());
|
||||
var timeline = new FirstlegUnifiedOrderClient.Timeline(
|
||||
"FIRSTLEG", "STATUS", "WAIT_MEASURE", "WAIT_PALLETIZE", "MEASUREMENT_APPROVED",
|
||||
null, 9L, "平台操作员", LocalDateTime.of(2026, 9, 8, 10, 0));
|
||||
var summary = summary("WAIT_PALLETIZE", "PALLETIZE_DROPSHIP");
|
||||
when(firstleg.detail(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new FirstlegUnifiedOrderClient.Detail(summary, List.of(timeline))));
|
||||
|
||||
var result = service.detail(10001L, 77L, false);
|
||||
|
||||
assertThat(result.summary().mainStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
assertThat(result.summary().fulfillmentType()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||
assertThat(result.summary().lxCode()).isEqualTo("LX-20260908-01");
|
||||
assertThat(result.timeline()).singleElement()
|
||||
.extracting(UnifiedOrderResponse.TimelineEvent::mainStatus)
|
||||
.isEqualTo("WAIT_PALLETIZE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listKeepsLoadedFulfillmentAheadOfRawApprovedOrderStatus() {
|
||||
UnifiedOrderQuery query = query(null);
|
||||
@@ -728,7 +792,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
@Test
|
||||
void fulfillmentTabsForwardStatusSearchAndPaginationToFirstleg() {
|
||||
List<String> statuses = List.of(
|
||||
"WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
|
||||
"WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 3, 20, 0, 0)));
|
||||
|
||||
@@ -840,6 +904,8 @@ class UnifiedOrderQueryServiceTest {
|
||||
when(inbound.locationCodes()).thenReturn("A0-01, B2-03");
|
||||
when(inbound.companyName()).thenReturn("测试客户有限公司");
|
||||
when(inbound.businessType()).thenReturn("SEA_LCL");
|
||||
when(inbound.fulfillmentType()).thenReturn("ONE_CLICK_DROPSHIP");
|
||||
when(inbound.lxCode()).thenReturn("LX-20260908-01");
|
||||
when(inbound.palletCount()).thenReturn(2);
|
||||
when(inbound.actualGrossWeightKg()).thenReturn(new BigDecimal("1787.500"));
|
||||
when(inbound.actualNetWeightKg()).thenReturn(new BigDecimal("1747.500"));
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
| 订舱与拖车服务中心链路 | `admin-test-env-modules/service-center-flow.mjs` | 复用本轮 `RELEASED` 订舱,创建订舱级拖车并验证附件、完整状态链、历史和稳定关联;由 Admin 总入口按依赖运行,会写目标环境 |
|
||||
| 尾程单控制与目的港码头拖车 | `admin-test-env-modules/phase4-tail-order-controls.mjs`、`admin-test-env-modules/phase5-terminal-drayage.mjs`、`admin-test-env-modules/phase4-carrier-nodes.mjs` | PDA 前覆盖整柜接柜仓变更与恢复、单/批目的仓变更、单/批合托标记、CSV 导出、订单费用、单票暂扣/解除、八个人工节点和逐票人工计划;接柜仓仅复用公共主档且恢复原值。PDA 卸柜后经正式码头拖车接口完成选车队、预约、提柜、还柜,船节点模块只读核对最终回写,不直接伪造最后两个节点 |
|
||||
| 定向 HTTP/诊断 | 以 `README.md` 的“仓库级入口清单”为准 | 必须明确是只读诊断还是会补证据、创建资源或推进状态,不能用诊断成功冒充全量通过 |
|
||||
| 本地质量与静态合同 | `backend-full-regression.ps1`、`frontend-full-regression.ps1`、`mapper-scan-contract.mjs`、`script-documentation-contract.test.mjs` 等 | 默认不访问开发或测试环境;数据库非 dry-run、Nacos 发布等有写入能力的入口必须按 README 单独识别 |
|
||||
| 本地质量与静态合同 | `backend-full-regression.ps1`、`frontend-full-regression.ps1`(含国内 PDA Node 全量测试)、`mapper-scan-contract.mjs`、`script-documentation-contract.test.mjs` 等 | 默认不访问开发或测试环境;数据库非 dry-run、Nacos 发布等有写入能力的入口必须按 README 单独识别 |
|
||||
| 部署与基础设施 | `../deploy/scripts/`、`../deploy/rocketmq/` | 靠近部署组件管理,不移动到根脚本目录;部署、初始化数据库或发布配置必须获得当前轮明确授权 |
|
||||
| 服务局部合同 | `../firstleg/scripts/`、`../lastleg-service/scripts/`、`../warehouse/scripts/` | 只服务所属模块,入口与命令登记在服务 `AGENTS.md` 或局部 README;不得散落到 `src/test/scripts/` |
|
||||
|
||||
|
||||
+5
-4
@@ -79,7 +79,7 @@ Client 全量、Client 入库测量与计费、待审核订单和批量待分柜
|
||||
| 订单与客户本地验收 | `phase1-business-maturity-regression.ps1` | 订单与客户业务成熟度回归,调用本地服务并生成报告 |
|
||||
| 订单与客户本地验收 | `phase1-onboarding-order-e2e.ps1` | 注册、资质、审核到下单的本地 E2E,会创建客户、仓库、往来单位和订单 |
|
||||
| 本地质量 | `backend-full-regression.ps1` | Mapper 门禁后执行受影响后端 Maven 测试;`-DryRun` 只打印与校验 |
|
||||
| 本地质量 | `frontend-full-regression.ps1` | 编排 Admin/Client 本地测试与类型检查,不访问 Gateway |
|
||||
| 本地质量 | `frontend-full-regression.ps1` | 编排 Admin/Client 本地测试、类型检查及国内 PDA Node 全量测试,不访问 Gateway |
|
||||
| 本地质量 | `database-comment-audit.ps1` | 离线扫描数据库中文注释,不连接数据库 |
|
||||
| 本地质量 | `local-db-full-acceptance.ps1` | 只允许本机 MySQL 的受控 SQL 验收;非 dry-run 会写本地测试库 |
|
||||
| 本地质量 | `mapper-scan-contract.mjs` | 离线检查 Mapper 注解与扫描入口,不启动 Spring、不连接数据库 |
|
||||
@@ -153,8 +153,8 @@ node scripts/create-pending-review-orders.mjs
|
||||
| 检查 | 入口 |
|
||||
|---|---|
|
||||
| 后端 Maven 契约/单元测试 | `scripts/backend-full-regression.ps1` |
|
||||
| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` |
|
||||
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
|
||||
| Admin/Client/国内 PDA 本地全量回归 | `scripts/frontend-full-regression.ps1` |
|
||||
| PDA 本地页面和状态定向测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
|
||||
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
|
||||
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、海外仓业务范围漂移、仓库主档唯一权威、订舱候选与柜量语义、整柜出仓与船节点单调性、海外 Gateway 覆盖、PDA 页面/运行配置/独立更新/语言预留/能力开关、人工分段验收链、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口与排除目录 |
|
||||
| Mapper 注册静态合同 | `node scripts/mapper-scan-contract.mjs`、`node --test scripts/mapper-scan-contract.test.mjs`;自动核对全部持久化模块的 `@Mapper`、服务根包注解过滤扫描和唯一扫描入口,不启动 Spring、不连接数据库;`backend-full-regression.ps1` 在 Maven 前强制执行 |
|
||||
@@ -182,13 +182,14 @@ Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块
|
||||
|
||||
## 前端全量回归
|
||||
|
||||
`frontend-full-regression.ps1` 只编排本地 Admin/Client 的 `pnpm test` 与 `pnpm typecheck`,不调用 Gateway、不部署、不连接数据库。
|
||||
`frontend-full-regression.ps1` 编排本地 Admin/Client 的 `pnpm test`、`pnpm typecheck`,并默认执行国内 PDA 的 `node --test tests/*.test.mjs`;不调用 Gateway、不部署、不连接数据库。
|
||||
|
||||
```powershell
|
||||
.\scripts\frontend-full-regression.ps1 -DryRun
|
||||
.\scripts\frontend-full-regression.ps1
|
||||
.\scripts\frontend-full-regression.ps1 -SkipAdmin
|
||||
.\scripts\frontend-full-regression.ps1 -SkipClient
|
||||
.\scripts\frontend-full-regression.ps1 -SkipPda
|
||||
```
|
||||
|
||||
脚本会检查 `pnpm` 和目标应用的 `package.json`;任一命令失败时立即透传退出码,不能同时跳过 Admin 和 Client。
|
||||
|
||||
@@ -887,7 +887,7 @@ $criticalPaths = @(
|
||||
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/dropship/service/DropshipOrderService.java',
|
||||
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/service/InboundOrderDetainedOutboxService.java',
|
||||
'baiyue-firstleg/common-event-contract/src/main/java/com/baiyue/common/event/OverseasInboundOrderDetainedChangedV1.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingWorkPalletService.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/DropshipPalletizationService.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/OverseasInboundOrderDetainedConsumer.java',
|
||||
'baiyue-firstleg/order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java',
|
||||
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftContainer.java',
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
param(
|
||||
[switch]$DryRun,
|
||||
[switch]$SkipAdmin,
|
||||
[switch]$SkipClient
|
||||
[switch]$SkipClient,
|
||||
[switch]$SkipPda
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -17,8 +18,22 @@ function Invoke-FrontendStep {
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
if ($SkipAdmin -and $SkipClient) { Write-Error 'At least one of Admin or Client must be selected.'; exit 2 }
|
||||
if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) { Write-Error 'pnpm was not found on PATH.'; exit 2 }
|
||||
function Invoke-PdaTests {
|
||||
param([string]$Directory)
|
||||
$pattern = Join-Path $Directory 'tests/*.test.mjs'
|
||||
Write-Host " [test] node --test `"$pattern`""
|
||||
if ($DryRun) { return }
|
||||
& node --test $pattern
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
if ($SkipAdmin -and $SkipClient -and $SkipPda) { Write-Error 'At least one of Admin, Client or PDA must be selected.'; exit 2 }
|
||||
if ((-not $SkipAdmin -or -not $SkipClient) -and -not (Get-Command pnpm -ErrorAction SilentlyContinue)) {
|
||||
Write-Error 'pnpm was not found on PATH.'; exit 2
|
||||
}
|
||||
if (-not $SkipPda -and -not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
Write-Error 'node was not found on PATH.'; exit 2
|
||||
}
|
||||
|
||||
$targets = @()
|
||||
if (-not $SkipAdmin) { $targets += @{ Name = 'Admin'; Directory = Join-Path $repoRoot 'apps/admin-web' } }
|
||||
@@ -37,4 +52,13 @@ foreach ($target in $targets) {
|
||||
Invoke-FrontendStep 'typecheck' $target.Directory 'typecheck'
|
||||
}
|
||||
|
||||
if (-not $SkipPda) {
|
||||
$pdaDirectory = Join-Path $repoRoot 'apps/pda-uniapp'
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $pdaDirectory 'tests'))) {
|
||||
Write-Error "PDA tests directory not found: $pdaDirectory"; exit 2
|
||||
}
|
||||
Write-Host '[PDA]'
|
||||
Invoke-PdaTests $pdaDirectory
|
||||
}
|
||||
|
||||
Write-Host 'Frontend regression completed.'
|
||||
|
||||
@@ -7,8 +7,16 @@ import { normalizeModuleOrder } from './admin-test-env-acceptance.mjs'
|
||||
|
||||
const scriptsDir = dirname(fileURLToPath(import.meta.url))
|
||||
const backendScript = readFileSync(resolve(scriptsDir, 'backend-full-regression.ps1'), 'utf8')
|
||||
const frontendScript = readFileSync(resolve(scriptsDir, 'frontend-full-regression.ps1'), 'utf8')
|
||||
const adminAcceptance = readFileSync(resolve(scriptsDir, 'admin-test-env-acceptance.mjs'), 'utf8')
|
||||
|
||||
test('frontend full regression includes domestic PDA node tests by default', () => {
|
||||
assert.match(frontendScript, /\[switch\]\$SkipPda/)
|
||||
assert.match(frontendScript, /apps\/pda-uniapp/)
|
||||
assert.match(frontendScript, /node --test/)
|
||||
assert.match(frontendScript, /tests\/\*\.test\.mjs/)
|
||||
})
|
||||
|
||||
test('backend full regression includes file reactor and critical file/container coverage', () => {
|
||||
assert.match(backendScript, /-pl["',\s\S]*order,firstleg,iam,gateway,file/)
|
||||
for (const required of [
|
||||
|
||||
Reference in New Issue
Block a user