Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
@@ -10450,8 +10450,22 @@ DELIMITER ;
|
|||||||
CALL assert_phase3_final_schema();
|
CALL assert_phase3_final_schema();
|
||||||
DROP PROCEDURE IF EXISTS assert_phase3_final_schema;
|
DROP PROCEDURE IF EXISTS assert_phase3_final_schema;
|
||||||
|
|
||||||
-- PDA 部分拣货释放策略:NULL/AUTO 按比例释放,MANUAL + [] 表示人工明确不释放。
|
-- PDA 整票库位块直接使用 Firstleg 托盘分配快照;旧明细级释放策略字段不再保留。
|
||||||
USE baiyue_firstleg;
|
USE baiyue_firstleg;
|
||||||
|
SET @phase3_pda_drop_release_locations = IF((SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='fl_loading_pick_item'
|
||||||
|
AND column_name='release_location_ids') > 0,
|
||||||
|
'ALTER TABLE fl_loading_pick_item DROP COLUMN release_location_ids', 'SELECT 1');
|
||||||
|
PREPARE phase3_pda_drop_release_locations_stmt FROM @phase3_pda_drop_release_locations;
|
||||||
|
EXECUTE phase3_pda_drop_release_locations_stmt;
|
||||||
|
DEALLOCATE PREPARE phase3_pda_drop_release_locations_stmt;
|
||||||
|
SET @phase3_pda_drop_release_mode = IF((SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='fl_loading_pick_item'
|
||||||
|
AND column_name='release_mode') > 0,
|
||||||
|
'ALTER TABLE fl_loading_pick_item DROP COLUMN release_mode', 'SELECT 1');
|
||||||
|
PREPARE phase3_pda_drop_release_mode_stmt FROM @phase3_pda_drop_release_mode;
|
||||||
|
EXECUTE phase3_pda_drop_release_mode_stmt;
|
||||||
|
DEALLOCATE PREPARE phase3_pda_drop_release_mode_stmt;
|
||||||
DROP PROCEDURE IF EXISTS phase3_pda_add_column;
|
DROP PROCEDURE IF EXISTS phase3_pda_add_column;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE phase3_pda_add_column(
|
CREATE PROCEDURE phase3_pda_add_column(
|
||||||
@@ -10471,14 +10485,6 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
CALL phase3_pda_add_column(
|
|
||||||
'fl_loading_pick_item', 'release_mode',
|
|
||||||
"VARCHAR(10) NULL COMMENT 'AUTO按比例释放,MANUAL按人工选择释放' AFTER item_status"
|
|
||||||
);
|
|
||||||
CALL phase3_pda_add_column(
|
|
||||||
'fl_loading_pick_item', 'release_location_ids',
|
|
||||||
"JSON NULL COMMENT '人工选择释放库位ID;空数组表示明确释放0个' AFTER release_mode"
|
|
||||||
);
|
|
||||||
CALL phase3_pda_add_column(
|
CALL phase3_pda_add_column(
|
||||||
'fl_inbound_pallet_allocation', 'block_id',
|
'fl_inbound_pallet_allocation', 'block_id',
|
||||||
"BIGINT UNSIGNED NOT NULL COMMENT 'Warehouse整票连续库位块ID' AFTER allocation_no"
|
"BIGINT UNSIGNED NOT NULL COMMENT 'Warehouse整票连续库位块ID' AFTER allocation_no"
|
||||||
@@ -13971,8 +13977,6 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'loading_pl
|
|||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'pick_task_id', 'PDA拣货任务ID');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'pick_task_id', 'PDA拣货任务ID');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'picked_package_count', '已拣货箱数');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'picked_package_count', '已拣货箱数');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'planned_package_count', '计划拣货箱数;上层拆分按业务规则向上取整');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'planned_package_count', '计划拣货箱数;上层拆分按业务规则向上取整');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_location_ids', '人工选择释放库位ID;空数组表示明确释放0个');
|
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_mode', 'AUTO按比例释放,MANUAL按人工选择释放');
|
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'updated_at', 'PDA拣货明细最后更新时间');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'updated_at', 'PDA拣货明细最后更新时间');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'version', '乐观锁版本号');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'version', '乐观锁版本号');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assigned_by', '执行领取或分配操作的用户ID');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assigned_by', '执行领取或分配操作的用户ID');
|
||||||
@@ -14991,17 +14995,24 @@ CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_opera
|
|||||||
|
|
||||||
USE baiyue_warehouse;
|
USE baiyue_warehouse;
|
||||||
CREATE TABLE IF NOT EXISTS wh_location_block (
|
CREATE TABLE IF NOT EXISTS wh_location_block (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '整票库位块ID', tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
id BIGINT UNSIGNED NOT NULL COMMENT '整票库位块ID',
|
||||||
warehouse_id BIGINT UNSIGNED NOT NULL, source_service VARCHAR(40) NOT NULL,
|
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共仓储资源域,固定为0',
|
||||||
business_id BIGINT UNSIGNED NOT NULL, inbound_code VARCHAR(40) NOT NULL,
|
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '仓库ID',
|
||||||
slot_count INT UNSIGNED NOT NULL, idempotency_key VARCHAR(128) NOT NULL,
|
source_service VARCHAR(40) NOT NULL COMMENT '来源服务',
|
||||||
block_status VARCHAR(20) NOT NULL, expires_at DATETIME(3) NOT NULL,
|
business_id BIGINT UNSIGNED NOT NULL COMMENT '当前归属入库单业务ID',
|
||||||
last_operation_key VARCHAR(128) NULL, created_by BIGINT UNSIGNED NOT NULL,
|
inbound_code VARCHAR(40) NOT NULL COMMENT '当前归属入仓码',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
slot_count INT UNSIGNED NOT NULL COMMENT '整票独占槽位数',
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
idempotency_key VARCHAR(128) NOT NULL COMMENT '预占幂等键',
|
||||||
version INT NOT NULL DEFAULT 0,
|
block_status VARCHAR(20) NOT NULL COMMENT '整票块状态:RESERVED、OCCUPIED或RELEASED',
|
||||||
|
expires_at DATETIME(3) NOT NULL COMMENT '预占过期时间',
|
||||||
|
last_operation_key VARCHAR(128) NULL COMMENT '最近确认、转移或释放幂等键',
|
||||||
|
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人或系统主体ID',
|
||||||
|
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 '更新时间',
|
||||||
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
active_business_block TINYINT GENERATED ALWAYS AS
|
active_business_block TINYINT GENERATED ALWAYS AS
|
||||||
(CASE WHEN block_status IN ('RESERVED','OCCUPIED') THEN 1 ELSE NULL END) STORED,
|
(CASE WHEN block_status IN ('RESERVED','OCCUPIED') THEN 1 ELSE NULL END) STORED
|
||||||
|
COMMENT '同一来源入库单仅允许一个活动整票块',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_location_block_idempotency (tenant_id, source_service, idempotency_key),
|
UNIQUE KEY uk_location_block_idempotency (tenant_id, source_service, idempotency_key),
|
||||||
UNIQUE KEY uk_location_block_active_business (tenant_id, source_service, business_id, active_business_block),
|
UNIQUE KEY uk_location_block_active_business (tenant_id, source_service, business_id, active_business_block),
|
||||||
@@ -15011,15 +15022,19 @@ CREATE TABLE IF NOT EXISTS wh_location_block (
|
|||||||
) ENGINE=InnoDB COMMENT='一票入仓码原子持有的稳定库位块';
|
) ENGINE=InnoDB COMMENT='一票入仓码原子持有的稳定库位块';
|
||||||
CREATE TABLE IF NOT EXISTS wh_location_block_slot (
|
CREATE TABLE IF NOT EXISTS wh_location_block_slot (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '槽位预占ID,对外作为reservationId',
|
id BIGINT UNSIGNED NOT NULL COMMENT '槽位预占ID,对外作为reservationId',
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0, block_id BIGINT UNSIGNED NOT NULL,
|
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共仓储资源域,固定为0',
|
||||||
slot_no INT UNSIGNED NOT NULL, location_id BIGINT UNSIGNED NOT NULL,
|
block_id BIGINT UNSIGNED NOT NULL COMMENT '整票库位块ID',
|
||||||
location_code VARCHAR(50) NOT NULL, shared_overflow TINYINT(1) NOT NULL DEFAULT 0,
|
slot_no INT UNSIGNED NOT NULL COMMENT '块内稳定槽位序号,从1开始',
|
||||||
resource_status VARCHAR(20) NOT NULL,
|
location_id BIGINT UNSIGNED NOT NULL COMMENT '库位ID',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
location_code VARCHAR(50) NOT NULL COMMENT '库位编码快照',
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否落入同仓共享A0',
|
||||||
version INT NOT NULL DEFAULT 0,
|
resource_status VARCHAR(20) NOT NULL COMMENT '资源状态:RESERVED、OCCUPIED或RELEASED',
|
||||||
|
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 '更新时间',
|
||||||
|
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||||
active_normal_slot TINYINT GENERATED ALWAYS AS
|
active_normal_slot TINYINT GENERATED ALWAYS AS
|
||||||
(CASE WHEN resource_status IN ('RESERVED','OCCUPIED') AND shared_overflow = 0 THEN 1 ELSE NULL END) STORED,
|
(CASE WHEN resource_status IN ('RESERVED','OCCUPIED') AND shared_overflow = 0 THEN 1 ELSE NULL END) STORED
|
||||||
|
COMMENT '普通库位活动块槽唯一标记;A0返回NULL',
|
||||||
PRIMARY KEY (id), UNIQUE KEY uk_location_block_slot_no (tenant_id, block_id, slot_no),
|
PRIMARY KEY (id), UNIQUE KEY uk_location_block_slot_no (tenant_id, block_id, slot_no),
|
||||||
UNIQUE KEY uk_location_block_active_normal (tenant_id, location_id, active_normal_slot),
|
UNIQUE KEY uk_location_block_active_normal (tenant_id, location_id, active_normal_slot),
|
||||||
KEY idx_location_block_slot_block (tenant_id, block_id, resource_status),
|
KEY idx_location_block_slot_block (tenant_id, block_id, resource_status),
|
||||||
|
|||||||
@@ -46,13 +46,6 @@ public interface WarehousePalletLocationClient {
|
|||||||
ApiResponse<BlockResult> releaseBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
ApiResponse<BlockResult> releaseBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
@RequestBody BlockReleaseRequest request);
|
@RequestBody BlockReleaseRequest request);
|
||||||
|
|
||||||
@GetMapping("/pallet-locations/occupied-resources")
|
|
||||||
ApiResponse<List<OccupiedResource>> occupiedResources(
|
|
||||||
@RequestHeader("X-Internal-Service-Token") String token,
|
|
||||||
@RequestParam("tenantId") long tenantId,
|
|
||||||
@RequestParam("sourceService") String sourceService,
|
|
||||||
@RequestParam("businessId") long businessId);
|
|
||||||
|
|
||||||
record BlockReserveRequest(long warehouseId, String sourceService, long businessId,
|
record BlockReserveRequest(long warehouseId, String sourceService, long businessId,
|
||||||
String inboundCode, int requiredSlotCount, String idempotencyKey,
|
String inboundCode, int requiredSlotCount, String idempotencyKey,
|
||||||
long operatorId, LocalDateTime expiresAt) {}
|
long operatorId, LocalDateTime expiresAt) {}
|
||||||
@@ -65,9 +58,6 @@ public interface WarehousePalletLocationClient {
|
|||||||
record BlockResource(long blockId, long reservationId, long locationId, String locationCode,
|
record BlockResource(long blockId, long reservationId, long locationId, String locationCode,
|
||||||
int slotNo, boolean sharedOverflow, String resourceStatus) {}
|
int slotNo, boolean sharedOverflow, String resourceStatus) {}
|
||||||
record BlockResult(long blockId, String blockStatus, List<BlockResource> resources) {}
|
record BlockResult(long blockId, String blockStatus, List<BlockResource> resources) {}
|
||||||
record OccupiedResource(long reservationId, long occupancyId, int allocationNo, long warehouseId,
|
|
||||||
long palletId, long locationId, String locationCode, boolean sharedOverflow,
|
|
||||||
int allocatedPackageCount) {}
|
|
||||||
record PalletSnapshot(long palletId, String palletCode, String palletType,
|
record PalletSnapshot(long palletId, String palletCode, String palletType,
|
||||||
BigDecimal capacityCbm, BigDecimal tareWeightKg) {}
|
BigDecimal capacityCbm, BigDecimal tareWeightKg) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ public final class LoadingPickDtos {
|
|||||||
|
|
||||||
public record PickItem(Long id, Long loadingPlanItemId, Long tenantId, Long inboundId, Long inboundDetailId,
|
public record PickItem(Long id, Long loadingPlanItemId, Long tenantId, Long inboundId, Long inboundDetailId,
|
||||||
String orderNo, String inboundCode, String cargoName, List<String> boxCodes,
|
String orderNo, String inboundCode, String cargoName, List<String> boxCodes,
|
||||||
|
List<InventoryResource> inventoryResources,
|
||||||
Integer plannedPackageCount, Integer pickedPackageCount,
|
Integer plannedPackageCount, Integer pickedPackageCount,
|
||||||
String itemStatus, Integer version) {
|
String itemStatus, Integer version) {
|
||||||
}
|
}
|
||||||
@@ -131,9 +132,10 @@ public final class LoadingPickDtos {
|
|||||||
Integer itemCount, Integer pendingItemCount) {
|
Integer itemCount, Integer pendingItemCount) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record InventoryResource(Long reservationId, Long occupancyId, Long palletId,
|
public record InventoryResource(Long blockId, Integer slotNo, Long reservationId,
|
||||||
|
Long weightResultId, Integer palletSequence, Long palletId,
|
||||||
Long locationId, String locationCode, Boolean sharedOverflow,
|
Long locationId, String locationCode, Boolean sharedOverflow,
|
||||||
Integer availablePackageCount) {
|
String slotStatus, Integer availablePackageCount) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record InboundInventory(Long inboundId, Integer availablePackageCount,
|
public record InboundInventory(Long inboundId, Integer availablePackageCount,
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.baiyue.firstleg.service;
|
package com.baiyue.firstleg.service;
|
||||||
|
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
|
||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
|
||||||
@@ -34,6 +32,7 @@ import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
|||||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||||
|
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||||
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
|
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
|
||||||
@@ -46,12 +45,11 @@ import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
|||||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
|
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
|
||||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||||
|
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||||
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
|
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
|
||||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import feign.FeignException;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
@@ -80,8 +78,6 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|||||||
@Service
|
@Service
|
||||||
public class LoadingPickService {
|
public class LoadingPickService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(LoadingPickService.class);
|
private static final Logger log = LoggerFactory.getLogger(LoadingPickService.class);
|
||||||
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
|
|
||||||
private static final String SOURCE_SERVICE = "firstleg-service";
|
|
||||||
@Resource
|
@Resource
|
||||||
private LoadingPickRepository repository;
|
private LoadingPickRepository repository;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -107,9 +103,7 @@ public class LoadingPickService {
|
|||||||
@Resource
|
@Resource
|
||||||
private PdaPickSettlementService settlementService;
|
private PdaPickSettlementService settlementService;
|
||||||
@Resource
|
@Resource
|
||||||
private WarehousePalletLocationClient warehouse;
|
private InboundPalletAllocationRepository palletAllocations;
|
||||||
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
|
|
||||||
private String internalToken;
|
|
||||||
|
|
||||||
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -908,14 +902,19 @@ public class LoadingPickService {
|
|||||||
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
|
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
|
||||||
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
|
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
|
||||||
.collect(Collectors.groupingBy(CartonLabelRow::getPickItemId));
|
.collect(Collectors.groupingBy(CartonLabelRow::getPickItemId));
|
||||||
|
Map<Long, Long> tenantByInbound = tenantByInbound(task.getId());
|
||||||
|
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
|
||||||
|
booking.getWarehouseId(), source, tenantByInbound);
|
||||||
List<PickItem> items = source.stream().map(item -> {
|
List<PickItem> items = source.stream().map(item -> {
|
||||||
List<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of());
|
List<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of());
|
||||||
CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0);
|
CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0);
|
||||||
return new PickItem(item.getId(), item.getLoadingPlanItemId(), first == null ? null : first.getTenantId(),
|
Long tenantId = first == null ? tenantByInbound.get(item.getInboundId()) : first.getTenantId();
|
||||||
|
return new PickItem(item.getId(), item.getLoadingPlanItemId(), tenantId,
|
||||||
item.getInboundId(),
|
item.getInboundId(),
|
||||||
item.getInboundDetailId(), first == null ? null : first.getOrderNo(),
|
item.getInboundDetailId(), first == null ? null : first.getOrderNo(),
|
||||||
first == null ? null : first.getInboundCode(), first == null ? null : first.getCargoName(),
|
first == null ? null : first.getInboundCode(), first == null ? null : first.getCargoName(),
|
||||||
cartons.stream().map(CartonLabelRow::getBoxCode).toList(), item.getPlannedPackageCount(),
|
cartons.stream().map(CartonLabelRow::getBoxCode).toList(),
|
||||||
|
inventoryByInbound.getOrDefault(item.getInboundId(), List.of()), item.getPlannedPackageCount(),
|
||||||
item.getPickedPackageCount(), item.getItemStatus(), item.getVersion());
|
item.getPickedPackageCount(), item.getItemStatus(), item.getVersion());
|
||||||
}).toList();
|
}).toList();
|
||||||
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
|
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
|
||||||
@@ -1038,17 +1037,11 @@ public class LoadingPickService {
|
|||||||
|
|
||||||
private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId,
|
private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId,
|
||||||
List<LoadingPickItemEntity> items) {
|
List<LoadingPickItemEntity> items) {
|
||||||
|
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
|
||||||
|
warehouseId, items, tenantByInbound(task.getId()));
|
||||||
List<InboundInventory> inbounds = items.stream().map(LoadingPickItemEntity::getInboundId)
|
List<InboundInventory> inbounds = items.stream().map(LoadingPickItemEntity::getInboundId)
|
||||||
.filter(Objects::nonNull).distinct().sorted().map(inboundId -> {
|
.filter(Objects::nonNull).distinct().sorted().map(inboundId -> {
|
||||||
List<WarehousePalletLocationClient.OccupiedResource> current = occupiedResources(inboundId);
|
List<InventoryResource> resources = inventoryByInbound.getOrDefault(inboundId, List.of());
|
||||||
List<InventoryResource> resources = current.stream()
|
|
||||||
.filter(resource -> resource.warehouseId() == warehouseId)
|
|
||||||
.sorted(Comparator.comparingLong(WarehousePalletLocationClient.OccupiedResource::reservationId)
|
|
||||||
.thenComparingLong(WarehousePalletLocationClient.OccupiedResource::occupancyId))
|
|
||||||
.map(resource -> new InventoryResource(resource.reservationId(), resource.occupancyId(),
|
|
||||||
resource.palletId(), resource.locationId(), resource.locationCode(),
|
|
||||||
resource.sharedOverflow(), resource.allocatedPackageCount()))
|
|
||||||
.toList();
|
|
||||||
int available = resources.stream().mapToInt(InventoryResource::availablePackageCount).sum();
|
int available = resources.stream().mapToInt(InventoryResource::availablePackageCount).sum();
|
||||||
return new InboundInventory(inboundId, available, resources);
|
return new InboundInventory(inboundId, available, resources);
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -1059,20 +1052,54 @@ public class LoadingPickService {
|
|||||||
LocalDateTime.now(), token, inbounds);
|
LocalDateTime.now(), token, inbounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<WarehousePalletLocationClient.OccupiedResource> occupiedResources(long inboundId) {
|
private Map<Long, Long> tenantByInbound(long taskId) {
|
||||||
ApiResponse<List<WarehousePalletLocationClient.OccupiedResource>> response;
|
return repository.inboundOwners(taskId).stream().collect(Collectors.toMap(
|
||||||
try {
|
LoadingPickItemMapper.InboundOwner::inboundId,
|
||||||
response = warehouse.occupiedResources(internalToken, PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID,
|
LoadingPickItemMapper.InboundOwner::tenantId,
|
||||||
SOURCE_SERVICE, inboundId);
|
(left, right) -> {
|
||||||
} catch (FeignException exception) {
|
if (!Objects.equals(left, right)) throw conflict("PDA任务入库单客户归属冲突");
|
||||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储实时库存查询失败,请刷新后重试");
|
return left;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, List<InventoryResource>> inventoryByInbound(long warehouseId,
|
||||||
|
List<LoadingPickItemEntity> items,
|
||||||
|
Map<Long, Long> tenantByInbound) {
|
||||||
|
Map<Long, List<InventoryResource>> result = new HashMap<>();
|
||||||
|
for (Long inboundId : items.stream().map(LoadingPickItemEntity::getInboundId)
|
||||||
|
.filter(Objects::nonNull).distinct().toList()) {
|
||||||
|
Long tenantId = tenantByInbound.get(inboundId);
|
||||||
|
if (tenantId == null) throw conflict("PDA任务缺少入库单客户归属");
|
||||||
|
List<InventoryResource> resources = palletAllocations.list(tenantId, inboundId).stream()
|
||||||
|
.filter(resource -> Objects.equals(resource.getWarehouseId(), warehouseId))
|
||||||
|
.filter(resource -> "CONFIRMED".equals(resource.getAllocationStatus()))
|
||||||
|
.sorted(Comparator.comparing(InboundPalletAllocationEntity::getBlockId,
|
||||||
|
Comparator.nullsLast(Long::compareTo))
|
||||||
|
.thenComparing(InboundPalletAllocationEntity::getSlotNo,
|
||||||
|
Comparator.nullsLast(Integer::compareTo)))
|
||||||
|
.map(LoadingPickService::inventoryResource)
|
||||||
|
.toList();
|
||||||
|
result.put(inboundId, resources);
|
||||||
}
|
}
|
||||||
if (response == null || !response.successful() || response.data() == null) {
|
return result;
|
||||||
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
|
}
|
||||||
throw new BizException(code, response == null || response.message() == null
|
|
||||||
? "仓储实时库存查询失败,请刷新后重试" : response.message());
|
private static InventoryResource inventoryResource(InboundPalletAllocationEntity resource) {
|
||||||
|
if (resource.getBlockId() == null || resource.getSlotNo() == null
|
||||||
|
|| resource.getReservationId() == null || resource.getLocationId() == null) {
|
||||||
|
throw conflict("入库单库位块快照不完整,请重新确认测量入库");
|
||||||
}
|
}
|
||||||
return response.data();
|
return new InventoryResource(resource.getBlockId(), resource.getSlotNo(),
|
||||||
|
resource.getReservationId(), resource.getWeightResultId(), resource.getPalletSequence(),
|
||||||
|
resource.getPalletId(), resource.getLocationId(), resource.getLocationCode(),
|
||||||
|
Boolean.TRUE.equals(resource.getSharedOverflow()), resource.getSlotStatus(), available(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int available(InboundPalletAllocationEntity resource) {
|
||||||
|
int allocated = count(resource.getAllocatedPackageCount());
|
||||||
|
int picked = count(resource.getPickedPackageCount());
|
||||||
|
if (picked > allocated) throw conflict("整票库位槽已拣箱数超过分配箱数");
|
||||||
|
return allocated - picked;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String inventoryToken(LoadingPickTaskEntity task, long warehouseId,
|
private static String inventoryToken(LoadingPickTaskEntity task, long warehouseId,
|
||||||
@@ -1082,8 +1109,9 @@ public class LoadingPickService {
|
|||||||
for (InboundInventory inbound : inbounds) {
|
for (InboundInventory inbound : inbounds) {
|
||||||
canonical.append('|').append(inbound.inboundId()).append('|').append(inbound.availablePackageCount());
|
canonical.append('|').append(inbound.inboundId()).append('|').append(inbound.availablePackageCount());
|
||||||
for (InventoryResource resource : inbound.resources()) {
|
for (InventoryResource resource : inbound.resources()) {
|
||||||
canonical.append('|').append(resource.reservationId()).append('|').append(resource.occupancyId())
|
canonical.append('|').append(resource.blockId()).append('|').append(resource.slotNo())
|
||||||
.append('|').append(resource.locationId()).append('|').append(resource.availablePackageCount());
|
.append('|').append(resource.reservationId()).append('|').append(resource.locationId())
|
||||||
|
.append('|').append(resource.availablePackageCount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hash(canonical.toString());
|
return hash(canonical.toString());
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ components:
|
|||||||
items: {$ref: '#/components/schemas/PickItem'}
|
items: {$ref: '#/components/schemas/PickItem'}
|
||||||
PickItem:
|
PickItem:
|
||||||
type: object
|
type: object
|
||||||
required: [id, loadingPlanItemId, tenantId, inboundId, inboundDetailId, boxCodes, plannedPackageCount, pickedPackageCount, itemStatus, version]
|
required: [id, loadingPlanItemId, tenantId, inboundId, inboundDetailId, boxCodes, inventoryResources, plannedPackageCount, pickedPackageCount, itemStatus, version]
|
||||||
properties:
|
properties:
|
||||||
id: {type: integer, format: int64}
|
id: {type: integer, format: int64}
|
||||||
loadingPlanItemId: {type: integer, format: int64}
|
loadingPlanItemId: {type: integer, format: int64}
|
||||||
@@ -308,6 +308,10 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
description: Stable carton label codes; one successful scan represents one carton
|
description: Stable carton label codes; one successful scan represents one carton
|
||||||
items: {type: string}
|
items: {type: string}
|
||||||
|
inventoryResources:
|
||||||
|
type: array
|
||||||
|
description: Current authoritative whole-block slot snapshots for this inbound; never projected from legacy occupancy rows
|
||||||
|
items: {$ref: '#/components/schemas/InventoryResource'}
|
||||||
plannedPackageCount: {type: integer, minimum: 1}
|
plannedPackageCount: {type: integer, minimum: 1}
|
||||||
pickedPackageCount: {type: integer, minimum: 0}
|
pickedPackageCount: {type: integer, minimum: 0}
|
||||||
itemStatus: {type: string, enum: [PENDING, PICKING, PICKED, PARTIAL, SKIPPED]}
|
itemStatus: {type: string, enum: [PENDING, PICKING, PICKED, PARTIAL, SKIPPED]}
|
||||||
@@ -427,12 +431,16 @@ components:
|
|||||||
items: {$ref: '#/components/schemas/InventoryResource'}
|
items: {$ref: '#/components/schemas/InventoryResource'}
|
||||||
InventoryResource:
|
InventoryResource:
|
||||||
type: object
|
type: object
|
||||||
required: [reservationId, occupancyId, palletId, locationId, sharedOverflow, availablePackageCount]
|
required: [blockId, slotNo, reservationId, locationId, sharedOverflow, slotStatus, availablePackageCount]
|
||||||
properties:
|
properties:
|
||||||
|
blockId: {type: integer, format: int64}
|
||||||
|
slotNo: {type: integer, minimum: 1}
|
||||||
reservationId: {type: integer, format: int64}
|
reservationId: {type: integer, format: int64}
|
||||||
occupancyId: {type: integer, format: int64}
|
weightResultId: {type: integer, format: int64, nullable: true}
|
||||||
palletId: {type: integer, format: int64}
|
palletSequence: {type: integer, minimum: 1, nullable: true}
|
||||||
|
palletId: {type: integer, format: int64, nullable: true}
|
||||||
locationId: {type: integer, format: int64}
|
locationId: {type: integer, format: int64}
|
||||||
locationCode: {type: string, nullable: true}
|
locationCode: {type: string, nullable: true}
|
||||||
sharedOverflow: {type: boolean}
|
sharedOverflow: {type: boolean}
|
||||||
availablePackageCount: {type: integer, minimum: 1}
|
slotStatus: {type: string, enum: [RESERVED_EMPTY, BOUND]}
|
||||||
|
availablePackageCount: {type: integer, minimum: 0}
|
||||||
|
|||||||
+7
@@ -52,6 +52,8 @@ class PdaConvergenceOpenApiContractTest {
|
|||||||
String batchItemRequest = yaml.substring(
|
String batchItemRequest = yaml.substring(
|
||||||
yaml.indexOf(" BatchItemRequest:"),
|
yaml.indexOf(" BatchItemRequest:"),
|
||||||
yaml.indexOf(" ReviewRequest:"));
|
yaml.indexOf(" ReviewRequest:"));
|
||||||
|
String inventoryResource = yaml.substring(
|
||||||
|
yaml.indexOf(" InventoryResource:"));
|
||||||
assertThat(taskLookup)
|
assertThat(taskLookup)
|
||||||
.contains("name: cabinetNo", "name: loadingPlanId")
|
.contains("name: cabinetNo", "name: loadingPlanId")
|
||||||
.doesNotContain("name: containerNo");
|
.doesNotContain("name: containerNo");
|
||||||
@@ -64,5 +66,10 @@ class PdaConvergenceOpenApiContractTest {
|
|||||||
.doesNotContain("containerNo:");
|
.doesNotContain("containerNo:");
|
||||||
assertThat(batchItemRequest)
|
assertThat(batchItemRequest)
|
||||||
.doesNotContain("releaseMode:", "reservationIds:", "locationIds:");
|
.doesNotContain("releaseMode:", "reservationIds:", "locationIds:");
|
||||||
|
assertThat(inventoryResource)
|
||||||
|
.contains("blockId:", "slotNo:", "reservationId:", "weightResultId:", "slotStatus:")
|
||||||
|
.doesNotContain("occupancyId:");
|
||||||
|
assertThat(yaml).contains("inventoryResources:",
|
||||||
|
"never projected from legacy occupancy rows");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@ class LoadingPickItemMapperContractTest {
|
|||||||
void pdaWritesAndReadsRequireWarehouseScope() throws Exception {
|
void pdaWritesAndReadsRequireWarehouseScope() throws Exception {
|
||||||
Method list = LoadingPickItemMapper.class.getDeclaredMethod("selectTaskItemsWithAccount", long.class, long.class);
|
Method list = LoadingPickItemMapper.class.getDeclaredMethod("selectTaskItemsWithAccount", long.class, long.class);
|
||||||
Method pick = LoadingPickItemMapper.class.getDeclaredMethod("pick", long.class, int.class,
|
Method pick = LoadingPickItemMapper.class.getDeclaredMethod("pick", long.class, int.class,
|
||||||
String.class, String.class, int.class, long.class);
|
int.class, long.class);
|
||||||
|
|
||||||
assertThat(list.getAnnotation(Select.class).value()[0])
|
assertThat(list.getAnnotation(Select.class).value()[0])
|
||||||
.contains("booking.warehouse_id = #{warehouseId}")
|
.contains("booking.warehouse_id = #{warehouseId}")
|
||||||
|
|||||||
+14
-3
@@ -157,11 +157,22 @@ class PdaSplitSettlementSqlContractTest {
|
|||||||
.contains("allocation_status = 'CONFIRMED'")
|
.contains("allocation_status = 'CONFIRMED'")
|
||||||
.doesNotContain("THEN 'RELEASED'");
|
.doesNotContain("THEN 'RELEASED'");
|
||||||
|
|
||||||
String release = InboundPalletAllocationMapper.class.getMethod("markReleased",
|
String release = InboundPalletAllocationMapper.class.getMethod("markBlockReleased",
|
||||||
long.class, long.class, long.class)
|
long.class, long.class, long.class)
|
||||||
.getAnnotation(Update.class).value()[0];
|
.getAnnotation(Update.class).value()[0];
|
||||||
assertThat(release)
|
assertThat(release)
|
||||||
.contains("allocation_status = 'RELEASED'")
|
.contains("allocation_status='RELEASED'")
|
||||||
.contains("allocation_status = 'CONFIRMED'");
|
.contains("slot_status='RELEASED'")
|
||||||
|
.contains("block_id=#{blockId}")
|
||||||
|
.contains("allocation_status='CONFIRMED'");
|
||||||
|
|
||||||
|
String transfer = InboundPalletAllocationMapper.class.getMethod("markBlockTransferred",
|
||||||
|
long.class, long.class, long.class)
|
||||||
|
.getAnnotation(Update.class).value()[0];
|
||||||
|
assertThat(transfer)
|
||||||
|
.contains("allocation_status='TRANSFERRED'")
|
||||||
|
.contains("slot_status='TRANSFERRED'")
|
||||||
|
.contains("block_id=#{blockId}")
|
||||||
|
.contains("allocation_status='CONFIRMED'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -252,6 +252,21 @@ class Phase3FinalUnifiedSqlContractTest {
|
|||||||
.contains("CREATE PROCEDURE phase3_pda_replace_operation_check");
|
.contains("CREATE PROCEDURE phase3_pda_replace_operation_check");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unifiedPdaBlockDropsLegacyPerItemReleaseColumns() throws Exception {
|
||||||
|
String sql = normalise(Files.readString(resolveUnified()));
|
||||||
|
|
||||||
|
assertThat(sql)
|
||||||
|
.contains("column_name='release_location_ids'")
|
||||||
|
.contains("ALTER TABLE fl_loading_pick_item DROP COLUMN release_location_ids")
|
||||||
|
.contains("column_name='release_mode'")
|
||||||
|
.contains("ALTER TABLE fl_loading_pick_item DROP COLUMN release_mode")
|
||||||
|
.doesNotContain("'fl_loading_pick_item', 'release_mode'")
|
||||||
|
.doesNotContain("'fl_loading_pick_item', 'release_location_ids'")
|
||||||
|
.doesNotContain("CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_mode'")
|
||||||
|
.doesNotContain("CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_location_ids'");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void legacySplitIndexesAreOnlyDroppedAndNeverRecreated() throws Exception {
|
void legacySplitIndexesAreOnlyDroppedAndNeverRecreated() throws Exception {
|
||||||
String sql = normalise(Files.readString(resolveUnified()));
|
String sql = normalise(Files.readString(resolveUnified()));
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
|
|||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
|
||||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||||
@@ -33,6 +32,7 @@ import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
|
|||||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||||
|
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||||
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
|
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
|
||||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||||
@@ -100,7 +100,7 @@ class LoadingPickServiceTest {
|
|||||||
@MockBean
|
@MockBean
|
||||||
private PdaPickSettlementService settlementService;
|
private PdaPickSettlementService settlementService;
|
||||||
@MockBean
|
@MockBean
|
||||||
private WarehousePalletLocationClient warehouse;
|
private InboundPalletAllocationRepository palletAllocations;
|
||||||
@Resource
|
@Resource
|
||||||
private LoadingPickService service;
|
private LoadingPickService service;
|
||||||
|
|
||||||
@@ -122,6 +122,10 @@ class LoadingPickServiceTest {
|
|||||||
when(completionCommands.insert(any())).thenReturn(1);
|
when(completionCommands.insert(any())).thenReturn(1);
|
||||||
when(completionCommands.renew(anyLong(), anyString(), any(java.time.LocalDateTime.class))).thenReturn(true);
|
when(completionCommands.renew(anyLong(), anyString(), any(java.time.LocalDateTime.class))).thenReturn(true);
|
||||||
when(repository.acceptCompletion(anyLong(), anyInt())).thenReturn(1);
|
when(repository.acceptCompletion(anyLong(), anyInt())).thenReturn(1);
|
||||||
|
when(repository.inboundOwners(anyLong())).thenReturn(List.of(
|
||||||
|
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||||
|
when(repository.inbound(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||||
|
new LoadingInboundMapper.Snapshot(invocation.getArgument(1), invocation.getArgument(0), "LOADING", 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -437,6 +441,7 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.allTaskItemsResolved(20L)).thenReturn(false);
|
when(repository.allTaskItemsResolved(20L)).thenReturn(false);
|
||||||
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||||
|
when(palletAllocations.list(101L, 50L)).thenReturn(List.of(allocation(50L, 2)));
|
||||||
when(repository.inbound(101L, 50L)).thenReturn(
|
when(repository.inbound(101L, 50L)).thenReturn(
|
||||||
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
|
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
|
||||||
var result = service.task(20L, 101L);
|
var result = service.task(20L, 101L);
|
||||||
@@ -449,6 +454,14 @@ class LoadingPickServiceTest {
|
|||||||
assertThat(result.sourceType()).isEqualTo("SIMULATION");
|
assertThat(result.sourceType()).isEqualTo("SIMULATION");
|
||||||
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
|
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
|
||||||
assertThat(result.mergedIntoContainerBookingId()).isNull();
|
assertThat(result.mergedIntoContainerBookingId()).isNull();
|
||||||
|
assertThat(result.items().get(0).inventoryResources()).singleElement().satisfies(resource -> {
|
||||||
|
assertThat(resource.blockId()).isEqualTo(71L);
|
||||||
|
assertThat(resource.slotNo()).isEqualTo(1);
|
||||||
|
assertThat(resource.reservationId()).isEqualTo(72L);
|
||||||
|
assertThat(resource.locationId()).isEqualTo(74L);
|
||||||
|
assertThat(resource.availablePackageCount()).isEqualTo(2);
|
||||||
|
});
|
||||||
|
verify(palletAllocations).list(101L, 50L);
|
||||||
verify(completionCommands, never()).latest(20L);
|
verify(completionCommands, never()).latest(20L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +471,9 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.task(20L)).thenReturn(task);
|
when(repository.task(20L)).thenReturn(task);
|
||||||
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
|
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
|
||||||
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
||||||
when(repository.inboundOwners(20L)).thenReturn(List.of());
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
|
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||||
|
when(palletAllocations.list(101L, 50L)).thenReturn(List.of());
|
||||||
doThrow(new IllegalStateException("completion command schema unavailable"))
|
doThrow(new IllegalStateException("completion command schema unavailable"))
|
||||||
.when(completionCommands).latest(20L);
|
.when(completionCommands).latest(20L);
|
||||||
|
|
||||||
@@ -1150,10 +1165,17 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||||
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||||
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
new LoadingPickItemMapper.InboundOwner(1L, 50L),
|
||||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
new LoadingPickItemMapper.InboundOwner(1L, 51L)));
|
||||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||||
|
List.of(allocation(invocation.getArgument(1), 2)));
|
||||||
|
var snapshot = service.inventorySnapshot(20L, 101L);
|
||||||
|
String snapshotToken = snapshot.snapshotToken();
|
||||||
|
assertThat(snapshot.inbounds()).hasSize(2);
|
||||||
|
assertThat(snapshot.inbounds().get(0).resources().get(0))
|
||||||
|
.extracting("blockId", "slotNo", "reservationId", "locationId", "availablePackageCount")
|
||||||
|
.containsExactly(71L, 1, 72L, 74L, 2);
|
||||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
||||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null),
|
new BatchItemRequest(30L, "PICK", 2, null, 0, null),
|
||||||
new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton")));
|
new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton")));
|
||||||
@@ -1191,9 +1213,10 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||||
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
|
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
|
||||||
when(smartLoadingRepository.operation("PDA-START-20")).thenReturn(existingStart);
|
when(smartLoadingRepository.operation("PDA-START-20")).thenReturn(existingStart);
|
||||||
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
new LoadingPickItemMapper.InboundOwner(1L, 50L)));
|
||||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 100))));
|
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||||
|
List.of(allocation(invocation.getArgument(1), 100)));
|
||||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
|
||||||
new BatchItemRequest(30L, "PICK", 100, null, 0, null)));
|
new BatchItemRequest(30L, "PICK", 100, null, 0, null)));
|
||||||
@@ -1242,9 +1265,10 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.assignment(20L)).thenReturn(assignment());
|
when(repository.assignment(20L)).thenReturn(assignment());
|
||||||
when(repository.items(20L)).thenReturn(List.of(picked));
|
when(repository.items(20L)).thenReturn(List.of(picked));
|
||||||
when(repository.items(20L, 101L)).thenReturn(List.of(picked));
|
when(repository.items(20L, 101L)).thenReturn(List.of(picked));
|
||||||
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
new LoadingPickItemMapper.InboundOwner(1L, 50L)));
|
||||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||||
|
List.of(allocation(invocation.getArgument(1), 2)));
|
||||||
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of(
|
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of(
|
||||||
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
|
||||||
|
|
||||||
@@ -1285,9 +1309,10 @@ class LoadingPickServiceTest {
|
|||||||
when(repository.pick(30L, 2, 0, 101L)).thenReturn(1);
|
when(repository.pick(30L, 2, 0, 101L)).thenReturn(1);
|
||||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||||
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
|
new LoadingPickItemMapper.InboundOwner(1L, 50L)));
|
||||||
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
|
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
|
||||||
|
List.of(allocation(invocation.getArgument(1), 2)));
|
||||||
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
|
||||||
BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null);
|
BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null);
|
||||||
|
|
||||||
@@ -1405,6 +1430,27 @@ class LoadingPickServiceTest {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static InboundPalletAllocationEntity allocation(long inboundId, int packageCount) {
|
||||||
|
InboundPalletAllocationEntity allocation = new InboundPalletAllocationEntity();
|
||||||
|
allocation.setId(72L);
|
||||||
|
allocation.setTenantId(1L);
|
||||||
|
allocation.setInboundId(inboundId);
|
||||||
|
allocation.setBlockId(71L);
|
||||||
|
allocation.setSlotNo(1);
|
||||||
|
allocation.setAllocationNo(1);
|
||||||
|
allocation.setWarehouseId(101L);
|
||||||
|
allocation.setPalletId(73L);
|
||||||
|
allocation.setLocationId(74L);
|
||||||
|
allocation.setLocationCode("A-01");
|
||||||
|
allocation.setSharedOverflow(false);
|
||||||
|
allocation.setReservationId(72L);
|
||||||
|
allocation.setSlotStatus("BOUND");
|
||||||
|
allocation.setAllocationStatus("CONFIRMED");
|
||||||
|
allocation.setAllocatedPackageCount(packageCount);
|
||||||
|
allocation.setPickedPackageCount(0);
|
||||||
|
return allocation;
|
||||||
|
}
|
||||||
|
|
||||||
private static LoadingPickAssignmentEntity assignment() {
|
private static LoadingPickAssignmentEntity assignment() {
|
||||||
LoadingPickAssignmentEntity assignment = new LoadingPickAssignmentEntity();
|
LoadingPickAssignmentEntity assignment = new LoadingPickAssignmentEntity();
|
||||||
assignment.setPickTaskId(20L);
|
assignment.setPickTaskId(20L);
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ function usage() {
|
|||||||
--wait-split-package-count <n> 单票指定总箱数,1-1000;省略时使用既有轮换
|
--wait-split-package-count <n> 单票指定总箱数,1-1000;省略时使用既有轮换
|
||||||
--wait-split-sequence <n> 单票全局序号,正整数,默认1
|
--wait-split-sequence <n> 单票全局序号,正整数,默认1
|
||||||
--wait-split-random-value <n> 单票随机选择值,正整数,默认使用全局序号
|
--wait-split-random-value <n> 单票随机选择值,正整数,默认使用全局序号
|
||||||
|
--verify-pallet-location-guide <true> 验证整票库位块、图片槽绑定与删除后 RESERVED_EMPTY
|
||||||
--route-mode <mode> 线路选择:fixed 或 random,默认fixed
|
--route-mode <mode> 线路选择:fixed 或 random,默认fixed
|
||||||
--manual-split-fixture-report <path> 人工分柜准备报告路径,仅只读核验模块使用
|
--manual-split-fixture-report <path> 人工分柜准备报告路径,仅只读核验模块使用
|
||||||
--manual-split-plan-id <id> 人工操作产生的智能装柜计划 ID,仅只读核验模块使用
|
--manual-split-plan-id <id> 人工操作产生的智能装柜计划 ID,仅只读核验模块使用
|
||||||
@@ -348,6 +349,7 @@ export function parseArguments(argv) {
|
|||||||
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
|
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
|
||||||
waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
|
waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
|
||||||
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
|
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
|
||||||
|
verifyPalletLocationGuide: values.has('verify-pallet-location-guide'),
|
||||||
routeMode: values.get('route-mode') || 'fixed',
|
routeMode: values.get('route-mode') || 'fixed',
|
||||||
quiet: values.get('quiet') === 'true',
|
quiet: values.get('quiet') === 'true',
|
||||||
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
|
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" s
|
|||||||
node scripts/admin-test-env-acceptance.mjs --modules merchant-qualification-fixture,orders,inbound-measurement,phase3-container-loading,phase3-loading,phase3-container-overview,phase3-pda-convergence,phase3-evidence-gate,phase3-customs,phase3-loading-finalize
|
node scripts/admin-test-env-acceptance.mjs --modules merchant-qualification-fixture,orders,inbound-measurement,phase3-container-loading,phase3-loading,phase3-container-overview,phase3-pda-convergence,phase3-evidence-gate,phase3-customs,phase3-loading-finalize
|
||||||
```
|
```
|
||||||
|
|
||||||
|
整票库位块专项可在只跑 `orders,inbound-measurement` 时追加
|
||||||
|
`--verify-pallet-location-guide true`。它在测量提交后核对
|
||||||
|
`palletLocationGuide` 计数、每槽 `blockId/slotNo/reservationId/locationCode/slotStatus`、
|
||||||
|
称重结果按 `weightResultId` 显式绑定槽位,并真实验证删除图片后原 reservation/location
|
||||||
|
保留且转为 `RESERVED_EMPTY`。脚本只观测自然出现的共享 A0;不会为制造普通位不足批量停用库位。
|
||||||
|
|
||||||
只验证真实集装箱号晚补门禁时,使用正式可选参数 `LATE_CONTAINER_GATE_ONLY=1` 并仍从总入口执行 `phase3-loading`。该模式使用本轮 marker 独立造数,固定验证无号 push、订舱关联、整份预装确认、PDA claim/batch、无号 complete `A0409`、LOADING 阶段首次补号和补号后 complete 受理;随后允许后台命令停在 `CABINET_EVIDENCE/A0409`,以证明柜号门禁已经消失且下一门禁来自未上传的五类柜级证据。该模式不继续完整报关或 LOADED 链,也不得与完整链同时解释为同一终态验收。
|
只验证真实集装箱号晚补门禁时,使用正式可选参数 `LATE_CONTAINER_GATE_ONLY=1` 并仍从总入口执行 `phase3-loading`。该模式使用本轮 marker 独立造数,固定验证无号 push、订舱关联、整份预装确认、PDA claim/batch、无号 complete `A0409`、LOADING 阶段首次补号和补号后 complete 受理;随后允许后台命令停在 `CABINET_EVIDENCE/A0409`,以证明柜号门禁已经消失且下一门禁来自未上传的五类柜级证据。该模式不继续完整报关或 LOADED 链,也不得与完整链同时解释为同一终态验收。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -69,6 +69,16 @@ test('demo package keeps pallet pages bounded and historical snapshots read-only
|
|||||||
assert.match(measurementTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/)
|
assert.match(measurementTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('opt-in HTTP acceptance verifies whole-ticket slot identity and delete-to-empty semantics', () => {
|
||||||
|
assert.match(source, /verifyPalletLocationGuide/)
|
||||||
|
assert.match(source, /expectedWeightResultIds\.every/)
|
||||||
|
assert.match(source, /asId\(row\.reservationId\) === asId\(slot\.reservationId\)/)
|
||||||
|
assert.match(source, /\['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'\]/)
|
||||||
|
assert.match(source, /deleteSucceeded/)
|
||||||
|
assert.match(source, /retainedSlot\?\.slotStatus === 'RESERVED_EMPTY'/)
|
||||||
|
assert.match(source, /不批量停用普通库位制造容量不足/)
|
||||||
|
})
|
||||||
|
|
||||||
test('approval freezes measurement and requires quantity weight and volume conservation', () => {
|
test('approval freezes measurement and requires quantity weight and volume conservation', () => {
|
||||||
assert.match(source, /审核及合托完成后箱码尺寸与图片快照不可再覆盖/)
|
assert.match(source, /审核及合托完成后箱码尺寸与图片快照不可再覆盖/)
|
||||||
assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/)
|
assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/)
|
||||||
|
|||||||
@@ -271,7 +271,10 @@ async function createConfirmedWeightEvidence(context, {
|
|||||||
staleConfirmResponse.http === 200 && String(staleConfirmResponse.body?.code) === 'A0409',
|
staleConfirmResponse.http === 200 && String(staleConfirmResponse.body?.code) === 'A0409',
|
||||||
`HTTP=${staleConfirmResponse.http}, code=${staleConfirmResponse.body?.code ?? 'missing'}, requestId=${staleConfirmResponse.requestId || 'missing'}`,
|
`HTTP=${staleConfirmResponse.http}, code=${staleConfirmResponse.body?.code ?? 'missing'}, requestId=${staleConfirmResponse.requestId || 'missing'}`,
|
||||||
OPTIONS)
|
OPTIONS)
|
||||||
confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg })
|
confirmedResults.push({
|
||||||
|
id: asId(created.id), palletId, netWeightKg, grossWeightKg,
|
||||||
|
version: Number(confirmed?.version),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceResponse = await context.request('GET',
|
const workspaceResponse = await context.request('GET',
|
||||||
@@ -292,7 +295,41 @@ async function createConfirmedWeightEvidence(context, {
|
|||||||
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
|
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
|
||||||
`confirmedImages=${confirmedRows.length}, expectedPallets=${palletIds.length}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
|
`confirmedImages=${confirmedRows.length}, expectedPallets=${palletIds.length}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
|
||||||
OPTIONS)
|
OPTIONS)
|
||||||
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg } : null
|
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg, confirmedResults } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyPalletLocationGuide(context, response, workspace, expectedWeightResultIds, name) {
|
||||||
|
const guide = workspace?.palletLocationGuide
|
||||||
|
const slots = Array.isArray(guide?.slots) ? guide.slots : []
|
||||||
|
const weights = Array.isArray(workspace?.weightResults) ? workspace.weightResults : []
|
||||||
|
const boundSlots = slots.filter((slot) => asId(slot?.boundWeightResultId))
|
||||||
|
const emptySlots = slots.filter((slot) => slot?.slotStatus === 'RESERVED_EMPTY')
|
||||||
|
const identityComplete = asId(guide?.blockId)
|
||||||
|
&& slots.length === Number(guide?.reservedSlotCount)
|
||||||
|
&& Number(guide?.actualPalletCount) === boundSlots.length
|
||||||
|
&& Number(guide?.reservedEmptySlotCount) === emptySlots.length
|
||||||
|
&& slots.every((slot) => Number.isInteger(Number(slot?.slotNo)) && Number(slot.slotNo) > 0
|
||||||
|
&& asId(slot?.reservationId) && asId(slot?.locationId)
|
||||||
|
&& String(slot?.locationCode || '').trim() && typeof slot?.sharedOverflow === 'boolean'
|
||||||
|
&& ['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'].includes(String(slot?.slotStatus || '')))
|
||||||
|
context.assert(MODULE, `${name}整票库位块计数与槽身份完整`, response, Boolean(identityComplete),
|
||||||
|
`blockId=${asId(guide?.blockId) || 'missing'}, reserved=${guide?.reservedSlotCount ?? 'missing'}, actual=${guide?.actualPalletCount ?? 'missing'}, empty=${guide?.reservedEmptySlotCount ?? 'missing'}, slots=${slots.length}`,
|
||||||
|
OPTIONS)
|
||||||
|
|
||||||
|
const bindingsStable = expectedWeightResultIds.every((resultId) => {
|
||||||
|
const row = weights.find((item) => asId(item?.id) === resultId)
|
||||||
|
const slot = slots.find((item) => asId(item?.boundWeightResultId) === resultId)
|
||||||
|
return row && slot && asId(row.blockId) === asId(guide?.blockId)
|
||||||
|
&& Number(row.slotNo) === Number(slot.slotNo)
|
||||||
|
&& asId(row.reservationId) === asId(slot.reservationId)
|
||||||
|
&& asId(row.locationId) === asId(slot.locationId)
|
||||||
|
&& String(row.locationCode || '') === String(slot.locationCode || '')
|
||||||
|
&& Boolean(row.sharedOverflow) === Boolean(slot.sharedOverflow)
|
||||||
|
})
|
||||||
|
context.assert(MODULE, `${name}称重图片按 resultId 显式绑定槽位`, response, bindingsStable,
|
||||||
|
`expectedResultIds=${expectedWeightResultIds.join('|') || 'none'}, boundResultIds=${boundSlots.map((slot) => asId(slot.boundWeightResultId)).join('|') || 'none'}`,
|
||||||
|
OPTIONS)
|
||||||
|
return { guide, slots, weights, identityComplete: Boolean(identityComplete), bindingsStable }
|
||||||
}
|
}
|
||||||
|
|
||||||
function findIsolatedInbound(context) {
|
function findIsolatedInbound(context) {
|
||||||
@@ -708,6 +745,62 @@ export async function run(context) {
|
|||||||
submitted?.processStatus === 'MEASURE_PENDING' && submitted?.measureReviewStatus === 'PENDING',
|
submitted?.processStatus === 'MEASURE_PENDING' && submitted?.measureReviewStatus === 'PENDING',
|
||||||
`processStatus=${submitted?.processStatus ?? 'missing'}, reviewStatus=${submitted?.measureReviewStatus ?? 'missing'}`, OPTIONS)
|
`processStatus=${submitted?.processStatus ?? 'missing'}, reviewStatus=${submitted?.measureReviewStatus ?? 'missing'}`, OPTIONS)
|
||||||
|
|
||||||
|
if (context.options?.verifyPalletLocationGuide) {
|
||||||
|
const guideResponse = await context.request('GET', measurementPath)
|
||||||
|
const guideWorkspace = context.apiSuccess(MODULE, '提交后读取整票库位块与称重图片槽绑定', guideResponse,
|
||||||
|
`inboundId=${inboundId}`, OPTIONS)
|
||||||
|
const beforeDelete = verifyPalletLocationGuide(context, guideResponse, guideWorkspace,
|
||||||
|
weightEvidence.confirmedResults.map((item) => item.id), '删除前')
|
||||||
|
const overflowSlots = beforeDelete.slots.filter((slot) => slot?.sharedOverflow === true)
|
||||||
|
context.addStep(MODULE, '正式数据 A0 共享溢出观测', guideResponse, true,
|
||||||
|
overflowSlots.length > 0
|
||||||
|
? `sharedOverflow=true, slots=${overflowSlots.map((slot) => `${slot.slotNo}:${slot.reservationId}:${slot.locationCode}`).join('|')}`
|
||||||
|
: '本票未触发 A0;为保护共享测试仓,不批量停用普通库位制造容量不足',
|
||||||
|
{ scenario: SCENARIO })
|
||||||
|
|
||||||
|
const deletedWeight = weightEvidence.confirmedResults[0]
|
||||||
|
const deleteResponse = await context.request('DELETE',
|
||||||
|
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${deletedWeight.id}?${query(tenantId)}&version=${deletedWeight.version}`)
|
||||||
|
const deleteSucceeded = deleteResponse.http === 200 && String(deleteResponse.body?.code) === '200'
|
||||||
|
context.assert(MODULE, '删除称重图片后保留原 reservation/location 并释放为空槽', deleteResponse,
|
||||||
|
deleteSucceeded,
|
||||||
|
`HTTP=${deleteResponse.http}, code=${deleteResponse.body?.code ?? 'missing'}, message=${deleteResponse.body?.message ?? 'missing'}, requestId=${deleteResponse.requestId || 'missing'}, traceId=${deleteResponse.traceId || 'missing'}`,
|
||||||
|
OPTIONS)
|
||||||
|
if (deleteSucceeded) {
|
||||||
|
const afterResponse = await context.request('GET', measurementPath)
|
||||||
|
const afterWorkspace = context.apiSuccess(MODULE, '删除后回查整票库位块', afterResponse,
|
||||||
|
`deletedWeightResultId=${deletedWeight.id}`, OPTIONS)
|
||||||
|
const afterDelete = verifyPalletLocationGuide(context, afterResponse, afterWorkspace,
|
||||||
|
weightEvidence.confirmedResults.slice(1).map((item) => item.id), '删除后')
|
||||||
|
const previousSlot = beforeDelete.slots.find((slot) => asId(slot?.boundWeightResultId) === deletedWeight.id)
|
||||||
|
const retainedSlot = afterDelete.slots.find((slot) =>
|
||||||
|
asId(slot?.reservationId) === asId(previousSlot?.reservationId)
|
||||||
|
&& asId(slot?.locationId) === asId(previousSlot?.locationId))
|
||||||
|
context.assert(MODULE, '删除只解绑图片且原槽变为 RESERVED_EMPTY', afterResponse,
|
||||||
|
Boolean(previousSlot) && retainedSlot?.slotStatus === 'RESERVED_EMPTY'
|
||||||
|
&& !asId(retainedSlot?.boundWeightResultId)
|
||||||
|
&& Number(afterDelete.guide?.reservedSlotCount) === Number(beforeDelete.guide?.reservedSlotCount),
|
||||||
|
`reservationId=${asId(retainedSlot?.reservationId) || 'missing'}, locationId=${asId(retainedSlot?.locationId) || 'missing'}, slotStatus=${retainedSlot?.slotStatus || 'missing'}`,
|
||||||
|
OPTIONS)
|
||||||
|
context.createdResources.inboundMeasurementLocationGuide = {
|
||||||
|
marker: context.marker, inboundId, tenantId, blockId: asId(afterDelete.guide?.blockId),
|
||||||
|
deletedWeightResultId: deletedWeight.id, deleteSucceeded: true,
|
||||||
|
sharedOverflowObserved: afterDelete.slots.some((slot) => slot?.sharedOverflow === true),
|
||||||
|
}
|
||||||
|
finishScenario(context, start)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
context.createdResources.inboundMeasurementLocationGuide = {
|
||||||
|
marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId),
|
||||||
|
deletedWeightResultId: deletedWeight.id, deleteSucceeded: false,
|
||||||
|
deleteCode: deleteResponse.body?.code || null,
|
||||||
|
deleteMessage: deleteResponse.body?.message || null,
|
||||||
|
deleteRequestId: deleteResponse.requestId || null,
|
||||||
|
deleteTraceId: deleteResponse.traceId || null,
|
||||||
|
sharedOverflowObserved: overflowSlots.length > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const rejectedScanResponse = await multipartRequest(context, scanPath, {
|
const rejectedScanResponse = await multipartRequest(context, scanPath, {
|
||||||
boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-03', measuredAt: new Date().toISOString().slice(0, 19),
|
boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-03', measuredAt: new Date().toISOString().slice(0, 19),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -124,8 +124,10 @@ function inventorySignature(snapshot) {
|
|||||||
inboundId: id(inbound?.inboundId),
|
inboundId: id(inbound?.inboundId),
|
||||||
availablePackageCount: Number(inbound?.availablePackageCount),
|
availablePackageCount: Number(inbound?.availablePackageCount),
|
||||||
resources: (inbound?.resources || []).map((resource) => ({
|
resources: (inbound?.resources || []).map((resource) => ({
|
||||||
reservationId: id(resource?.reservationId), occupancyId: id(resource?.occupancyId),
|
blockId: id(resource?.blockId), slotNo: Number(resource?.slotNo),
|
||||||
palletId: id(resource?.palletId), locationId: id(resource?.locationId),
|
reservationId: id(resource?.reservationId), weightResultId: id(resource?.weightResultId),
|
||||||
|
palletSequence: Number(resource?.palletSequence), palletId: id(resource?.palletId),
|
||||||
|
locationId: id(resource?.locationId), slotStatus: resource?.slotStatus || null,
|
||||||
sharedOverflow: resource?.sharedOverflow === true,
|
sharedOverflow: resource?.sharedOverflow === true,
|
||||||
availablePackageCount: Number(resource?.availablePackageCount),
|
availablePackageCount: Number(resource?.availablePackageCount),
|
||||||
})),
|
})),
|
||||||
@@ -622,8 +624,9 @@ export async function run(context) {
|
|||||||
&& id(inventory?.warehouseId) === warehouseId
|
&& id(inventory?.warehouseId) === warehouseId
|
||||||
&& Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string'
|
&& Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string'
|
||||||
&& inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1
|
&& inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1
|
||||||
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.reservationId)
|
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.blockId)
|
||||||
&& id(resource?.occupancyId) && id(resource?.palletId) && id(resource?.locationId)
|
&& Number(resource?.slotNo) >= 1 && id(resource?.reservationId) && id(resource?.locationId)
|
||||||
|
&& ['RESERVED_EMPTY', 'BOUND'].includes(resource?.slotStatus)
|
||||||
&& Number(resource?.availablePackageCount) >= 0)
|
&& Number(resource?.availablePackageCount) >= 0)
|
||||||
&& Number(inventoryInbound?.availablePackageCount) === (inventoryInbound?.resources || [])
|
&& Number(inventoryInbound?.availablePackageCount) === (inventoryInbound?.resources || [])
|
||||||
.reduce((total, resource) => total + Number(resource?.availablePackageCount || 0), 0)
|
.reduce((total, resource) => total + Number(resource?.availablePackageCount || 0), 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user