This commit is contained in:
2026-09-18 09:59:07 +08:00
parent 13a8dde0a5
commit 4f59f7bd28
45 changed files with 358 additions and 157 deletions
@@ -106,7 +106,7 @@ class ContainerControllerContractTest {
.contains("未满配 requirementId")
.contains("containerNo 可空")
.contains("提柜后由拖车链调用柜次服务原子补录真实柜号")
.contains("校验 ISO 6346 结构、设备类别、校验位和全局唯一性")
.contains("校验 ISO 6346 结构、设备类别和校验位,并保证同一订舱内唯一")
.doesNotContain("/api/platform/v1/containers/{id}/transport:")
.doesNotContain("ContainerOceanBookingUnlinkRequest")
.doesNotContain("ContainerTransportUpdateRequest")
@@ -52,6 +52,7 @@ class OceanBookingMapperContractTest {
com.baomidou.mybatisplus.extension.plugins.pagination.Page.class, LinkCandidateQuery.class)
.getAnnotation(Select.class).value());
assertThat(sql)
.contains("booking.bill_of_lading_no")
.contains("booking.status='RELEASED'")
.contains("booking.warehouse_id=#{query.warehouseId}")
.contains("booking.channel_code=#{query.channelGroupCode}")
@@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
@@ -23,6 +25,8 @@ class OceanBookingSqlContractTest {
.contains("customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照'")
.contains("MODIFY COLUMN customer_id BIGINT UNSIGNED NULL COMMENT '可选客户ID快照'")
.contains("MODIFY COLUMN customer_name VARCHAR(128) NULL COMMENT '可选客户名称快照'")
.contains("UNIQUE KEY uk_ocean_booking_bill_of_lading (bill_of_lading_no)")
.contains("ADD UNIQUE KEY uk_ocean_booking_bill_of_lading (bill_of_lading_no)")
.doesNotContain("service_term")
.doesNotContain("freight_term")
.contains("carrier_code VARCHAR(16) NULL COMMENT '船司标准编码")
@@ -33,6 +37,14 @@ class OceanBookingSqlContractTest {
.doesNotContain("ALTER TABLE fl_container_booking");
}
@Test
void billOfLadingNumberIsUniqueInModuleAndUnifiedInitialization() throws Exception {
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
for (String source : new String[] {sql(), unified}) {
assertThat(source).contains("UNIQUE KEY uk_ocean_booking_bill_of_lading (bill_of_lading_no)");
}
}
@Test
void enforcesStateIdempotencyAndAuditContracts() throws Exception {
assertThat(sql())
@@ -326,7 +326,7 @@ class OceanBookingServiceTest {
@Test
void linkCandidatesDefaultsPaginationAndDelegatesOneNormalizedSetQuery() {
LinkCandidate candidate = new LinkCandidate(101L, "OB-260818-0001", 201L, "40HQ",
LinkCandidate candidate = new LinkCandidate(101L, "OB-260818-0001", "BL-001", 201L, "40HQ",
new BigDecimal("68"), new BigDecimal("19500"), 3, 1, 2,
"客户A", "深圳中转仓", "美森正班");
when(repository.linkCandidates(any())).thenReturn(new PageResult<>(List.of(candidate), 1, 20, 1, 1));
@@ -347,6 +347,18 @@ class OceanBookingServiceTest {
verify(repository, never()).containers(anyLong());
}
@Test
void rejectsDuplicateBillOfLadingNumberBeforeCreatingDraft() {
stubMasterData();
when(repository.billOfLadingNoExists("BL-001", null)).thenReturn(true);
assertThatThrownBy(() -> service.create(9L, "admin", "duplicate-bill",
new CreateRequest(0, completeDraft())))
.isInstanceOf(BizException.class).hasMessageContaining("提单号已存在");
verify(repository, never()).insert(any(OceanBookingEntity.class));
}
@Test
void linkCandidatesReturnsNormalEmptyPageWithOneSetQueryAndNoDiagnosticFields() {
when(repository.linkCandidates(any())).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
@@ -106,16 +106,18 @@ class ContainerBookingMapperContractTest {
}
@Test
@DisplayName("初始化与统一增量锁定真实柜号权威、唯一和ISO结构约束")
@DisplayName("初始化与统一增量锁定真实柜号权威、订舱内唯一和ISO结构约束")
void databaseContractsFreezeAuthoritativeContainerNumber() throws Exception {
String module = Files.readString(Path.of("src/main/resources/db/phase3-smart-loading-schema.sql"));
String unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
assertThat(module.toLowerCase())
.contains("国内唯一权威真实集装箱号")
.contains("国内柜次真实集装箱号权威快照")
.contains("同一订舱内唯一,不同提单允许复用")
.contains("提柜后可原子补录");
for (String source : new String[] {module, unified}) {
assertThat(source.toLowerCase())
.contains("unique key uk_container_booking_container_no (container_no)")
.contains("unique key uk_container_booking_ocean_container (ocean_booking_id,container_no)")
.contains("key idx_container_booking_container_no (container_no,updated_at,id)")
.contains("constraint chk_container_booking_iso6346")
.contains("^[a-z]{3}[ujz][0-9]{7}$");
}
@@ -68,7 +68,8 @@ class SmartLoadingSqlContractTest {
.contains("source_type varchar(24) NOT NULL DEFAULT 'SIMULATION'")
.contains("record_status varchar(16) NOT NULL DEFAULT 'ACTIVE'")
.contains("merged_into_container_booking_id bigint unsigned DEFAULT NULL")
.contains("UNIQUE KEY uk_container_booking_container_no (container_no)")
.contains("UNIQUE KEY uk_container_booking_ocean_container (ocean_booking_id,container_no)")
.contains("KEY idx_container_booking_container_no (container_no,updated_at,id)")
.contains("KEY idx_container_booking_merge (record_status,merged_into_container_booking_id)")
.contains("CREATE TRIGGER trg_loading_plan_before_update")
.contains("装柜状态只允许单向推进")
@@ -246,7 +246,7 @@ class ContainerServiceTest {
assertThat(result.getContainerNo()).isNull();
assertThat(result.getCabinetNo()).isEqualTo("柜-001");
verify(repository, never()).containerNoExists(anyString(), anyLong());
verify(repository, never()).containerNoExists(anyLong(), anyString(), anyLong());
verify(repository).linkReleasedBooking(eq(11L), eq(21L), eq(31L), isNull(),
any(OceanBookingEntity.class), eq(2), eq(9L), eq("admin"));
verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class));
@@ -303,12 +303,12 @@ class ContainerServiceTest {
}
@Test
@DisplayName("提柜补录真实柜号仍拒绝全局重复号")
@DisplayName("提柜补录真实柜号拒绝同一订舱内重复号")
void rejectsDuplicateRealContainerNumberAfterPickup() {
ContainerBookingEntity linked = booking("EDITING", 3);
linked.setContainerNo(null); linked.setOceanBookingId(21L); linked.setOceanBookingRequirementId(31L);
when(repository.find(11L)).thenReturn(linked);
when(repository.containerNoExists("MSCU6639870", 11L)).thenReturn(true);
when(repository.containerNoExists(21L, "MSCU6639870", 11L)).thenReturn(true);
assertThatThrownBy(() -> service.assignRealContainerNo(
11L, "MSCU6639870", 3, 9L, "admin"))
@@ -331,10 +331,10 @@ class ContainerServiceTest {
}
@Test
@DisplayName("首次关联规范化后按全局唯一真实柜号拒绝冲突")
@DisplayName("首次关联规范化后按同一订舱唯一真实柜号拒绝冲突")
void rejectsNormalizedContainerNumberConflict() {
stubLinkContext();
when(repository.containerNoExists("MSCU6639870", 11L)).thenReturn(true);
when(repository.containerNoExists(21L, "MSCU6639870", 11L)).thenReturn(true);
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
new OceanBookingLinkRequest(21L, 31L, "ms cu6639870", 2)))
@@ -276,19 +276,19 @@ class LoadingListServiceTest {
}
@Test
void rejectsGlobalUniqueConflictIncludingConcurrentConstraintRace() {
void rejectsSameBookingContainerConflictIncludingConcurrentConstraintRace() {
AggregateRow before = row("EDITING", 3);
before.setContainerNo("MSCU6639870");
when(repository.aggregate(10L)).thenReturn(before);
when(containerRepository.containerNoExists("CSQU3054383", 30L)).thenReturn(true);
when(containerRepository.containerNoExists(90L, "CSQU3054383", 30L)).thenReturn(true);
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "unique-key",
new TransportUpdateRequest("CSQU3054383", null, 3, "唯一冲突")))
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
when(containerRepository.containerNoExists("CSQU3054383", 30L)).thenReturn(false);
when(containerRepository.containerNoExists(90L, "CSQU3054383", 30L)).thenReturn(false);
when(repository.updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator"))
.thenThrow(new org.springframework.dao.DuplicateKeyException("uk_container_booking_container_no"));
.thenThrow(new org.springframework.dao.DuplicateKeyException("uk_container_booking_ocean_container"));
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "race-key",
new TransportUpdateRequest("CSQU3054383", null, 3, "并发唯一冲突")))
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
@@ -961,6 +961,7 @@ class LoadingListServiceTest {
row.setPlanStatus(status);
row.setWarehouseId(100L);
row.setChannelCode("OA_LA_TRUCK");
row.setOceanBookingId(90L);
row.setOutboundConfirmedAt(java.time.LocalDateTime.now());
row.setVersion(version);
return row;