This commit is contained in:
2026-08-07 17:14:40 +08:00
parent f4c3a95772
commit 91f31d9567
20 changed files with 354 additions and 7 deletions
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -19,6 +20,8 @@ class InboundOrderControllerContractTest {
AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
assertAuthority("detail", "hasAuthority('platform:firstleg:inbound:view')",
AuthenticatedUser.class, long.class, long.class);
assertAuthority("printCartonLabels", "hasAuthority('platform:firstleg:inbound:view')",
AuthenticatedUser.class, long.class, long.class, List.class, String.class);
Method page = PlatformInboundOrderController.class.getDeclaredMethod(
"page", AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
@@ -0,0 +1,22 @@
package com.baiyue.firstleg.mapper;
import org.apache.ibatis.annotations.Delete;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("价格版本渠道 Mapper 契约")
class PriceBookChannelMapperTest {
@Test
@DisplayName("重新生成版本快照时使用物理删除避免唯一键冲突")
void shouldPhysicallyDeleteVersionChannels() throws Exception {
Method method = PriceBookChannelMapper.class
.getMethod("physicallyDeleteByPriceBookVersionId", long.class);
assertThat(method.getAnnotation(Delete.class).value())
.containsExactly("DELETE FROM fl_price_book_channel WHERE price_book_version_id = #{versionId}");
}
}
@@ -17,6 +17,10 @@ class PriceBookSqlContractTest {
.getContentAsString(StandardCharsets.UTF_8);
String migration = new ClassPathResource("db/phase2-fba-pricing-migration.sql")
.getContentAsString(StandardCharsets.UTF_8);
String channelTable = schema.substring(
schema.indexOf("CREATE TABLE IF NOT EXISTS fl_price_book_channel"),
schema.indexOf("ENGINE=InnoDB COMMENT='价格版本渠道及船期快照'")
);
assertThat(schema)
.contains("CREATE TABLE IF NOT EXISTS fl_price_channel_catalog")
.contains("CREATE TABLE IF NOT EXISTS fl_price_origin_alias")
@@ -32,6 +36,11 @@ class PriceBookSqlContractTest {
.contains("destination_name VARCHAR(128)")
.contains("parse_issues_json JSON NULL")
.contains("DEFAULT 'PENDING_CONFIRMATION'");
assertThat(channelTable)
.contains("created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID'")
.contains("updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID'")
.contains("deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记'")
.contains("version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'");
assertThat(migration)
.doesNotContain("fl_price_book_version")
.doesNotContain("'pricing_tax_mode'")
@@ -50,6 +50,7 @@ class CartonMeasurementServiceTest {
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(repository.lockInbound(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)));
@@ -130,7 +131,7 @@ class CartonMeasurementServiceTest {
}
@Test
void confirmedPackageCountCanBeCorrectedAfterSubmissionAndSyncsInboundSnapshot() {
void confirmedPackageCountCanBeCorrectedAfterSubmissionWithoutChangingOriginalPackageCount() {
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
InboundMeasurementEntity measurement = measurement(30001L, "SUBMITTED", 2);
measurement.setTotalPackageCount(1);
@@ -0,0 +1,89 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.PrintPdfClient;
import com.baiyue.firstleg.dto.InboundCartonLabels;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class InboundCartonLabelPrintServiceTest {
private final InboundCartonLabelService labels = mock(InboundCartonLabelService.class);
private final PrintPdfClient print = mock(PrintPdfClient.class);
private InboundCartonLabelPrintService service;
@BeforeEach
void setUp() {
service = new InboundCartonLabelPrintService(labels, print, new ObjectMapper());
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
}
@Test
void forwardsStableLabelsAndOnlySafePdfHeaders() {
when(labels.list(10001L, 70001L, List.of(1))).thenReturn(batch());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", "labels.pdf");
headers.set("X-Print-Page-Count", "1");
headers.set("X-Internal-Service-Token", "must-not-leak");
when(print.generateInboundMarks(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any()))
.thenReturn(new ResponseEntity<>(new byte[] {1, 2, 3}, headers,
org.springframework.http.HttpStatus.OK));
ResponseEntity<byte[]> response = service.generate(10001L, 70001L, List.of(1), "request-1");
ArgumentCaptor<PrintPdfClient.InboundMarkPdfRequest> request =
ArgumentCaptor.forClass(PrintPdfClient.InboundMarkPdfRequest.class);
verify(print).generateInboundMarks(org.mockito.ArgumentMatchers.eq("internal-token"),
org.mockito.ArgumentMatchers.eq("request-1"), request.capture());
assertThat(request.getValue().downloadFileName()).isEqualTo("PCHK2608040174-19件-箱唛.pdf");
assertThat(request.getValue().labels()).singleElement().satisfies(label -> {
assertThat(label.qrContent()).isEqualTo("PCHK2608040174#19-1");
assertThat(label.cartonText()).isEqualTo("CTNS:19-1");
});
assertThat(response.getBody()).containsExactly(1, 2, 3);
assertThat(response.getHeaders().getFirst("X-Print-Page-Count")).isEqualTo("1");
assertThat(response.getHeaders()).doesNotContainKey("X-Internal-Service-Token");
}
@Test
void convertsPrintJsonBusinessFailureInsteadOfReturningItAsPdf() throws Exception {
when(labels.list(10001L, 70001L, null)).thenReturn(batch());
byte[] error = "{\"code\":\"A0422\",\"message\":\"标签字段超出模板限制\","
.concat("\"data\":null,\"requestId\":\"req-2\",\"traceId\":\"trace-2\"}")
.getBytes(java.nio.charset.StandardCharsets.UTF_8);
when(print.generateInboundMarks(org.mockito.ArgumentMatchers.anyString(),
org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
.thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(error));
assertThatThrownBy(() -> service.generate(10001L, 70001L, null, null))
.isInstanceOf(BizException.class)
.hasMessageContaining("标签字段超出模板限制")
.extracting(value -> ((BizException) value).getResultCode())
.isEqualTo(ResultCode.VALIDATION_FAILED);
}
private InboundCartonLabels.Batch batch() {
InboundCartonLabels.Label label = new InboundCartonLabels.Label(
1L, 2L, 1, "音箱", 19, "PCHK2608040174#19-1", 19, 1,
"50x40x30cm", new BigDecimal("50"), new BigDecimal("40"), new BigDecimal("30"),
"PCHK2608040174", "MCC1", "CTNS:19-1", "洛杉矶普船海卡");
return new InboundCartonLabels.Batch(70001L, 80001L, "PCHK2608040174", 19, List.of(label));
}
}
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.config.InboundProperties;
import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.repository.InboundWorkflowRepository;
@@ -79,6 +80,23 @@ class MeasurementReviewServiceTest {
assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_SPLIT");
}
@Test
@DisplayName("确认箱数合计不等于订单总箱数时不能审核通过")
void shouldRejectApprovalWhenConfirmedPackageCountDoesNotMatchOrder() {
Fixture fixture = fixture(false);
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setPackageCount(4);
when(fixture.repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
assertThatThrownBy(() -> fixture.service.approve(10001L, 9L, "审核员", 10L, "approve-mismatch",
new MeasurementReviewRequest(20L, 3, null)))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.VALIDATION_FAILED);
verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean());
}
@Test
@DisplayName("驳回原因必填且状态推进为测量驳回")
void shouldRequireRejectReason() {
@@ -175,10 +193,15 @@ class MeasurementReviewServiceTest {
InboundOrderEntity order = new InboundOrderEntity();
order.setProcessStatus("MEASURE_PENDING");
order.setMeasureReviewStatus("PENDING");
order.setTotalPackageCount(5);
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(20L);
measurement.setStatus("SUBMITTED");
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setPackageCount(5);
when(repository.findOwned(10001L, 10L)).thenReturn(order);
when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement);
when(repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item));
return new Fixture(service, repository, idempotency, order, measurement);
}