优化
This commit is contained in:
@@ -25,7 +25,7 @@ import java.util.Set;
|
||||
|
||||
/** 首次生成后不可变的箱号分配;重打和测量解析只读取该映射。 */
|
||||
@Service
|
||||
public class InboundCartonLabelService {
|
||||
public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
private static final int MAX_LABELS = 2000;
|
||||
private static final int MAX_QR_CODE_POINTS = 34;
|
||||
private static final Set<String> ALLOCATABLE_STATUSES = Set.of("WAIT_MEASURE", "MEASURING");
|
||||
@@ -82,16 +82,29 @@ public class InboundCartonLabelService {
|
||||
batch.totalCount(), labels);
|
||||
}
|
||||
|
||||
public InboundCartonLabels.Resolution resolveByBoxCode(long tenantId, String boxCode) {
|
||||
@Override
|
||||
public CartonLabelBindingResolver.Binding resolveByBoxCode(long tenantId, String boxCode) {
|
||||
InboundCartonLabelEntity label = requireByBoxCode(tenantId, boxCode);
|
||||
return new CartonLabelBindingResolver.Binding(label.getInboundId(), label.getInboundDetailId(),
|
||||
label.getInboundCode(), label.getCartonIndex(), label.getTotalCartonCount(),
|
||||
label.getBoxSpecification());
|
||||
}
|
||||
|
||||
public InboundCartonLabels.Resolution resolveDetailsByBoxCode(long tenantId, String boxCode) {
|
||||
InboundCartonLabelEntity label = requireByBoxCode(tenantId, boxCode);
|
||||
return new InboundCartonLabels.Resolution(label.getInboundId(), label.getOrderId(),
|
||||
label.getInboundDetailId(), label.getBoxCode(), label.getTotalCartonCount(),
|
||||
label.getCartonIndex(), label.getCargoItemNo(), label.getCargoName(),
|
||||
label.getBoxSpecification());
|
||||
}
|
||||
|
||||
private InboundCartonLabelEntity requireByBoxCode(long tenantId, String boxCode) {
|
||||
if (tenantId <= 0 || !StringUtils.hasText(boxCode)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "客户ID和箱号不能为空");
|
||||
}
|
||||
InboundCartonLabelEntity label = repository.findByBoxCode(tenantId, boxCode.trim());
|
||||
if (label == null) throw new BizException(ResultCode.NOT_FOUND, "箱号不存在");
|
||||
return new InboundCartonLabels.Resolution(label.getInboundId(), label.getOrderId(),
|
||||
label.getInboundDetailId(), label.getBoxCode(), label.getTotalCartonCount(),
|
||||
label.getCartonIndex(), label.getCargoItemNo(), label.getCargoName(),
|
||||
label.getBoxSpecification());
|
||||
return label;
|
||||
}
|
||||
|
||||
private List<OrderCargoClient.CargoLabelBinding> loadCargoes(InboundOrderEntity inbound) {
|
||||
@@ -105,8 +118,10 @@ public class InboundCartonLabelService {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "订单没有可生成唛头的货物明细");
|
||||
}
|
||||
return response.data().stream()
|
||||
.sorted(java.util.Comparator.comparing(OrderCargoClient.CargoLabelBinding::itemNo)
|
||||
.thenComparing(OrderCargoClient.CargoLabelBinding::cargoId))
|
||||
.sorted(java.util.Comparator.comparing(OrderCargoClient.CargoLabelBinding::itemNo,
|
||||
java.util.Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(OrderCargoClient.CargoLabelBinding::cargoId,
|
||||
java.util.Comparator.nullsLast(Long::compareTo)))
|
||||
.toList();
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法生成唛头");
|
||||
|
||||
+3
@@ -22,6 +22,9 @@ class PlatformInboundMeasurementControllerContractTest {
|
||||
assertAuthority("submit", "hasAuthority('platform:firstleg:inbound:measure:submit')",
|
||||
AuthenticatedUser.class, long.class, long.class,
|
||||
InboundMeasurementRequests.SubmitRequest.class);
|
||||
assertAuthority("updateConfirmedPackageCount", "hasAuthority('platform:firstleg:inbound:measure:edit')",
|
||||
AuthenticatedUser.class, long.class, long.class, long.class,
|
||||
InboundMeasurementRequests.ConfirmedPackageCountRequest.class);
|
||||
assertAuthority("createWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')",
|
||||
AuthenticatedUser.class, long.class, long.class,
|
||||
InboundMeasurementRequests.CreateWeightResultRequest.class);
|
||||
|
||||
+18
@@ -38,4 +38,22 @@ class InboundWorkflowSqlContractTest {
|
||||
.contains("SET warehouse_resource_tenant_id = 0")
|
||||
.contains("MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("单箱当前测量记录包含箱号、设备元数据和唯一约束")
|
||||
void shouldPersistCurrentCartonMeasurementFields() throws Exception {
|
||||
String schema = new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String migration = new ClassPathResource("db/phase2-carton-measurement-current-record.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(schema)
|
||||
.contains("box_code VARCHAR(128)")
|
||||
.contains("machine_no VARCHAR(64)")
|
||||
.contains("device_measured_at DATETIME(3)")
|
||||
.contains("uk_measure_box_code");
|
||||
assertThat(migration)
|
||||
.contains("phase2_add_carton_measurement_column")
|
||||
.contains("uk_measure_box_code")
|
||||
.contains("information_schema.STATISTICS");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.support.TransactionCallback;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class CartonMeasurementServiceTest {
|
||||
private final InboundMeasurementRepository repository = mock(InboundMeasurementRepository.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
private final ObjectProvider<CartonLabelBindingResolver> resolverProvider = mock(ObjectProvider.class);
|
||||
private final CartonLabelBindingResolver resolver = mock(CartonLabelBindingResolver.class);
|
||||
private final MeasurementImageClient images = mock(MeasurementImageClient.class);
|
||||
private final TransactionTemplate transactions = mock(TransactionTemplate.class);
|
||||
private CartonMeasurementService service;
|
||||
private final AtomicReference<InboundMeasurementItemEntity> insertedItem = new AtomicReference<>();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CartonMeasurementService(repository, resolverProvider, images, transactions);
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||
when(resolverProvider.getIfAvailable()).thenReturn(resolver);
|
||||
when(resolver.resolveByBoxCode(10001L, "RK-001#5-1"))
|
||||
.thenReturn(new CartonLabelBindingResolver.Binding(70001L, 80001L, "RK-001", 1, 5, "50x40x30cm"));
|
||||
when(repository.findInbound(10001L, 70001L)).thenReturn(inbound("WAIT_MEASURE"));
|
||||
when(images.upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any()))
|
||||
.thenReturn(ApiResponse.success(new MeasurementImageClient.UploadResult(
|
||||
99001L, "measure.jpg", "image/jpeg", 10L, "NOT_REQUIRED", null, null, null, null)));
|
||||
when(transactions.execute(any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
TransactionCallback<CartonMeasurementResult> callback = invocation.getArgument(0);
|
||||
return callback.doInTransaction(mock(org.springframework.transaction.TransactionStatus.class));
|
||||
});
|
||||
doAnswer(invocation -> {
|
||||
insertedItem.set(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(repository).insertItem(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstScanCreatesOneRecordWithConfirmedCountOne() {
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 0);
|
||||
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);
|
||||
|
||||
CartonMeasurementResult result = service.upsert(10001L, 90001L, command());
|
||||
|
||||
assertThat(result.created()).isTrue();
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(1);
|
||||
assertThat(result.boxCode()).isEqualTo("RK-001#5-1");
|
||||
assertThat(insertedItem.get().getMeasurementId()).isEqualTo(30001L);
|
||||
assertThat(insertedItem.get().getTotalVolumeCbm()).isEqualByComparingTo("0.0600");
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedScanReplacesDeviceValuesButPreservesManualCountAndOldImageIsDeleted() {
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 4);
|
||||
InboundMeasurementItemEntity existing = item(40001L, 30001L, 7, 88001L);
|
||||
when(repository.findItemByBoxCode(10001L, "RK-001#5-1")).thenReturn(existing);
|
||||
when(repository.findMeasurement(10001L, 70001L, 30001L)).thenReturn(measurement);
|
||||
when(repository.updateItem(existing)).thenReturn(true);
|
||||
when(repository.listItems(10001L, 30001L)).thenReturn(List.of(existing));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
|
||||
CartonMeasurementResult result = service.upsert(10001L, 90001L, command());
|
||||
|
||||
assertThat(result.created()).isFalse();
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(7);
|
||||
assertThat(existing.getLengthCm()).isEqualByComparingTo("60.100");
|
||||
assertThat(existing.getSourceImageFileId()).isEqualTo(99001L);
|
||||
verify(images).delete("internal-token", 90001L, 10001L, 70001L, 88001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submittedInboundRejectsDeviceBeforeUploadingImage() {
|
||||
when(repository.findInbound(10001L, 70001L)).thenReturn(inbound("MEASURE_PENDING"));
|
||||
|
||||
assertThatThrownBy(() -> service.upsert(10001L, 90001L, command()))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不能新增或覆盖");
|
||||
|
||||
verify(images, never()).upload(any(), any(Long.TYPE), any(Long.TYPE), any(Long.TYPE), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmedPackageCountUsesMeasurementOptimisticVersion() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundMeasurementEntity measurement = measurement(30001L, "RECEIVED", 2);
|
||||
InboundMeasurementItemEntity item = item(40001L, 30001L, 1, 99001L);
|
||||
when(repository.findItem(10001L, 40001L)).thenReturn(item);
|
||||
when(repository.findMeasurement(10001L, 70001L, 30001L)).thenReturn(measurement);
|
||||
when(repository.updateItem(item)).thenReturn(true);
|
||||
when(repository.listItems(10001L, 30001L)).thenReturn(List.of(item));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
|
||||
CartonMeasurementResult result = service.updateConfirmedPackageCount(
|
||||
10001L, 70001L, 40001L, 2, 5);
|
||||
|
||||
assertThat(result.confirmedPackageCount()).isEqualTo(5);
|
||||
assertThat(item.getPackageCount()).isEqualTo(5);
|
||||
verify(repository).updateMeasurement(measurement);
|
||||
}
|
||||
|
||||
private CartonMeasurementCommand command() {
|
||||
return new CartonMeasurementCommand("RK-001#5-1", new BigDecimal("60.100"),
|
||||
new BigDecimal("40.200"), new BigDecimal("30.300"), new BigDecimal("12.500"),
|
||||
"MJ-01", LocalDateTime.parse("2026-08-07T10:30:00"),
|
||||
new MockMultipartFile("image", "measure.jpg", "image/jpeg", "image".getBytes()));
|
||||
}
|
||||
|
||||
private InboundOrderEntity inbound(String status) {
|
||||
InboundOrderEntity inbound = new InboundOrderEntity();
|
||||
inbound.setId(70001L); inbound.setTenantId(10001L); inbound.setProcessStatus(status);
|
||||
inbound.setTotalPackageCount(5); inbound.setVersion(0);
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private InboundMeasurementEntity measurement(long id, String status, int version) {
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(id); measurement.setTenantId(10001L); measurement.setInboundId(70001L);
|
||||
measurement.setStatus(status); measurement.setVersion(version);
|
||||
return measurement;
|
||||
}
|
||||
|
||||
private InboundMeasurementItemEntity item(long id, long measurementId, int packageCount, long imageId) {
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setId(id); item.setTenantId(10001L); item.setMeasurementId(measurementId);
|
||||
item.setBoxCode("RK-001#5-1"); item.setPackageCount(packageCount);
|
||||
item.setLengthCm(new BigDecimal("50")); item.setWidthCm(new BigDecimal("40"));
|
||||
item.setHeightCm(new BigDecimal("30")); item.setUnitWeightKg(new BigDecimal("10"));
|
||||
item.setSourceImageFileId(imageId); item.setItemNo("B1"); item.setBoxSpecification("50x40x30cm");
|
||||
return item;
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -90,12 +90,26 @@ class InboundCartonLabelServiceTest {
|
||||
var resolved = service.resolveByBoxCode(10001L, " PCHK2608040174#1-1 ");
|
||||
|
||||
assertThat(resolved.inboundId()).isEqualTo(70001L);
|
||||
assertThat(resolved.orderId()).isEqualTo(60001L);
|
||||
assertThat(resolved.inboundDetailId()).isEqualTo(80001L);
|
||||
assertThat(resolved.inboundCode()).isEqualTo("PCHK2608040174");
|
||||
assertThat(resolved.cartonIndex()).isEqualTo(1);
|
||||
assertThat(resolved.totalCartons()).isEqualTo(1);
|
||||
assertThat(resolved.boxSpecification()).isEqualTo("60x40x30cm");
|
||||
verify(repository).findByBoxCode(10001L, "PCHK2608040174#1-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesDetailedResolutionForFirstlegConsumers() {
|
||||
InboundCartonLabelEntity label = labels(inbound(), 1).get(0);
|
||||
when(repository.findByBoxCode(10001L, "PCHK2608040174#1-1")).thenReturn(label);
|
||||
|
||||
var resolved = service.resolveDetailsByBoxCode(10001L, "PCHK2608040174#1-1");
|
||||
|
||||
assertThat(resolved.orderId()).isEqualTo(60001L);
|
||||
assertThat(resolved.boxCode()).isEqualTo("PCHK2608040174#1-1");
|
||||
assertThat(resolved.cargoItemNo()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesFirstAllocationAfterMeasurementSubmission() {
|
||||
InboundOrderEntity inbound = inbound();
|
||||
|
||||
@@ -116,6 +116,7 @@ class InboundMeasurementServiceTest {
|
||||
measurement.setTotalVolumeCbm(new BigDecimal("1.2345"));
|
||||
measurement.setVolumeWeightKg(new BigDecimal("206.162"));
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setPackageCount(3);
|
||||
item.setSourceImageFileId(500L);
|
||||
InboundWeightResultEntity weight = weight(20L, "20.000", "KG", "CONFIRMED", 1);
|
||||
weight.setFinalWeightKg(new BigDecimal("20.000"));
|
||||
@@ -145,6 +146,7 @@ class InboundMeasurementServiceTest {
|
||||
measurement.setStatus("DRAFT");
|
||||
measurement.setVersion(0);
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setPackageCount(3);
|
||||
item.setSourceImageFileId(500L);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findMeasurement(1L, 10L, 30L)).thenReturn(measurement);
|
||||
@@ -195,6 +197,7 @@ class InboundMeasurementServiceTest {
|
||||
inbound.setId(10L);
|
||||
inbound.setTenantId(1L);
|
||||
inbound.setProcessStatus(status);
|
||||
inbound.setTotalPackageCount(3);
|
||||
inbound.setVersion(0);
|
||||
return inbound;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user