二期内容提交
This commit is contained in:
+10
-5
@@ -3,7 +3,8 @@ package com.baiyue.firstleg.controller;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -13,12 +14,14 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@DisplayName("第三方测量开放接口契约")
|
||||
class OpenApiInboundMeasurementControllerContractTest {
|
||||
@Test
|
||||
@DisplayName("回调以原始字符串接收报文,保证签名正文不被重新序列化")
|
||||
void shouldReceiveRawRequestBody() throws Exception {
|
||||
@DisplayName("回调接收原始data和唯一图片,保证签名与一对一绑定")
|
||||
void shouldReceiveRawDataAndImage() throws Exception {
|
||||
Method method = OpenApiInboundMeasurementController.class.getDeclaredMethod("receive",
|
||||
String.class, String.class, String.class, String.class, String.class, String.class);
|
||||
assertThat(method.getParameters()[5].getAnnotation(RequestBody.class)).isNotNull();
|
||||
String.class, String.class, String.class, String.class, String.class,
|
||||
String.class, MultipartFile.class);
|
||||
assertThat(method.getParameters()[5].getAnnotation(RequestPart.class).value()).isEqualTo("data");
|
||||
assertThat(method.getParameters()[5].getType()).isEqualTo(String.class);
|
||||
assertThat(method.getParameters()[6].getAnnotation(RequestPart.class).value()).isEqualTo("image");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -33,6 +36,8 @@ class OpenApiInboundMeasurementControllerContractTest {
|
||||
.contains("Idempotency-Key")
|
||||
.contains("appKey + eventNo")
|
||||
.contains("Nonce")
|
||||
.contains("inboundDetailId")
|
||||
.contains("multipart/form-data")
|
||||
.contains("不改变入库单状态");
|
||||
}
|
||||
}
|
||||
|
||||
+89
-10
@@ -1,7 +1,11 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.firstleg.dto.OpenApiMeasurementDtos.EventResponse;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderLookupEntity;
|
||||
import com.baiyue.firstleg.entity.ThirdPartyCallLogEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementOpenApiRepository;
|
||||
@@ -15,12 +19,15 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.TransactionCallback;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -30,15 +37,19 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
@DisplayName("第三方测量开放接口服务")
|
||||
class InboundMeasurementOpenApiServiceTest {
|
||||
private static final String BODY = """
|
||||
{"eventNo":"M202608010001","warehouseCode":"CN-YW-01","inboundCode":"RK202608010001",
|
||||
"deviceCode":"DEVICE-01","measuredAt":"2026-08-01T10:30:00+08:00",
|
||||
"items":[{"itemNo":"001","lengthCm":50.20,"widthCm":40.10,"heightCm":30.00,
|
||||
"quantity":5,"boxSpecification":"50.2x40.1x30cm"}]}
|
||||
"externalDetailNo":"BOX-001","inboundDetailId":"80001","deviceCode":"DEVICE-01",
|
||||
"measuredAt":"2026-08-01T10:30:00+08:00","lengthCm":50.20,"widthCm":40.10,
|
||||
"heightCm":30.00,"packageCount":5,"unitWeightKg":12.5,
|
||||
"boxSpecification":"50.2x40.1x30cm"}
|
||||
""";
|
||||
private static final byte[] IMAGE = "image-content".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
@Test
|
||||
@DisplayName("回调只写批次明细和幂等结果,不存在入库主单update入口")
|
||||
@@ -47,12 +58,23 @@ class InboundMeasurementOpenApiServiceTest {
|
||||
InboundOrderLookupEntity inbound = inbound();
|
||||
when(fixture.repository.findInbound(10001L, "RK202608010001")).thenReturn(inbound);
|
||||
|
||||
EventResponse response = fixture.service.receive(headers("nonce-1"), BODY);
|
||||
EventResponse response = fixture.service.receive(headers("nonce-1"), BODY, image());
|
||||
|
||||
assertThat(response.accepted()).isTrue();
|
||||
assertThat(response.status()).isEqualTo("RECEIVED");
|
||||
verify(fixture.repository).insertMeasurement(any(InboundMeasurementEntity.class));
|
||||
verify(fixture.repository).insertItem(any());
|
||||
ArgumentCaptor<InboundMeasurementItemEntity> itemCaptor =
|
||||
ArgumentCaptor.forClass(InboundMeasurementItemEntity.class);
|
||||
verify(fixture.repository).insertItem(itemCaptor.capture());
|
||||
assertThat(itemCaptor.getValue().getInboundDetailId()).isEqualTo(80001L);
|
||||
assertThat(itemCaptor.getValue().getExternalDetailNo()).isEqualTo("BOX-001");
|
||||
assertThat(itemCaptor.getValue().getSourceImageFileId()).isEqualTo(99001L);
|
||||
assertThat(itemCaptor.getValue().getItemNo()).isEqualTo("1");
|
||||
assertThat(itemCaptor.getValue().getTotalVolumeCbm()).isEqualByComparingTo("0.3020");
|
||||
verify(fixture.orderCargo).detail("internal-token", 10001L, 60001L, 80001L);
|
||||
verify(fixture.images).upload(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.eq(90001L), org.mockito.ArgumentMatchers.eq(10001L),
|
||||
org.mockito.ArgumentMatchers.eq(70001L), any());
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
InboundMeasurementOpenApiRepository.class.getDeclaredMethod("findInbound", long.class, String.class));
|
||||
assertThat(InboundMeasurementOpenApiRepository.class.getDeclaredMethods())
|
||||
@@ -67,38 +89,82 @@ class InboundMeasurementOpenApiServiceTest {
|
||||
EventResponse first = new EventResponse("M202608010001", "RK202608010001", true,
|
||||
"RECEIVED", OffsetDateTime.parse("2026-08-01T02:30:00Z"));
|
||||
ThirdPartyCallLogEntity existing = new ThirdPartyCallLogEntity();
|
||||
existing.setRequestHash(MeasurementRequestAuthenticator.sha256Hex(BODY));
|
||||
existing.setRequestHash(requestHash());
|
||||
existing.setResultJson(mapper.writeValueAsString(first));
|
||||
existing.setProcessingStatus("ACCEPTED");
|
||||
when(fixture.logs.claimIdempotency(1L)).thenReturn(false);
|
||||
when(fixture.logs.findIdempotencyOwner(10001L, "device-provider", "idem-1")).thenReturn(existing);
|
||||
|
||||
EventResponse duplicate = fixture.service.receive(headers("nonce-2"), BODY);
|
||||
EventResponse duplicate = fixture.service.receive(headers("nonce-2"), BODY, image());
|
||||
|
||||
assertThat(duplicate).isEqualTo(first);
|
||||
verify(fixture.repository, never()).insertMeasurement(any());
|
||||
verify(fixture.repository, never()).insertItem(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("同一订单明细和第三方明细号已经绑定时在上传图片前拒绝")
|
||||
void shouldRejectAlreadyBoundExternalDetailBeforeUploadingImage() {
|
||||
Fixture fixture = fixture();
|
||||
when(fixture.repository.findInbound(10001L, "RK202608010001")).thenReturn(inbound());
|
||||
when(fixture.repository.findExternalDetail(10001L, 80001L, "BOX-001"))
|
||||
.thenReturn(new InboundMeasurementItemEntity());
|
||||
|
||||
assertThatThrownBy(() -> fixture.service.receive(headers("nonce-3"), BODY, image()))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class)
|
||||
.hasMessageContaining("已绑定");
|
||||
|
||||
verify(fixture.images, never()).upload(anyString(),
|
||||
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
|
||||
org.mockito.ArgumentMatchers.anyLong(), any());
|
||||
verify(fixture.repository, never()).insertMeasurement(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("图片已保存但测量落库失败时调用文件中心补偿清理")
|
||||
void shouldCompensateImageWhenMeasurementPersistenceFails() {
|
||||
Fixture fixture = fixture();
|
||||
when(fixture.repository.findInbound(10001L, "RK202608010001")).thenReturn(inbound());
|
||||
doThrow(new IllegalStateException("db failed"))
|
||||
.when(fixture.repository).insertMeasurement(any(InboundMeasurementEntity.class));
|
||||
|
||||
assertThatThrownBy(() -> fixture.service.receive(headers("nonce-4"), BODY, image()))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("db failed");
|
||||
|
||||
verify(fixture.images).delete("internal-token", 90001L, 10001L, 70001L, 99001L);
|
||||
verify(fixture.repository, never()).insertItem(any());
|
||||
}
|
||||
|
||||
private Fixture fixture() {
|
||||
InboundMeasurementOpenApiRepository repository = mock(InboundMeasurementOpenApiRepository.class);
|
||||
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
|
||||
ThirdPartyCallLogService logs = mock(ThirdPartyCallLogService.class);
|
||||
TransactionTemplate transactions = mock(TransactionTemplate.class);
|
||||
when(logs.start(anyString(), anyString(), any(), anyString())).thenReturn(1L);
|
||||
OrderCargoClient orderCargo = mock(OrderCargoClient.class);
|
||||
MeasurementImageClient images = mock(MeasurementImageClient.class);
|
||||
when(logs.start(anyString(), anyString(), any(), anyString(), anyString())).thenReturn(1L);
|
||||
when(logs.claimIdempotency(1L)).thenReturn(true);
|
||||
when(authenticator.authenticate(anyString(), anyString(), any(), anyString(), anyBoolean()))
|
||||
.thenReturn(new AuthenticatedMeasurementApp("device-provider", 10001L, 90001L,
|
||||
Map.of("CN-YW-01", 30001L)));
|
||||
when(orderCargo.detail("internal-token", 10001L, 60001L, 80001L)).thenReturn(ApiResponse.success(
|
||||
new OrderCargoClient.CargoBinding(80001L, 60001L, 1, "测试货物", 5,
|
||||
new BigDecimal("62.500"), new BigDecimal("0.3010"))));
|
||||
when(images.upload(org.mockito.ArgumentMatchers.eq("internal-token"),
|
||||
org.mockito.ArgumentMatchers.eq(90001L), org.mockito.ArgumentMatchers.eq(10001L),
|
||||
org.mockito.ArgumentMatchers.eq(70001L), any())).thenReturn(ApiResponse.success(
|
||||
new MeasurementImageClient.UploadResult(99001L, "measure.jpg", "image/jpeg", 13L,
|
||||
"NOT_REQUIRED", null, null, null, null)));
|
||||
when(transactions.execute(any())).thenAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
TransactionCallback<EventResponse> callback = invocation.getArgument(0);
|
||||
return callback.doInTransaction(mock(TransactionStatus.class));
|
||||
});
|
||||
InboundMeasurementOpenApiService service = new InboundMeasurementOpenApiService(repository,
|
||||
authenticator, logs, objectMapper(),
|
||||
authenticator, logs, orderCargo, images, "internal-token", objectMapper(),
|
||||
Validation.buildDefaultValidatorFactory().getValidator(), transactions);
|
||||
return new Fixture(repository, logs, service);
|
||||
return new Fixture(repository, logs, orderCargo, images, service);
|
||||
}
|
||||
|
||||
private ObjectMapper objectMapper() {
|
||||
@@ -110,6 +176,7 @@ class InboundMeasurementOpenApiServiceTest {
|
||||
inbound.setId(70001L);
|
||||
inbound.setTenantId(10001L);
|
||||
inbound.setInboundCode("RK202608010001");
|
||||
inbound.setOrderId(60001L);
|
||||
inbound.setWarehouseId(30001L);
|
||||
inbound.setProcessStatus("WAIT_MEASURE");
|
||||
inbound.setDeleted(0);
|
||||
@@ -120,8 +187,20 @@ class InboundMeasurementOpenApiServiceTest {
|
||||
return new MeasurementRequestHeaders("device-provider", "1785551400000", nonce, "signature", "idem-1");
|
||||
}
|
||||
|
||||
private MockMultipartFile image() {
|
||||
return new MockMultipartFile("image", "measure.jpg", "image/jpeg", IMAGE);
|
||||
}
|
||||
|
||||
private String requestHash() throws Exception {
|
||||
String imageHash = java.util.HexFormat.of().formatHex(
|
||||
java.security.MessageDigest.getInstance("SHA-256").digest(IMAGE));
|
||||
return MeasurementRequestAuthenticator.sha256Hex(BODY + "\n" + imageHash);
|
||||
}
|
||||
|
||||
private record Fixture(InboundMeasurementOpenApiRepository repository,
|
||||
ThirdPartyCallLogService logs,
|
||||
OrderCargoClient orderCargo,
|
||||
MeasurementImageClient images,
|
||||
InboundMeasurementOpenApiService service) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user