fix: keep location guidance non-blocking
This commit is contained in:
@@ -25,6 +25,8 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import feign.FeignException;
|
import feign.FeignException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
@@ -37,6 +39,7 @@ import java.util.Set;
|
|||||||
/** 平台端入库测量、OCR 修正、重量确认和提交审核应用服务。 */
|
/** 平台端入库测量、OCR 修正、重量确认和提交审核应用服务。 */
|
||||||
@Service
|
@Service
|
||||||
public class InboundMeasurementService {
|
public class InboundMeasurementService {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(InboundMeasurementService.class);
|
||||||
/** 测量批次已提交,等待测量审核。 */
|
/** 测量批次已提交,等待测量审核。 */
|
||||||
private static final String STATUS_SUBMITTED = "SUBMITTED";
|
private static final String STATUS_SUBMITTED = "SUBMITTED";
|
||||||
/** OCR 或人工修正的重量已确认,可参与计费。 */
|
/** OCR 或人工修正的重量已确认,可参与计费。 */
|
||||||
@@ -298,8 +301,7 @@ public class InboundMeasurementService {
|
|||||||
request.adjustedWeightValue(), request.adjustedWeightUnit(),
|
request.adjustedWeightValue(), request.adjustedWeightUnit(),
|
||||||
request.adjustmentReason(), operatorId, operatorName);
|
request.adjustmentReason(), operatorId, operatorName);
|
||||||
} else if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) {
|
} else if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) {
|
||||||
locationOccupation.ensureForConfirmedWeight(
|
ensureLocationWithoutBlocking(tenantId, inboundId, operatorId, weight.getId(), weight.getVersion());
|
||||||
tenantId, inboundId, operatorId, weight.getId(), weight.getVersion());
|
|
||||||
return toWeight(weight);
|
return toWeight(weight);
|
||||||
}
|
}
|
||||||
BigDecimal value = effectiveValue(weight);
|
BigDecimal value = effectiveValue(weight);
|
||||||
@@ -329,11 +331,21 @@ public class InboundMeasurementService {
|
|||||||
insertRevision(weight, confirmedVersion, "CONFIRM", value, unit, value, unit,
|
insertRevision(weight, confirmedVersion, "CONFIRM", value, unit, value, unit,
|
||||||
grossWeightKg, netWeightKg, null, operatorId, operatorName);
|
grossWeightKg, netWeightKg, null, operatorId, operatorName);
|
||||||
recalculateConfirmedWeight(inbound, operatorId);
|
recalculateConfirmedWeight(inbound, operatorId);
|
||||||
locationOccupation.ensureForConfirmedWeight(
|
ensureLocationWithoutBlocking(tenantId, inboundId, operatorId, weight.getId(), confirmedVersion);
|
||||||
tenantId, inboundId, operatorId, weight.getId(), confirmedVersion);
|
|
||||||
return toWeight(weight);
|
return toWeight(weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureLocationWithoutBlocking(long tenantId, long inboundId, long operatorId,
|
||||||
|
long weightResultId, int weightVersion) {
|
||||||
|
try {
|
||||||
|
locationOccupation.ensureForConfirmedWeight(
|
||||||
|
tenantId, inboundId, operatorId, weightResultId, weightVersion);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
LOGGER.warn("称重确认已完成,但库位指引暂未生成,不阻断测量主流程: tenantId={}, inboundId={}, weightResultId={}",
|
||||||
|
tenantId, inboundId, weightResultId, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteWeightResult(long tenantId, long inboundId, long resultId,
|
public void deleteWeightResult(long tenantId, long inboundId, long resultId,
|
||||||
int version, long operatorId) {
|
int version, long operatorId) {
|
||||||
|
|||||||
+10
-1
@@ -94,7 +94,16 @@ public class MeasurementLocationOccupationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requireOccupied(long tenantId, long inboundId) {
|
public void ensureForSubmission(long tenantId, long inboundId, long operatorId) {
|
||||||
|
var weights = confirmedWeights(tenantId, inboundId);
|
||||||
|
InboundWeightResultEntity latest = weights.stream()
|
||||||
|
.max(java.util.Comparator.comparing(InboundWeightResultEntity::getConfirmedAt)
|
||||||
|
.thenComparing(InboundWeightResultEntity::getId))
|
||||||
|
.orElseThrow(() -> new BizException(ResultCode.CONFLICT,
|
||||||
|
"至少需要一张已确认的称重图片才能提交测量"));
|
||||||
|
ensureForConfirmedWeight(tenantId, inboundId, operatorId, latest.getId(),
|
||||||
|
latest.getVersion() == null ? 0 : latest.getVersion());
|
||||||
|
|
||||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||||
long warehouseId = requireWarehouse(order);
|
long warehouseId = requireWarehouse(order);
|
||||||
var confirmedWeights = confirmedWeights(tenantId, inboundId);
|
var confirmedWeights = confirmedWeights(tenantId, inboundId);
|
||||||
|
|||||||
+9
-1
@@ -6,9 +6,12 @@ import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
|||||||
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class MeasurementSubmissionService {
|
public class MeasurementSubmissionService {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(MeasurementSubmissionService.class);
|
||||||
@Resource
|
@Resource
|
||||||
private InboundMeasurementService measurements;
|
private InboundMeasurementService measurements;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -19,7 +22,12 @@ public class MeasurementSubmissionService {
|
|||||||
long tenantId, long inboundId, long operatorId,
|
long tenantId, long inboundId, long operatorId,
|
||||||
InboundMeasurementRequests.SubmitRequest request) {
|
InboundMeasurementRequests.SubmitRequest request) {
|
||||||
var result = measurements.submit(tenantId, inboundId, operatorId, request);
|
var result = measurements.submit(tenantId, inboundId, operatorId, request);
|
||||||
locations.requireOccupied(tenantId, inboundId);
|
try {
|
||||||
|
locations.ensureForSubmission(tenantId, inboundId, operatorId);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
LOGGER.warn("测量已提交,但库位指引暂未补齐,不阻断提交审核主流程: tenantId={}, inboundId={}",
|
||||||
|
tenantId, inboundId, exception);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.eq;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.clearInvocations;
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -579,6 +580,28 @@ class InboundMeasurementServiceTest {
|
|||||||
assertThat(result.slotStatus()).isEqualTo("BOUND");
|
assertThat(result.slotStatus()).isEqualTo("BOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void locationAllocationFailureDoesNotBlockWeightConfirmation() {
|
||||||
|
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||||
|
InboundWeightResultEntity weight = weight(20L, "30.000", "KG", "PENDING_CONFIRM", 0);
|
||||||
|
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||||
|
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||||
|
when(repository.updateWeight(weight)).thenReturn(true);
|
||||||
|
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
|
||||||
|
doThrow(new IllegalStateException("warehouse unavailable"))
|
||||||
|
.when(locationOccupation).ensureForConfirmedWeight(1L, 10L, 9L, 20L, 1);
|
||||||
|
|
||||||
|
InboundMeasurementResponses.WeightResult result = service.confirmWeightResult(
|
||||||
|
1L, 10L, 20L, 9L, "操作员",
|
||||||
|
new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null));
|
||||||
|
|
||||||
|
assertThat(result.recognitionStatus()).isEqualTo("CONFIRMED");
|
||||||
|
assertThat(result.grossWeightKg()).isEqualByComparingTo("30.000");
|
||||||
|
assertThat(result.locationCode()).isNull();
|
||||||
|
verify(repository).insertRevision(any(InboundWeightRevisionEntity.class));
|
||||||
|
verify(repository).updateInboundWeightSummary(inbound);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void confirmingFifthPalletSeparatelyAggregatesGrossAndCargoNetWeight() {
|
void confirmingFifthPalletSeparatelyAggregatesGrossAndCargoNetWeight() {
|
||||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||||
|
|||||||
+58
-1
@@ -320,7 +320,7 @@ class MeasurementLocationOccupationServiceTest {
|
|||||||
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights);
|
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights);
|
||||||
when(allocations.list(BUSINESS_TENANT, INBOUND)).thenReturn(List.of(first, second));
|
when(allocations.list(BUSINESS_TENANT, INBOUND)).thenReturn(List.of(first, second));
|
||||||
|
|
||||||
service.requireOccupied(BUSINESS_TENANT, INBOUND);
|
service.ensureForSubmission(BUSINESS_TENANT, INBOUND, OPERATOR);
|
||||||
|
|
||||||
verify(warehouse, never()).reserveBlock(org.mockito.ArgumentMatchers.anyString(),
|
verify(warehouse, never()).reserveBlock(org.mockito.ArgumentMatchers.anyString(),
|
||||||
org.mockito.ArgumentMatchers.any());
|
org.mockito.ArgumentMatchers.any());
|
||||||
@@ -330,6 +330,51 @@ class MeasurementLocationOccupationServiceTest {
|
|||||||
org.mockito.ArgumentMatchers.any());
|
org.mockito.ArgumentMatchers.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void submissionReconcilesMissingBlockFromLatestConfirmedWeight() {
|
||||||
|
InboundOrderEntity inbound = inbound();
|
||||||
|
inbound.setActualVolumeCbm(new BigDecimal("2.5000"));
|
||||||
|
List<InboundWeightResultEntity> weights = confirmedWeights().subList(0, 1);
|
||||||
|
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound);
|
||||||
|
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights);
|
||||||
|
when(warehouse.reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||||
|
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("RESERVED", 3)));
|
||||||
|
when(warehouse.confirmBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||||
|
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
|
||||||
|
when(allocations.list(BUSINESS_TENANT, INBOUND))
|
||||||
|
.thenReturn(List.of(), blockSlotsBoundTo(weights));
|
||||||
|
|
||||||
|
service.ensureForSubmission(BUSINESS_TENANT, INBOUND, OPERATOR);
|
||||||
|
|
||||||
|
verify(warehouse).reserveBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||||
|
org.mockito.ArgumentMatchers.argThat(request -> request.requiredSlotCount() == 3));
|
||||||
|
verify(allocations).bindConfirmedWeights(BUSINESS_TENANT, INBOUND, weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void submissionExpandsUndersizedBlockWhenMeasuredVolumeGrows() {
|
||||||
|
InboundOrderEntity inbound = inbound();
|
||||||
|
inbound.setActualVolumeCbm(new BigDecimal("2.5000"));
|
||||||
|
List<InboundWeightResultEntity> weights = confirmedWeights().subList(0, 1);
|
||||||
|
var current = slot(1);
|
||||||
|
current.setWeightResultId(weights.get(0).getId());
|
||||||
|
current.setSlotStatus("BOUND");
|
||||||
|
when(workflow.findOwned(BUSINESS_TENANT, INBOUND)).thenReturn(inbound);
|
||||||
|
when(measurements.listWeights(BUSINESS_TENANT, INBOUND)).thenReturn(weights);
|
||||||
|
when(allocations.list(BUSINESS_TENANT, INBOUND))
|
||||||
|
.thenReturn(List.of(current), blockSlotsBoundTo(weights));
|
||||||
|
when(warehouse.expandBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||||
|
org.mockito.ArgumentMatchers.any())).thenReturn(ApiResponse.success(block("OCCUPIED", 3)));
|
||||||
|
|
||||||
|
service.ensureForSubmission(BUSINESS_TENANT, INBOUND, OPERATOR);
|
||||||
|
|
||||||
|
verify(warehouse).expandBlock(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||||
|
org.mockito.ArgumentMatchers.argThat(request -> request.blockId() == 500L
|
||||||
|
&& request.requiredSlotCount() == 3));
|
||||||
|
verify(allocations).replaceBlock(BUSINESS_TENANT, INBOUND, WAREHOUSE,
|
||||||
|
20, new BigDecimal("2.5000"), block("OCCUPIED", 3));
|
||||||
|
}
|
||||||
|
|
||||||
private static InboundProperties properties() {
|
private static InboundProperties properties() {
|
||||||
InboundProperties value = new InboundProperties();
|
InboundProperties value = new InboundProperties();
|
||||||
value.setDefaultPalletCapacityCbm(new BigDecimal("1.200000"));
|
value.setDefaultPalletCapacityCbm(new BigDecimal("1.200000"));
|
||||||
@@ -377,6 +422,18 @@ class MeasurementLocationOccupationServiceTest {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> blockSlotsBoundTo(
|
||||||
|
List<InboundWeightResultEntity> weights) {
|
||||||
|
return java.util.stream.IntStream.rangeClosed(1, 3).mapToObj(slotNo -> {
|
||||||
|
var slot = slot(slotNo);
|
||||||
|
if (slotNo <= weights.size()) {
|
||||||
|
slot.setWeightResultId(weights.get(slotNo - 1).getId());
|
||||||
|
slot.setSlotStatus("BOUND");
|
||||||
|
}
|
||||||
|
return slot;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
private static List<InboundWeightResultEntity> confirmedWeights() {
|
private static List<InboundWeightResultEntity> confirmedWeights() {
|
||||||
return java.util.stream.LongStream.rangeClosed(1, 3).mapToObj(index -> {
|
return java.util.stream.LongStream.rangeClosed(1, 3).mapToObj(index -> {
|
||||||
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
||||||
|
|||||||
+6
-2
@@ -9,6 +9,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ class MeasurementSubmissionServiceTest {
|
|||||||
|
|
||||||
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
|
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
|
||||||
|
|
||||||
verify(locations).requireOccupied(10001L, 88L);
|
verify(locations).ensureForSubmission(10001L, 88L, 9L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -44,7 +45,10 @@ class MeasurementSubmissionServiceTest {
|
|||||||
var submitted = new InboundMeasurementResponses.MeasurementSubmitted(
|
var submitted = new InboundMeasurementResponses.MeasurementSubmitted(
|
||||||
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
|
30L, 3, "MEASURE_PENDING", "PENDING", "STANDARD", null, null, null);
|
||||||
when(measurements.submit(10001L, 88L, 9L, request)).thenReturn(submitted);
|
when(measurements.submit(10001L, 88L, 9L, request)).thenReturn(submitted);
|
||||||
|
doThrow(new IllegalStateException("warehouse unavailable"))
|
||||||
|
.when(locations).ensureForSubmission(10001L, 88L, 9L);
|
||||||
|
|
||||||
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
|
assertThat(service.submit(10001L, 88L, 9L, request)).isSameAs(submitted);
|
||||||
verify(locations).requireOccupied(10001L, 88L);
|
verify(locations).ensureForSubmission(10001L, 88L, 9L);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user