优化
This commit is contained in:
@@ -6183,14 +6183,56 @@ ON DUPLICATE KEY UPDATE
|
||||
color = VALUES(color),
|
||||
remark = VALUES(remark);
|
||||
|
||||
-- 国内作业仓最小库位基线:每仓一个普通 A1 和一个系统共享溢出位 A0。
|
||||
-- 义乌仓初始化 A-F 六个库区,每区 A1-A15 共 90 个普通库位。
|
||||
-- A0 仍是系统共享溢出位;东莞仓继续保留最小 A1 + A0 基线。
|
||||
-- 不初始化预占、占用或任何业务单据。
|
||||
INSERT INTO wh_location (
|
||||
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
|
||||
location_type, max_weight_kg, shared_overflow, status, remark, created_by, updated_by
|
||||
)
|
||||
WITH RECURSIVE
|
||||
zones AS (
|
||||
SELECT 0 AS zone_index
|
||||
UNION ALL
|
||||
SELECT zone_index + 1 FROM zones WHERE zone_index < 5
|
||||
),
|
||||
slots AS (
|
||||
SELECT 1 AS slot_number
|
||||
UNION ALL
|
||||
SELECT slot_number + 1 FROM slots WHERE slot_number < 15
|
||||
)
|
||||
SELECT
|
||||
CAST(193000000000200000 + CASE
|
||||
WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201
|
||||
ELSE 203 + zones.zone_index * 15 + slots.slot_number
|
||||
END AS UNSIGNED),
|
||||
0,
|
||||
193000000000200001,
|
||||
CHAR(65 + zones.zone_index),
|
||||
CONCAT(CHAR(65 + zones.zone_index), '区'),
|
||||
CONCAT(CHAR(65 + zones.zone_index), slots.slot_number),
|
||||
CONCAT(CHAR(65 + zones.zone_index), slots.slot_number, ' 普通库位'),
|
||||
'PALLET',1000.000,0,'AVAILABLE',NULL,0,0
|
||||
FROM zones
|
||||
CROSS JOIN slots
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id = 0,
|
||||
warehouse_id = VALUES(warehouse_id),
|
||||
zone_code = VALUES(zone_code),
|
||||
zone_name = VALUES(zone_name),
|
||||
location_name = VALUES(location_name),
|
||||
location_type = VALUES(location_type),
|
||||
max_weight_kg = VALUES(max_weight_kg),
|
||||
shared_overflow = VALUES(shared_overflow),
|
||||
status = 'AVAILABLE',
|
||||
remark = VALUES(remark),
|
||||
deleted = 0;
|
||||
|
||||
INSERT INTO wh_location (
|
||||
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
|
||||
location_type, max_weight_kg, shared_overflow, status, remark, created_by, updated_by
|
||||
)
|
||||
VALUES
|
||||
(193000000000200201,0,193000000000200001,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0),
|
||||
(193000000000200202,0,193000000000200001,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0),
|
||||
(193000000000200203,0,193000000000200002,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0),
|
||||
(193000000000200204,0,193000000000200002,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0)
|
||||
@@ -6207,6 +6249,35 @@ ON DUPLICATE KEY UPDATE
|
||||
remark = VALUES(remark),
|
||||
deleted = 0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE assert_yiwu_location_seed()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*)
|
||||
FROM wh_location
|
||||
WHERE tenant_id = 0
|
||||
AND warehouse_id = 193000000000200001
|
||||
AND deleted = 0
|
||||
AND shared_overflow = 0
|
||||
AND zone_code IN ('A','B','C','D','E','F')
|
||||
AND location_code REGEXP '^[A-F]([1-9]|1[0-5])$'
|
||||
AND LEFT(location_code, 1) = zone_code) <> 90 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '义乌仓 A-F 每区 15 个普通库位初始化不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*)
|
||||
FROM wh_location
|
||||
WHERE tenant_id = 0
|
||||
AND warehouse_id = 193000000000200001
|
||||
AND location_code = 'A0'
|
||||
AND shared_overflow = 1
|
||||
AND deleted = 0) <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '义乌仓系统共享溢出位 A0 初始化不完整';
|
||||
END IF;
|
||||
END//
|
||||
DELIMITER ;
|
||||
CALL assert_yiwu_location_seed();
|
||||
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_wh_fba_public_master_data;
|
||||
CREATE TEMPORARY TABLE tmp_wh_fba_public_master_data (
|
||||
warehouse_id_seed BIGINT UNSIGNED NOT NULL,
|
||||
|
||||
+5
-4
@@ -59,18 +59,19 @@ public interface WarehousePalletLocationClient {
|
||||
|
||||
record ReserveRequest(long tenantId, long warehouseId, String sourceService, long businessId,
|
||||
String inboundCode, String idempotencyKey, long operatorId,
|
||||
BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, String palletType,
|
||||
BigDecimal totalVolumeCbm, List<PalletReference> pallets,
|
||||
LocalDateTime expiresAt, boolean measurementCompleted) {}
|
||||
record PalletReference(long weightResultId, long palletId) {}
|
||||
record ConfirmRequest(long tenantId, long warehouseId, String sourceService, long businessId,
|
||||
long operatorId, BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm,
|
||||
int packageCount) {}
|
||||
long operatorId, BigDecimal totalVolumeCbm, int packageCount) {}
|
||||
record ReleaseRequest(long tenantId, String sourceService, long businessId, long operatorId,
|
||||
String reason) {}
|
||||
record SelectiveReleaseRequest(long tenantId, String sourceService, long businessId,
|
||||
String idempotencyKey, long operatorId, String reason,
|
||||
List<Long> locationIds) {}
|
||||
record AllocationResult(long reservationId, String reservationNo, int allocationNo,
|
||||
long palletId, String palletCode, long locationId, String locationCode,
|
||||
long palletId, String palletCode, BigDecimal palletCapacityCbm,
|
||||
long locationId, String locationCode,
|
||||
BigDecimal allocatedVolumeCbm, String state) {}
|
||||
record BatchResult(int palletCount, List<AllocationResult> allocations) {}
|
||||
record ActiveResource(long reservationId, long warehouseId, long palletId, String palletCode,
|
||||
|
||||
+2
-5
@@ -22,7 +22,7 @@ public class InboundPalletAllocationRepository {
|
||||
|
||||
@Transactional
|
||||
public void replace(long tenantId, long inboundId, long warehouseId, long warehouseResourceTenantId,
|
||||
BigDecimal capacity, int totalPackageCount, BatchResult result) {
|
||||
int totalPackageCount, BatchResult result) {
|
||||
Map<Long, String> reservationNos = mapper.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId))
|
||||
@@ -33,9 +33,6 @@ public class InboundPalletAllocationRepository {
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int allocationCount = result.allocations().size();
|
||||
if (allocationCount > totalPackageCount) {
|
||||
throw new IllegalArgumentException("托盘数量不能超过入库单总箱数");
|
||||
}
|
||||
int basePackageCount = allocationCount == 0 ? 0 : totalPackageCount / allocationCount;
|
||||
int remainder = allocationCount == 0 ? 0 : totalPackageCount % allocationCount;
|
||||
for (int index = 0; index < result.allocations().size(); index++) {
|
||||
@@ -48,7 +45,7 @@ public class InboundPalletAllocationRepository {
|
||||
value.setWarehouseResourceTenantId(warehouseResourceTenantId);
|
||||
value.setPalletId(source.palletId());
|
||||
value.setPalletCode(source.palletCode());
|
||||
value.setPalletCapacityCbm(capacity);
|
||||
value.setPalletCapacityCbm(source.palletCapacityCbm());
|
||||
value.setAllocatedVolumeCbm(source.allocatedVolumeCbm());
|
||||
int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
|
||||
value.setAllocatedPackageCount(allocatedPackages);
|
||||
|
||||
@@ -117,6 +117,9 @@ public class InboundMeasurementService {
|
||||
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||
BigDecimal confirmedWeight = confirmedWeightForSubmission(weights);
|
||||
int confirmedPalletCount = Math.toIntExact(weights.stream()
|
||||
.filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))
|
||||
.count());
|
||||
|
||||
if (!alreadyAdvanced) {
|
||||
requireConfirmedPackageTotal(inbound, items);
|
||||
@@ -137,6 +140,7 @@ public class InboundMeasurementService {
|
||||
if (confirmedWeight != null) {
|
||||
inbound.setActualGrossWeightKg(confirmedWeight);
|
||||
}
|
||||
inbound.setPalletCount(confirmedPalletCount);
|
||||
if (measurement.getTotalVolumeCbm() != null) {
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
}
|
||||
|
||||
+27
-7
@@ -8,6 +8,8 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
||||
import com.baiyue.firstleg.config.InboundProperties;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
import feign.FeignException;
|
||||
@@ -29,6 +31,8 @@ public class MeasurementLocationOccupationService {
|
||||
@Resource
|
||||
private InboundWorkflowRepository workflow;
|
||||
@Resource
|
||||
private InboundMeasurementRepository measurements;
|
||||
@Resource
|
||||
private InboundProperties properties;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
@@ -39,6 +43,7 @@ public class MeasurementLocationOccupationService {
|
||||
long warehouseId = requireWarehouse(order);
|
||||
long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID;
|
||||
BigDecimal volume = requireVolume(order);
|
||||
var palletReferences = confirmedPalletReferences(tenantId, inboundId);
|
||||
// 补偿释放后的预占记录需要保留审计,失败重试必须使用新的物理预占键;
|
||||
// 若仍存在活动预占,仓储侧会按业务单据复用,不会创建新的资源占用。
|
||||
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion
|
||||
@@ -46,19 +51,16 @@ public class MeasurementLocationOccupationService {
|
||||
WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
|
||||
internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId,
|
||||
SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume,
|
||||
properties.getDefaultPalletCapacityCbm(), "STANDARD",
|
||||
palletReferences,
|
||||
LocalDateTime.now().plus(properties.getReservationTtl()), true))),
|
||||
"测量完成后库位预占失败");
|
||||
int packageCount = requirePackageCount(order);
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId,
|
||||
properties.getDefaultPalletCapacityCbm(), packageCount, reserved);
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, reserved);
|
||||
try {
|
||||
WarehousePalletLocationClient.BatchResult occupied = requireSuccess(call(() -> warehouse.confirm(
|
||||
internalToken, new WarehousePalletLocationClient.ConfirmRequest(resourceTenantId, warehouseId,
|
||||
SOURCE_SERVICE, inboundId, operatorId, volume,
|
||||
properties.getDefaultPalletCapacityCbm(), packageCount))), "测量完成后库位占用失败");
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId,
|
||||
properties.getDefaultPalletCapacityCbm(), packageCount, occupied);
|
||||
SOURCE_SERVICE, inboundId, operatorId, volume, packageCount))), "测量完成后库位占用失败");
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, occupied);
|
||||
} catch (RuntimeException failure) {
|
||||
try {
|
||||
warehouse.release(internalToken, new WarehousePalletLocationClient.ReleaseRequest(
|
||||
@@ -97,6 +99,24 @@ public class MeasurementLocationOccupationService {
|
||||
return order.getTotalPackageCount();
|
||||
}
|
||||
|
||||
private java.util.List<WarehousePalletLocationClient.PalletReference> confirmedPalletReferences(
|
||||
long tenantId, long inboundId) {
|
||||
java.util.List<WarehousePalletLocationClient.PalletReference> references = measurements
|
||||
.listWeights(tenantId, inboundId).stream()
|
||||
.filter(weight -> "CONFIRMED".equals(weight.getRecognitionStatus()))
|
||||
.filter(weight -> weight.getConfirmedAt() != null && weight.getNetWeightKg() != null)
|
||||
.filter(weight -> weight.getPalletId() != null && weight.getPalletId() > 0)
|
||||
.map(weight -> new WarehousePalletLocationClient.PalletReference(weight.getId(), weight.getPalletId()))
|
||||
.toList();
|
||||
if (references.isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "至少需要一张已确认的称重图片才能提交测量");
|
||||
}
|
||||
if (references.size() > 200) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "单票最多记录200张已确认称重图片");
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
private static <T> ApiResponse<T> call(RemoteCall<T> call) {
|
||||
try { return call.invoke(); }
|
||||
catch (FeignException exception) {
|
||||
|
||||
+3
-3
@@ -58,12 +58,12 @@ class InboundPalletAllocationRepositoryTest {
|
||||
when(mapper.selectList(any())).thenReturn(List.of(first, second));
|
||||
|
||||
var result = new WarehousePalletLocationClient.BatchResult(2, List.of(
|
||||
new WarehousePalletLocationClient.AllocationResult(91L, null, 1, 501L, "P1", 11L, "A1",
|
||||
new WarehousePalletLocationClient.AllocationResult(91L, null, 1, 501L, "P1", new BigDecimal("1.2"), 11L, "A1",
|
||||
new BigDecimal("1.2"), "OCCUPIED"),
|
||||
new WarehousePalletLocationClient.AllocationResult(92L, null, 2, 501L, "P1", 12L, "A2",
|
||||
new WarehousePalletLocationClient.AllocationResult(92L, null, 2, 501L, "P1", new BigDecimal("1.2"), 12L, "A2",
|
||||
new BigDecimal("1.2"), "OCCUPIED")));
|
||||
|
||||
repository.replace(10001L, 31L, 88L, 0L, new BigDecimal("1.2"), 2, result);
|
||||
repository.replace(10001L, 31L, 88L, 0L, 2, result);
|
||||
|
||||
ArgumentCaptor<InboundPalletAllocationEntity> inserted = ArgumentCaptor.forClass(InboundPalletAllocationEntity.class);
|
||||
verify(mapper, org.mockito.Mockito.times(2)).insert(inserted.capture());
|
||||
|
||||
+13
-6
@@ -115,21 +115,28 @@ class CartonMeasurementServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void measuringInboundAllowsManualEntryBeforeMeasurementSubmission() {
|
||||
void measuringInboundWithoutMeasurementAllowsManualEntryBeforeSubmission() {
|
||||
InboundOrderEntity measuring = inbound("MEASURING");
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
|
||||
when(repository.findInbound(10001L, 70001L)).thenReturn(measuring);
|
||||
when(repository.lockInbound(10001L, 70001L)).thenReturn(measuring);
|
||||
when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(null);
|
||||
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(measurement);
|
||||
when(repository.listItems(10001L, 30001L)).thenAnswer(invocation -> List.of(insertedItem.get()));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
when(repository.findLatestEditableMeasurement(10001L, 70001L)).thenReturn(null);
|
||||
when(repository.listItems(org.mockito.ArgumentMatchers.eq(10001L),
|
||||
org.mockito.ArgumentMatchers.anyLong())).thenAnswer(invocation -> List.of(insertedItem.get()));
|
||||
when(repository.updateMeasurement(any())).thenReturn(true);
|
||||
|
||||
CartonMeasurementResult result = service.upsertManualForInbound(
|
||||
10001L, 90001L, 70001L, acceptanceCommand());
|
||||
|
||||
ArgumentCaptor<InboundMeasurementEntity> measurement =
|
||||
ArgumentCaptor.forClass(InboundMeasurementEntity.class);
|
||||
verify(repository).insertMeasurement(measurement.capture());
|
||||
assertThat(result.created()).isTrue();
|
||||
assertThat(measurement.getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL);
|
||||
assertThat(result.measurementId()).isEqualTo(measurement.getValue().getId());
|
||||
assertThat(measurement.getValue().getSourceType()).isEqualTo(MeasurementBatchService.SOURCE_MANUAL);
|
||||
assertThat(measurement.getValue().getSourceCode()).isEqualTo("PLATFORM");
|
||||
assertThat(measurement.getValue().getStatus()).isEqualTo(MeasurementBatchService.STATUS_RECEIVED);
|
||||
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(measurement.getValue().getId());
|
||||
verify(images).upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any());
|
||||
}
|
||||
|
||||
|
||||
@@ -598,6 +598,7 @@ class InboundMeasurementServiceTest {
|
||||
assertThat(inbound.getMeasureReviewStatus()).isEqualTo("PENDING");
|
||||
assertThat(measurement.getStatus()).isEqualTo("SUBMITTED");
|
||||
assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("20.000");
|
||||
assertThat(inbound.getPalletCount()).isEqualTo(1);
|
||||
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("1.2345");
|
||||
}
|
||||
|
||||
|
||||
+27
-7
@@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
||||
import com.baiyue.firstleg.config.InboundProperties;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -39,6 +41,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
@MockBean private WarehousePalletLocationClient warehouse;
|
||||
@MockBean private InboundPalletAllocationRepository allocations;
|
||||
@MockBean private InboundWorkflowRepository workflow;
|
||||
@MockBean private InboundMeasurementRepository measurements;
|
||||
@Resource private InboundProperties properties;
|
||||
@Resource private MeasurementLocationOccupationService service;
|
||||
|
||||
@@ -46,6 +49,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
void platformWarehouseResourcesUseZeroWhileSnapshotsRemainInBusinessTenant() {
|
||||
properties.setWarehouseResourceTenantId(90001L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
@@ -59,6 +63,9 @@ class MeasurementLocationOccupationServiceTest {
|
||||
assertThat(reserve.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
assertThat(reserve.getValue().businessId()).isEqualTo(INBOUND);
|
||||
assertThat(reserve.getValue().idempotencyKey()).startsWith("measure-88-30-3-");
|
||||
assertThat(reserve.getValue().pallets())
|
||||
.extracting(WarehousePalletLocationClient.PalletReference::palletId)
|
||||
.containsExactly(601L, 601L, 601L);
|
||||
|
||||
ArgumentCaptor<WarehousePalletLocationClient.ConfirmRequest> confirm =
|
||||
ArgumentCaptor.forClass(WarehousePalletLocationClient.ConfirmRequest.class);
|
||||
@@ -72,7 +79,6 @@ class MeasurementLocationOccupationServiceTest {
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND),
|
||||
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
|
||||
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
|
||||
org.mockito.ArgumentMatchers.eq(20),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
@@ -81,6 +87,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
void defaultResourceTenantRemainsPlatformZero() {
|
||||
properties.setWarehouseResourceTenantId(0L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
when(warehouse.confirm(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
@@ -104,7 +111,6 @@ class MeasurementLocationOccupationServiceTest {
|
||||
org.mockito.ArgumentMatchers.eq(INBOUND),
|
||||
org.mockito.ArgumentMatchers.eq(WAREHOUSE),
|
||||
org.mockito.ArgumentMatchers.eq(PLATFORM_RESOURCE_TENANT),
|
||||
org.mockito.ArgumentMatchers.eq(new BigDecimal("1.200000")),
|
||||
org.mockito.ArgumentMatchers.eq(20),
|
||||
org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
@@ -113,6 +119,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
void confirmFailureReleasesReservationInResourceTenantAndKeepsOriginalFailure() {
|
||||
properties.setWarehouseResourceTenantId(90001L);
|
||||
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound());
|
||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(confirmedWeights());
|
||||
when(warehouse.reserve(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(batch("RESERVED")));
|
||||
IllegalStateException failure = new IllegalStateException("confirm failed");
|
||||
@@ -130,7 +137,7 @@ class MeasurementLocationOccupationServiceTest {
|
||||
assertThat(release.getValue().tenantId()).isEqualTo(PLATFORM_RESOURCE_TENANT);
|
||||
assertThat(release.getValue().businessId()).isEqualTo(INBOUND);
|
||||
verify(allocations).replace(BUSINESS_TENANT, INBOUND, WAREHOUSE, PLATFORM_RESOURCE_TENANT,
|
||||
new BigDecimal("1.200000"), 20, batch("RESERVED"));
|
||||
20, batch("RESERVED"));
|
||||
}
|
||||
|
||||
private static InboundProperties properties() {
|
||||
@@ -159,9 +166,22 @@ class MeasurementLocationOccupationServiceTest {
|
||||
}
|
||||
|
||||
private static WarehousePalletLocationClient.BatchResult batch(String state) {
|
||||
return new WarehousePalletLocationClient.BatchResult(1, List.of(
|
||||
new WarehousePalletLocationClient.AllocationResult(
|
||||
501L, "RES-001", 1, 601L, "PL-001", 701L, "A-001",
|
||||
new BigDecimal("1.0000"), state)));
|
||||
return new WarehousePalletLocationClient.BatchResult(3, java.util.stream.IntStream.rangeClosed(1, 3)
|
||||
.mapToObj(index -> new WarehousePalletLocationClient.AllocationResult(
|
||||
500L + index, "RES-00" + index, index, 601L, "PL-001", new BigDecimal("1.200000"),
|
||||
700L + index, "A-00" + index, new BigDecimal("0.333333"), state))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private static List<InboundWeightResultEntity> confirmedWeights() {
|
||||
return java.util.stream.LongStream.rangeClosed(1, 3).mapToObj(index -> {
|
||||
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
||||
weight.setId(800L + index);
|
||||
weight.setPalletId(601L);
|
||||
weight.setRecognitionStatus("CONFIRMED");
|
||||
weight.setConfirmedAt(java.time.LocalDateTime.now());
|
||||
weight.setNetWeightKg(new BigDecimal("10"));
|
||||
return weight;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ public final class LocationDtos {
|
||||
public record OccupancyTarget(
|
||||
@Min(1) long reservationId,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal allocatedVolumeCbm,
|
||||
@Min(1) Integer allocatedPackageCount
|
||||
@Min(0) Integer allocatedPackageCount
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,19 @@ public final class PalletAllocationDtos {
|
||||
@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Min(1) long operatorId,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal totalVolumeCbm,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal palletCapacityCbm,
|
||||
@Size(max = 32) String palletType,
|
||||
@NotNull @Size(min = 1, max = 200) List<@NotNull @jakarta.validation.Valid PalletReference> pallets,
|
||||
@NotNull LocalDateTime expiresAt,
|
||||
@NotNull Boolean measurementCompleted
|
||||
) {
|
||||
}
|
||||
|
||||
/** 一张已确认称重图片对应一托;同一托盘主数据允许重复引用。 */
|
||||
public record PalletReference(
|
||||
@Min(1) long weightResultId,
|
||||
@Min(1) long palletId
|
||||
) {
|
||||
}
|
||||
|
||||
public record ConfirmRequest(
|
||||
@Min(0) long tenantId,
|
||||
@Min(1) long warehouseId,
|
||||
@@ -39,7 +45,6 @@ public final class PalletAllocationDtos {
|
||||
@Min(1) long businessId,
|
||||
@Min(1) long operatorId,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal totalVolumeCbm,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6) BigDecimal palletCapacityCbm,
|
||||
@Min(1) int packageCount
|
||||
) {
|
||||
}
|
||||
@@ -193,6 +198,7 @@ public final class PalletAllocationDtos {
|
||||
int allocationNo,
|
||||
long palletId,
|
||||
String palletCode,
|
||||
BigDecimal palletCapacityCbm,
|
||||
long locationId,
|
||||
String locationCode,
|
||||
BigDecimal allocatedVolumeCbm,
|
||||
|
||||
@@ -62,16 +62,6 @@ public class PalletRepository {
|
||||
.ne(excludedId != null, PalletEntity::getId, excludedId)) > 0;
|
||||
}
|
||||
|
||||
public List<PalletEntity> findReusable(long tenantId, long warehouseId, String palletType,
|
||||
java.math.BigDecimal capacityCbm) {
|
||||
return mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
|
||||
.eq(PalletEntity::getTenantId, tenantId)
|
||||
.eq(PalletEntity::getWarehouseId, warehouseId)
|
||||
.eq(StringUtils.hasText(palletType), PalletEntity::getPalletType, palletType)
|
||||
.eq(capacityCbm != null, PalletEntity::getCapacityCbm, capacityCbm)
|
||||
.orderByAsc(PalletEntity::getPalletCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成平台作用域内下一个编码。编码序列跨仓库共用,且包含逻辑删除记录,确保编码永不复用。
|
||||
*/
|
||||
|
||||
+49
-41
@@ -32,8 +32,6 @@ import java.security.NoSuchAlgorithmException;
|
||||
*/
|
||||
@Service
|
||||
public class PalletLocationAllocationService {
|
||||
/** 测量完成后默认推荐的普通托盘类型编码。 */
|
||||
public static final String DEFAULT_PALLET_TYPE = "STANDARD";
|
||||
/** 仓库、托盘和库位属于平台公共资源域。 */
|
||||
private static final long PLATFORM_RESOURCE_TENANT_ID = 0L;
|
||||
/** 仅头程服务可结算头程入库单占用的仓储资源。 */
|
||||
@@ -41,8 +39,6 @@ public class PalletLocationAllocationService {
|
||||
@Resource
|
||||
private PalletRepository palletRepository;
|
||||
@Resource
|
||||
private PalletVolumeSplitter splitter;
|
||||
@Resource
|
||||
private LocationResourceService locationResourceService;
|
||||
@Resource
|
||||
private PalletLocationTransferOperationRepository transferOperations;
|
||||
@@ -59,74 +55,78 @@ public class PalletLocationAllocationService {
|
||||
if (!Boolean.TRUE.equals(request.measurementCompleted())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "只能使用已完成测量的总体积拆分托盘并占用库位");
|
||||
}
|
||||
var split = splitter.split(request.totalVolumeCbm(), request.palletCapacityCbm());
|
||||
List<LocationDtos.ActiveResource> existing = active(request.tenantId(), request.sourceService(), request.businessId());
|
||||
if (!existing.isEmpty()) {
|
||||
return fromActive(existing, split);
|
||||
return fromActive(request.tenantId(), request.warehouseId(), existing, request.totalVolumeCbm());
|
||||
}
|
||||
|
||||
String palletType = org.springframework.util.StringUtils.hasText(request.palletType())
|
||||
? request.palletType() : DEFAULT_PALLET_TYPE;
|
||||
List<PalletEntity> pallets = palletRepository.findReusable(request.tenantId(), request.warehouseId(),
|
||||
palletType, request.palletCapacityCbm());
|
||||
if (pallets.isEmpty()) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "未配置匹配类型和容量的托盘主数据");
|
||||
List<PalletEntity> pallets = new ArrayList<>(request.pallets().size());
|
||||
for (PalletAllocationDtos.PalletReference reference : request.pallets()) {
|
||||
PalletEntity pallet = palletRepository.findOwned(request.tenantId(), request.warehouseId(), reference.palletId());
|
||||
if (pallet == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "称重图片关联的托盘主数据不存在或不属于当前仓库");
|
||||
}
|
||||
pallets.add(pallet);
|
||||
}
|
||||
|
||||
List<LocationDtos.ReservationTarget> targets = new ArrayList<>(split.palletCount());
|
||||
for (int index = 0; index < split.palletCount(); index++) {
|
||||
PalletEntity pallet = pallets.get(index % pallets.size());
|
||||
List<LocationDtos.ReservationTarget> targets = new ArrayList<>(pallets.size());
|
||||
for (int index = 0; index < pallets.size(); index++) {
|
||||
PalletEntity pallet = pallets.get(index);
|
||||
targets.add(new LocationDtos.ReservationTarget(pallet.getId(), pallet.getPalletCode(),
|
||||
request.idempotencyKey() + ":" + (index + 1)));
|
||||
request.idempotencyKey() + ":" + request.pallets().get(index).weightResultId()));
|
||||
}
|
||||
|
||||
List<LocationDtos.ReservationResult> reservations = locationResourceService.reserveAvailable(
|
||||
new LocationDtos.ReserveCommand(request.tenantId(), request.warehouseId(), request.sourceService(),
|
||||
request.businessId(), request.inboundCode(), request.operatorId(), request.expiresAt(), targets));
|
||||
if (reservations.size() != split.palletCount()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位预占结果数量不一致");
|
||||
if (reservations.size() != pallets.size()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位预占结果数量与已确认称重图片数量不一致");
|
||||
}
|
||||
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(reservations.size());
|
||||
for (int index = 0; index < reservations.size(); index++) {
|
||||
var item = reservations.get(index);
|
||||
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), item.reservationNo(), index + 1,
|
||||
item.palletId(), item.palletCode(), item.locationId(), item.locationCode(),
|
||||
split.allocations().get(index).allocatedVolumeCbm(), "RESERVED"));
|
||||
item.palletId(), item.palletCode(), pallets.get(index).getCapacityCbm(),
|
||||
item.locationId(), item.locationCode(), allocatedVolume(request.totalVolumeCbm(), reservations.size(), index),
|
||||
"RESERVED"));
|
||||
}
|
||||
return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results));
|
||||
return new PalletAllocationDtos.BatchResult(results.size(), List.copyOf(results));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PALLET_LOCATION_CONFIRM", resourceType = "INBOUND", resourceId = "#request.businessId", description = "确认库位占用")
|
||||
public PalletAllocationDtos.BatchResult confirm(PalletAllocationDtos.ConfirmRequest request) {
|
||||
var split = splitter.split(request.totalVolumeCbm(), request.palletCapacityCbm());
|
||||
List<LocationDtos.ActiveResource> active = active(request.tenantId(), request.sourceService(), request.businessId());
|
||||
if (active.size() != split.palletCount()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "活动预占数量与托盘拆分数量不一致");
|
||||
}
|
||||
if (active.size() > request.packageCount()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘数量超过入库单总箱数,无法为每个托盘分配至少一箱");
|
||||
if (active.isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单没有可确认的活动库位预占");
|
||||
}
|
||||
active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId));
|
||||
List<LocationDtos.OccupancyTarget> targets = new ArrayList<>(active.size());
|
||||
int basePackageCount = request.packageCount() / active.size();
|
||||
int remainder = request.packageCount() % active.size();
|
||||
int packageRemainder = request.packageCount() % active.size();
|
||||
for (int index = 0; index < active.size(); index++) {
|
||||
int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
|
||||
targets.add(new LocationDtos.OccupancyTarget(active.get(index).reservationId(),
|
||||
split.allocations().get(index).allocatedVolumeCbm(), allocatedPackages));
|
||||
allocatedVolume(request.totalVolumeCbm(), active.size(), index),
|
||||
basePackageCount + (index < packageRemainder ? 1 : 0)));
|
||||
}
|
||||
List<LocationDtos.OccupancyResult> occupied = locationResourceService.confirmOccupied(
|
||||
new LocationDtos.ConfirmCommand(request.tenantId(), request.warehouseId(), request.sourceService(),
|
||||
request.businessId(), request.operatorId(), targets));
|
||||
if (occupied.size() != active.size()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位占用结果数量与已确认称重图片数量不一致");
|
||||
}
|
||||
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(occupied.size());
|
||||
for (int index = 0; index < occupied.size(); index++) {
|
||||
var item = occupied.get(index);
|
||||
PalletEntity pallet = palletRepository.findOwnedIncludingDeleted(request.tenantId(), request.warehouseId(), item.palletId());
|
||||
if (pallet == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "库位占用引用的托盘主数据不存在");
|
||||
}
|
||||
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), active.get(index).reservationNo(), index + 1,
|
||||
item.palletId(), active.get(index).palletCode(), item.locationId(), item.locationCode(),
|
||||
split.allocations().get(index).allocatedVolumeCbm(), "OCCUPIED"));
|
||||
item.palletId(), active.get(index).palletCode(), pallet.getCapacityCbm(), item.locationId(), item.locationCode(),
|
||||
allocatedVolume(request.totalVolumeCbm(), occupied.size(), index), "OCCUPIED"));
|
||||
}
|
||||
return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results));
|
||||
return new PalletAllocationDtos.BatchResult(results.size(), List.copyOf(results));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -528,19 +528,27 @@ public class PalletLocationAllocationService {
|
||||
return locationResourceService.findOccupiedResources(tenantId, sourceService, businessId);
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.BatchResult fromActive(List<LocationDtos.ActiveResource> active,
|
||||
PalletVolumeSplitter.SplitResult split) {
|
||||
if (active.size() != split.palletCount()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "幂等请求与已有托盘拆分结果不一致");
|
||||
}
|
||||
private PalletAllocationDtos.BatchResult fromActive(long tenantId, long warehouseId,
|
||||
List<LocationDtos.ActiveResource> active,
|
||||
java.math.BigDecimal totalVolumeCbm) {
|
||||
active.sort(Comparator.comparingLong(LocationDtos.ActiveResource::reservationId));
|
||||
List<PalletAllocationDtos.AllocationResult> results = new ArrayList<>(active.size());
|
||||
for (int index = 0; index < active.size(); index++) {
|
||||
var item = active.get(index);
|
||||
PalletEntity pallet = palletRepository.findOwnedIncludingDeleted(
|
||||
tenantId, warehouseId, item.palletId());
|
||||
if (pallet == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "已有库位分配引用的托盘主数据不存在");
|
||||
}
|
||||
results.add(new PalletAllocationDtos.AllocationResult(item.reservationId(), item.reservationNo(), index + 1,
|
||||
item.palletId(), item.palletCode(), item.locationId(), item.locationCode(),
|
||||
split.allocations().get(index).allocatedVolumeCbm(), item.state()));
|
||||
item.palletId(), item.palletCode(), pallet.getCapacityCbm(), item.locationId(), item.locationCode(),
|
||||
allocatedVolume(totalVolumeCbm, active.size(), index), item.state()));
|
||||
}
|
||||
return new PalletAllocationDtos.BatchResult(split.palletCount(), List.copyOf(results));
|
||||
return new PalletAllocationDtos.BatchResult(results.size(), List.copyOf(results));
|
||||
}
|
||||
|
||||
private static java.math.BigDecimal allocatedVolume(java.math.BigDecimal total, int count, int index) {
|
||||
java.math.BigDecimal base = total.divide(java.math.BigDecimal.valueOf(count), 6, java.math.RoundingMode.DOWN);
|
||||
return index == count - 1 ? total.subtract(base.multiply(java.math.BigDecimal.valueOf(count - 1L))) : base;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 按托盘容积向上拆分入库总体积,不涉及SKU库存。 */
|
||||
@Component
|
||||
public class PalletVolumeSplitter {
|
||||
static final int MAX_PALLET_COUNT = 200;
|
||||
|
||||
public SplitResult split(BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm) {
|
||||
requirePositive(totalVolumeCbm, "总体积必须大于0");
|
||||
requirePositive(palletCapacityCbm, "托盘容积必须大于0");
|
||||
final int palletCount;
|
||||
try {
|
||||
palletCount = totalVolumeCbm.divide(palletCapacityCbm, 0, RoundingMode.CEILING).intValueExact();
|
||||
} catch (ArithmeticException exception) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "托盘拆分数量超出系统处理范围");
|
||||
}
|
||||
if (palletCount > MAX_PALLET_COUNT) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "单次最多拆分200个托盘");
|
||||
}
|
||||
|
||||
List<Allocation> allocations = new ArrayList<>(palletCount);
|
||||
for (int index = 1; index <= palletCount; index++) {
|
||||
BigDecimal allocated = index == palletCount
|
||||
? totalVolumeCbm.subtract(palletCapacityCbm.multiply(BigDecimal.valueOf(palletCount - 1L)))
|
||||
: palletCapacityCbm;
|
||||
allocations.add(new Allocation(index, allocated));
|
||||
}
|
||||
return new SplitResult(totalVolumeCbm, palletCapacityCbm, palletCount, List.copyOf(allocations));
|
||||
}
|
||||
|
||||
private static void requirePositive(BigDecimal value, String message) {
|
||||
if (value == null || value.signum() <= 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, message);
|
||||
}
|
||||
}
|
||||
|
||||
public record Allocation(int allocationNo, BigDecimal allocatedVolumeCbm) {
|
||||
}
|
||||
|
||||
public record SplitResult(BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm,
|
||||
int palletCount, List<Allocation> allocations) {
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,56 @@ ON DUPLICATE KEY UPDATE
|
||||
color=VALUES(color),
|
||||
remark=VALUES(remark);
|
||||
|
||||
-- 每个国内作业仓初始化一个普通库位和一个系统共享溢出位 A0。
|
||||
-- 义乌仓初始化 A-F 六个库区,每区 A1-A15 共 90 个普通库位。
|
||||
-- A0 仍是系统共享溢出位;东莞仓继续保留最小 A1 + A0 基线。
|
||||
-- 这里只创建公共资源,不初始化预占、占用或业务单据。
|
||||
INSERT INTO wh_location(
|
||||
id,tenant_id,warehouse_id,zone_code,zone_name,location_code,location_name,
|
||||
location_type,max_weight_kg,shared_overflow,status,remark,created_by,updated_by
|
||||
)
|
||||
WITH RECURSIVE
|
||||
zones AS (
|
||||
SELECT 0 AS zone_index
|
||||
UNION ALL
|
||||
SELECT zone_index + 1 FROM zones WHERE zone_index < 5
|
||||
),
|
||||
slots AS (
|
||||
SELECT 1 AS slot_number
|
||||
UNION ALL
|
||||
SELECT slot_number + 1 FROM slots WHERE slot_number < 15
|
||||
)
|
||||
SELECT
|
||||
CAST(190000000000000000 + CASE
|
||||
WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201
|
||||
ELSE 203 + zones.zone_index * 15 + slots.slot_number
|
||||
END AS UNSIGNED),
|
||||
0,
|
||||
190000000000000001,
|
||||
CHAR(65 + zones.zone_index),
|
||||
CONCAT(CHAR(65 + zones.zone_index), '区'),
|
||||
CONCAT(CHAR(65 + zones.zone_index), slots.slot_number),
|
||||
CONCAT(CHAR(65 + zones.zone_index), slots.slot_number, ' 普通库位'),
|
||||
'PALLET',1000.000,0,'AVAILABLE',NULL,0,0
|
||||
FROM zones
|
||||
CROSS JOIN slots
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id=0,
|
||||
warehouse_id=VALUES(warehouse_id),
|
||||
zone_code=VALUES(zone_code),
|
||||
zone_name=VALUES(zone_name),
|
||||
location_name=VALUES(location_name),
|
||||
location_type=VALUES(location_type),
|
||||
max_weight_kg=VALUES(max_weight_kg),
|
||||
shared_overflow=VALUES(shared_overflow),
|
||||
status='AVAILABLE',
|
||||
remark=VALUES(remark),
|
||||
deleted=0;
|
||||
|
||||
INSERT INTO wh_location(
|
||||
id,tenant_id,warehouse_id,zone_code,zone_name,location_code,location_name,
|
||||
location_type,max_weight_kg,shared_overflow,status,remark,created_by,updated_by
|
||||
)
|
||||
VALUES
|
||||
(190000000000000201,0,190000000000000001,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0),
|
||||
(190000000000000202,0,190000000000000001,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0),
|
||||
(190000000000000203,0,190000000000000002,'A','A区','A1','A1 普通库位','PALLET',1000.000,0,'AVAILABLE',NULL,0,0),
|
||||
(190000000000000204,0,190000000000000002,'A','A区','A0','A0 共享溢出位','PALLET',NULL,1,'AVAILABLE','系统共享临时溢出位',0,0)
|
||||
@@ -39,3 +81,32 @@ ON DUPLICATE KEY UPDATE
|
||||
status='AVAILABLE',
|
||||
remark=VALUES(remark),
|
||||
deleted=0;
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE assert_yiwu_location_seed()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*)
|
||||
FROM wh_location
|
||||
WHERE tenant_id=0
|
||||
AND warehouse_id=190000000000000001
|
||||
AND deleted=0
|
||||
AND shared_overflow=0
|
||||
AND zone_code IN ('A','B','C','D','E','F')
|
||||
AND location_code REGEXP '^[A-F]([1-9]|1[0-5])$'
|
||||
AND LEFT(location_code,1)=zone_code) <> 90 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='义乌仓 A-F 每区 15 个普通库位初始化不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*)
|
||||
FROM wh_location
|
||||
WHERE tenant_id=0
|
||||
AND warehouse_id=190000000000000001
|
||||
AND location_code='A0'
|
||||
AND shared_overflow=1
|
||||
AND deleted=0) <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='义乌仓系统共享溢出位 A0 初始化不完整';
|
||||
END IF;
|
||||
END//
|
||||
DELIMITER ;
|
||||
CALL assert_yiwu_location_seed();
|
||||
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
|
||||
|
||||
+3
-2
@@ -18,9 +18,10 @@ class PalletAllocationDtosValidationTest {
|
||||
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(30);
|
||||
|
||||
assertThat(validator.validate(new PalletAllocationDtos.ReserveRequest(0L, 20L, "FIRSTLEG", 88L,
|
||||
"RK-001", "measure-88", 9L, BigDecimal.ONE, BigDecimal.ONE, "STANDARD", expiresAt, true))).isEmpty();
|
||||
"RK-001", "measure-88", 9L, BigDecimal.ONE,
|
||||
List.of(new PalletAllocationDtos.PalletReference(7001L, 30L)), expiresAt, true))).isEmpty();
|
||||
assertThat(validator.validate(new PalletAllocationDtos.ConfirmRequest(0L, 20L, "FIRSTLEG", 88L,
|
||||
9L, BigDecimal.ONE, BigDecimal.ONE, 20))).isEmpty();
|
||||
9L, BigDecimal.ONE, 20))).isEmpty();
|
||||
assertThat(validator.validate(new PalletAllocationDtos.ReleaseRequest(0L, "FIRSTLEG", 88L, 9L,
|
||||
"入库补偿"))).isEmpty();
|
||||
assertThat(validator.validate(new PalletAllocationDtos.SelectiveReleaseRequest(0L, "FIRSTLEG", 88L,
|
||||
|
||||
+10
-2
@@ -10,7 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InitialWarehouseResourceSeedSqlContractTest {
|
||||
@Test
|
||||
void initializesReusablePalletMasterDataAndAvailableLocationsForBothDomesticWarehouses() throws Exception {
|
||||
void initializesReusablePalletsAndYiwuLocationGrid() throws Exception {
|
||||
assertModuleSeed(Files.readString(Path.of("src", "main", "resources", "db", "phase2-pallet-seed.sql"),
|
||||
StandardCharsets.UTF_8));
|
||||
assertUnifiedSeed(Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
@@ -30,7 +30,15 @@ class InitialWarehouseResourceSeedSqlContractTest {
|
||||
assertThat(sql)
|
||||
.contains("0," + yiwuId + ",'TP-001'")
|
||||
.contains("0," + dongguanId + ",'TP-002'")
|
||||
.contains("(" + idPrefix + "201,0," + yiwuId + ",'A','A区','A1'")
|
||||
.contains("WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201")
|
||||
.contains("ELSE 203 + zones.zone_index * 15 + slots.slot_number")
|
||||
.contains("SELECT zone_index + 1 FROM zones WHERE zone_index < 5")
|
||||
.contains("SELECT slot_number + 1 FROM slots WHERE slot_number < 15")
|
||||
.contains("CONCAT(CHAR(65 + zones.zone_index), slots.slot_number)")
|
||||
.contains("CROSS JOIN slots")
|
||||
.contains("location_code REGEXP '^[A-F]([1-9]|1[0-5])$'")
|
||||
.contains("<> 90 THEN")
|
||||
.contains("义乌仓 A-F 每区 15 个普通库位初始化不完整")
|
||||
.contains("(" + idPrefix + "202,0," + yiwuId + ",'A','A区','A0'")
|
||||
.contains("(" + idPrefix + "203,0," + dongguanId + ",'A','A区','A1'")
|
||||
.contains("(" + idPrefix + "204,0," + dongguanId + ",'A','A区','A0'")
|
||||
|
||||
+27
-38
@@ -47,50 +47,48 @@ class PalletLocationAllocationServiceTest {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
@Bean PalletVolumeSplitter splitter() { return new PalletVolumeSplitter(); }
|
||||
@Bean ObjectMapper objectMapper() { return new ObjectMapper(); }
|
||||
}
|
||||
|
||||
@Test
|
||||
void reusesOnePalletMasterForMultipleAllocations() {
|
||||
void oneConfirmedWeightImageCreatesOneAllocationWithoutCapacityMatching() {
|
||||
when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of());
|
||||
when(pallets.findReusable(10001, 20, "木托盘", new BigDecimal("2")))
|
||||
.thenReturn(List.of(pallet(30, "TP-001")));
|
||||
when(pallets.findOwned(10001, 20, 30)).thenReturn(pallet(30, "TP-001"));
|
||||
when(locations.reserveAvailable(any())).thenReturn(List.of(
|
||||
new LocationDtos.ReservationResult(101, "LR101", 30, "TP-001", 40, "A-01", false,
|
||||
LocalDateTime.now().plusMinutes(30)),
|
||||
new LocationDtos.ReservationResult(102, "LR102", 30, "TP-001", 41, "A-02", false,
|
||||
LocalDateTime.now().plusMinutes(30))));
|
||||
|
||||
var request = new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", 88, "RK-001", "measure-001",
|
||||
9001, new BigDecimal("4"), new BigDecimal("2"), "木托盘", LocalDateTime.now().plusMinutes(30), true);
|
||||
assertThat(service.reserve(request).allocations()).hasSize(2)
|
||||
.extracting(PalletAllocationDtos.AllocationResult::palletId)
|
||||
.containsOnly(30L);
|
||||
9001, new BigDecimal("4"), List.of(new PalletAllocationDtos.PalletReference(7001, 30)),
|
||||
LocalDateTime.now().plusMinutes(30), true);
|
||||
assertThat(service.reserve(request).allocations()).singleElement().satisfies(item -> {
|
||||
assertThat(item.palletId()).isEqualTo(30L);
|
||||
assertThat(item.allocatedVolumeCbm()).isEqualByComparingTo("4");
|
||||
assertThat(item.palletCapacityCbm()).isEqualByComparingTo("1.2");
|
||||
});
|
||||
|
||||
org.mockito.ArgumentCaptor<LocationDtos.ReserveCommand> command =
|
||||
org.mockito.ArgumentCaptor.forClass(LocationDtos.ReserveCommand.class);
|
||||
verify(locations).reserveAvailable(command.capture());
|
||||
assertThat(command.getValue().targets()).hasSize(2)
|
||||
.extracting(LocationDtos.ReservationTarget::palletId)
|
||||
.containsOnly(30L);
|
||||
assertThat(command.getValue().targets()).singleElement()
|
||||
.extracting(LocationDtos.ReservationTarget::palletId).isEqualTo(30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase02() {
|
||||
when(locations.findActiveResources(10001, "FIRSTLEG", 99)).thenReturn(List.of());
|
||||
when(pallets.findReusable(10001, 20, "STANDARD", new BigDecimal("2")))
|
||||
.thenReturn(List.of(pallet(30, "TP-001")));
|
||||
when(pallets.findOwned(10001, 20, 30)).thenReturn(pallet(30, "TP-001"));
|
||||
when(locations.reserveAvailable(any())).thenReturn(List.of(
|
||||
new LocationDtos.ReservationResult(101, "LR101", 30, "TP-001", 40, "A0", true,
|
||||
LocalDateTime.now().plusMinutes(30))));
|
||||
|
||||
var request = new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", 99, "RK-001", "measure-99",
|
||||
9001, new BigDecimal("2"), new BigDecimal("2"), null, LocalDateTime.now().plusMinutes(30), true);
|
||||
9001, new BigDecimal("2"), List.of(new PalletAllocationDtos.PalletReference(7001, 30)),
|
||||
LocalDateTime.now().plusMinutes(30), true);
|
||||
var result = service.reserve(request);
|
||||
|
||||
assertThat(result.allocations().get(0).locationCode()).isEqualTo("A0");
|
||||
verify(pallets).findReusable(10001, 20, "STANDARD", new BigDecimal("2"));
|
||||
verify(pallets).findOwned(10001, 20, 30);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -129,7 +127,7 @@ class PalletLocationAllocationServiceTest {
|
||||
|
||||
assertThat(result.palletCount()).isEqualTo(1);
|
||||
assertThat(result.allocations().get(0).state()).isEqualTo("RESERVED");
|
||||
verify(pallets, never()).findReusable(any(Long.class), any(Long.class), any(), any());
|
||||
verify(pallets, never()).findOwned(anyLong(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,9 +137,10 @@ class PalletLocationAllocationServiceTest {
|
||||
when(locations.findActiveResources(10001L, "FIRSTLEG", 88L)).thenReturn(List.of(resource));
|
||||
when(locations.confirmOccupied(any())).thenReturn(List.of(new LocationDtos.OccupancyResult(
|
||||
301L, "OC-301", 101L, 30L, 40L, "A-01", false)));
|
||||
when(pallets.findOwnedIncludingDeleted(10001L, 20L, 30L)).thenReturn(pallet(30L, "TP-001"));
|
||||
|
||||
service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L, "FIRSTLEG", 88L,
|
||||
9001L, new BigDecimal("1.0"), new BigDecimal("2.0"), 20));
|
||||
9001L, new BigDecimal("1.0"), 20));
|
||||
|
||||
org.mockito.ArgumentCaptor<LocationDtos.ConfirmCommand> command =
|
||||
org.mockito.ArgumentCaptor.forClass(LocationDtos.ConfirmCommand.class);
|
||||
@@ -152,10 +151,10 @@ class PalletLocationAllocationServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmDistributesRemainderWithoutZeroPackageAllocations() {
|
||||
void confirmedImageCountDefinesPalletCountEvenWhenItExceedsPackageCount() {
|
||||
List<LocationDtos.ActiveResource> resources = new ArrayList<>();
|
||||
List<LocationDtos.OccupancyResult> occupancyResults = new ArrayList<>();
|
||||
for (int index = 1; index <= 10; index++) {
|
||||
for (int index = 1; index <= 3; index++) {
|
||||
long reservationId = 100L + index;
|
||||
long palletId = 200L + index;
|
||||
long locationId = 300L + index;
|
||||
@@ -163,33 +162,21 @@ class PalletLocationAllocationServiceTest {
|
||||
"TP-" + index, locationId, "A-" + index, false, "RESERVED"));
|
||||
occupancyResults.add(new LocationDtos.OccupancyResult(400L + index, "OC-" + index,
|
||||
reservationId, palletId, locationId, "A-" + index, false));
|
||||
PalletEntity pallet = pallet(palletId, "TP-" + index);
|
||||
when(pallets.findOwnedIncludingDeleted(10001L, 20L, palletId)).thenReturn(pallet);
|
||||
}
|
||||
when(locations.findActiveResources(10001L, "FIRSTLEG", 89L)).thenReturn(resources);
|
||||
when(locations.confirmOccupied(any())).thenReturn(occupancyResults);
|
||||
|
||||
service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L, "FIRSTLEG", 89L,
|
||||
9001L, new BigDecimal("12.0"), new BigDecimal("1.2"), 12));
|
||||
9001L, new BigDecimal("12.0"), 2));
|
||||
|
||||
org.mockito.ArgumentCaptor<LocationDtos.ConfirmCommand> command =
|
||||
org.mockito.ArgumentCaptor.forClass(LocationDtos.ConfirmCommand.class);
|
||||
verify(locations).confirmOccupied(command.capture());
|
||||
assertThat(command.getValue().targets())
|
||||
.extracting(LocationDtos.OccupancyTarget::allocatedPackageCount)
|
||||
.containsExactly(2, 2, 1, 1, 1, 1, 1, 1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmRejectsMorePalletsThanPackages() {
|
||||
List<LocationDtos.ActiveResource> resources = List.of(
|
||||
new LocationDtos.ActiveResource(101L, null, 20L, 30L, "TP-001", 40L, "A-01", false, "RESERVED"),
|
||||
new LocationDtos.ActiveResource(102L, null, 20L, 31L, "TP-002", 41L, "A-02", false, "RESERVED"));
|
||||
when(locations.findActiveResources(10001L, "FIRSTLEG", 90L)).thenReturn(resources);
|
||||
|
||||
assertThatThrownBy(() -> service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L,
|
||||
"FIRSTLEG", 90L, 9001L, new BigDecimal("2.0"), new BigDecimal("1.0"), 1)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("托盘数量超过入库单总箱数");
|
||||
verify(locations, never()).confirmOccupied(any());
|
||||
.containsExactly(1, 1, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -490,13 +477,15 @@ class PalletLocationAllocationServiceTest {
|
||||
|
||||
private static PalletAllocationDtos.ReserveRequest reserve(long businessId) {
|
||||
return new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", businessId, "RK-001", "measure-001",
|
||||
9001, new BigDecimal("2"), new BigDecimal("2"), "木托盘", LocalDateTime.now().plusMinutes(30), true);
|
||||
9001, new BigDecimal("2"), List.of(new PalletAllocationDtos.PalletReference(7001, 30)),
|
||||
LocalDateTime.now().plusMinutes(30), true);
|
||||
}
|
||||
|
||||
private static PalletEntity pallet(long id, String code) {
|
||||
PalletEntity entity = new PalletEntity();
|
||||
entity.setId(id);
|
||||
entity.setPalletCode(code);
|
||||
entity.setCapacityCbm(new BigDecimal("1.2"));
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class PalletVolumeSplitterTest {
|
||||
private final PalletVolumeSplitter splitter = new PalletVolumeSplitter();
|
||||
|
||||
@Test
|
||||
void shouldHandleCase01() {
|
||||
var result = splitter.split(new BigDecimal("20"), new BigDecimal("2"));
|
||||
assertThat(result.palletCount()).isEqualTo(10);
|
||||
assertThat(result.allocations()).allMatch(item -> item.allocatedVolumeCbm().compareTo(new BigDecimal("2")) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase02() {
|
||||
var result = splitter.split(new BigDecimal("20.1"), new BigDecimal("2"));
|
||||
assertThat(result.palletCount()).isEqualTo(11);
|
||||
assertThat(result.allocations().get(10).allocatedVolumeCbm()).isEqualByComparingTo("0.1");
|
||||
assertThat(result.allocations()).extracting(PalletVolumeSplitter.Allocation::allocationNo)
|
||||
.containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase03() {
|
||||
assertThatThrownBy(() -> splitter.split(null, BigDecimal.ONE)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> splitter.split(BigDecimal.ZERO, BigDecimal.ONE)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> splitter.split(BigDecimal.ONE, BigDecimal.ZERO)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> splitter.split(new BigDecimal("10001"), BigDecimal.ONE)).isInstanceOf(BizException.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user