This commit is contained in:
2026-09-15 10:37:34 +08:00
parent 4f13ee2ec3
commit 88e2fb0eb1
49 changed files with 465 additions and 236 deletions
@@ -84,7 +84,7 @@ class PlatformSmartLoadingControllerContractTest {
.contains("成功响应中的 version 是递增后的 planVersion")
.contains("items[].version 是递增后的 itemVersion")
.contains("数量调整或动态增量不会覆盖已有协作备注")
.contains("柜内总容积和总重量分别允许达到柜型额定值的150%")
.contains("柜内总容积和总重量分别允许达到柜型基准值的150%")
.contains("人工分配不限制订单国内仓及渠道与柜次一致")
.contains("智能推荐允许跨国内仓,但仍只选择柜次渠道")
.contains("canAssign: {type: boolean}")
@@ -1,24 +0,0 @@
package com.baiyue.firstleg.domain;
import com.baiyue.common.core.exception.BizException;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ContainerSpecificationCatalogTest {
@Test
void exposesCanonicalSpecificationsAndRejectsAliases() {
var specification = ContainerSpecificationCatalog.requireMatch(
"40HC", new BigDecimal("76.000"), new BigDecimal("28000.000"));
assertThat(specification.containerType()).isEqualTo("40HC");
assertThatThrownBy(() -> ContainerSpecificationCatalog.require("40HQ"))
.isInstanceOf(BizException.class).hasMessageContaining("固定规格");
assertThatThrownBy(() -> ContainerSpecificationCatalog.requireMatch(
"40HC", new BigDecimal("68"), new BigDecimal("28000")))
.isInstanceOf(BizException.class).hasMessageContaining("容量和限重");
}
}
@@ -23,8 +23,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("service_term VARCHAR(32) NULL COMMENT '运输服务条款快照;发布前必填'")
.contains("freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照;发布前必填'")
.doesNotContain("service_term")
.doesNotContain("freight_term")
.contains("carrier_code VARCHAR(16) NULL COMMENT '船司标准编码")
.contains("carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER')")
.contains("container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '明确引用现有柜次ID'")
@@ -67,8 +67,6 @@ class OceanBookingPrintServiceTest {
Detail detail = detail("DRAFT");
detail.oceanBooking().setBillOfLadingNo(null);
detail.oceanBooking().setPlaceOfDelivery(null);
detail.oceanBooking().setServiceTerm(null);
detail.oceanBooking().setFreightTerm(null);
detail = new Detail(detail.oceanBooking(), List.of(detail.parties().get(0)),
detail.containerRequirements(), List.of(), List.of(), List.of());
when(bookings.detail(10L)).thenReturn(detail);
@@ -102,8 +100,7 @@ class OceanBookingPrintServiceTest {
booking.setId(10L); booking.setBookingNo("OB-1"); booking.setStatus(status);
booking.setBillOfLadingNo("BL-1"); booking.setVesselName("Vessel"); booking.setVoyageNo("V001");
booking.setDeparturePortName("Ningbo"); booking.setDestinationPortName("LA");
booking.setPlaceOfDelivery("LA DC"); booking.setServiceTerm("CY-CY");
booking.setFreightTerm("PREPAID"); booking.setBookingAgentName("Agent");
booking.setPlaceOfDelivery("LA DC"); booking.setBookingAgentName("Agent");
booking.setEstimatedSailingAt(LocalDateTime.of(2026, 9, 1, 0, 0));
OceanBookingPartyEntity shipper = new OceanBookingPartyEntity();
shipper.setPartyType("SHIPPER"); shipper.setPartyName("Shipper"); shipper.setAddress("Address");
@@ -113,7 +110,7 @@ class OceanBookingPrintServiceTest {
notify.setPartyType("NOTIFY_PARTY"); notify.setPartyName("Notify");
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
requirement.setContainerType("40HQ"); requirement.setContainerKind("DRY"); requirement.setQuantity(2);
requirement.setCapacityCbm(new BigDecimal("68")); requirement.setMaxWeightKg(new BigDecimal("28000"));
requirement.setCapacityCbm(new BigDecimal("68")); requirement.setMaxWeightKg(new BigDecimal("19500"));
return new Detail(booking, List.of(shipper, consignee, notify), List.of(requirement), List.of(), List.of(), List.of());
}
}
@@ -160,7 +160,7 @@ class OceanBookingServiceTest {
ArgumentCaptor<List<OceanBookingContainerRequirementEntity>> captor = ArgumentCaptor.forClass(List.class);
verify(repository).replaceRequirements(eq(inserted.get().getId()), captor.capture());
assertThat(captor.getValue()).singleElement().satisfies(value -> {
assertThat(value.getContainerType()).isEqualTo("40HC");
assertThat(value.getContainerType()).isEqualTo("40HQ");
assertThat(value.getQuantity()).isEqualTo(2);
});
verify(repository, never()).insertContainer(any());
@@ -326,13 +326,13 @@ class OceanBookingServiceTest {
@Test
void linkCandidatesDefaultsPaginationAndDelegatesOneNormalizedSetQuery() {
LinkCandidate candidate = new LinkCandidate(101L, "OB-260818-0001", 201L, "40HC",
new BigDecimal("76"), new BigDecimal("28000"), 3, 1, 2,
LinkCandidate candidate = new LinkCandidate(101L, "OB-260818-0001", 201L, "40HQ",
new BigDecimal("68"), new BigDecimal("19500"), 3, 1, 2,
"客户A", "深圳中转仓", "美西海运");
when(repository.linkCandidates(any(), anySet())).thenReturn(new PageResult<>(List.of(candidate), 1, 20, 1, 1));
PageResult<LinkCandidate> result = service.linkCandidates(new LinkCandidateQuery(701L,
" MATSON_CLX ", " 40HC ", null, null));
" MATSON_CLX ", " 40HQ ", null, null));
assertThat(result.records()).containsExactly(candidate);
assertThat(result.records().get(0).remainingQuantity()).isEqualTo(2);
@@ -340,7 +340,7 @@ class OceanBookingServiceTest {
@SuppressWarnings("unchecked") ArgumentCaptor<java.util.Set<String>> channels = ArgumentCaptor.forClass(java.util.Set.class);
verify(repository).linkCandidates(query.capture(), channels.capture());
assertThat(query.getValue().channelGroupCode()).isEqualTo("MATSON_CLX");
assertThat(query.getValue().containerType()).isEqualTo("40HC");
assertThat(query.getValue().containerType()).isEqualTo("40HQ");
assertThat(query.getValue().page()).isEqualTo(1L);
assertThat(query.getValue().size()).isEqualTo(20L);
assertThat(channels.getValue()).contains("MATSON_CLX_TRUCK", "MATSON_CLX_PARCEL", "ZBYHK");
@@ -354,7 +354,7 @@ class OceanBookingServiceTest {
when(repository.linkCandidates(any(), anySet())).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
PageResult<LinkCandidate> result = service.linkCandidates(
new LinkCandidateQuery(701L, "MATSON_CLX", "45HC", null, null));
new LinkCandidateQuery(701L, "MATSON_CLX", "45HQ", null, null));
assertThat(result.records()).isEmpty();
assertThat(result.total()).isZero();
@@ -402,19 +402,19 @@ class OceanBookingServiceTest {
private static DraftData partialDraft(String remark) {
return new DraftData(null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, remark, null, null);
null, null, null, null, null, null, null, null, null, remark, null, null);
}
private static DraftData completeDraft() {
return new DraftData(100L, "客户", "SEA-US", 701L, "BL-001", 71L, "船公司", "MATSON",
81L, null, "VESSEL", "V001", "CNSZX", "深圳", "USLAX", "洛杉矶",
"LAX", "CY-CY", "PREPAID", LocalDateTime.of(2026, 9, 1, 0, 0),
"LAX", LocalDateTime.of(2026, 9, 1, 0, 0),
LocalDateTime.of(2026, 9, 20, 0, 0), null,
List.of(new PartyRequest("SHIPPER", "发货人", null, null, null),
new PartyRequest("CONSIGNEE", "收货人", null, null, null),
new PartyRequest("NOTIFY_PARTY", "通知人", null, null, null)),
List.of(new ContainerRequirementRequest("40HC", "DRY", 2,
new BigDecimal("76"), new BigDecimal("28000"))));
List.of(new ContainerRequirementRequest("40HQ", "DRY", 2,
new BigDecimal("68"), new BigDecimal("19500"))));
}
private static OceanBookingEntity completeBooking(String status, int version) {
@@ -426,7 +426,6 @@ class OceanBookingServiceTest {
value.setDeparturePortCode("CNSZX"); value.setDeparturePortName("深圳");
value.setDestinationPortCode("USLAX"); value.setDestinationPortName("洛杉矶");
value.setPlaceOfDelivery("LAX");
value.setServiceTerm("CY-CY"); value.setFreightTerm("PREPAID");
value.setEstimatedSailingAt(LocalDateTime.of(2026, 9, 1, 0, 0));
value.setEstimatedArrivalAt(LocalDateTime.of(2026, 9, 20, 0, 0));
return value;
@@ -447,9 +446,9 @@ class OceanBookingServiceTest {
private static OceanBookingContainerRequirementEntity requirement() {
var value = new OceanBookingContainerRequirementEntity();
value.setId(11L); value.setContainerType("40HC"); value.setContainerKind("DRY");
value.setQuantity(1); value.setCapacityCbm(new BigDecimal("76"));
value.setMaxWeightKg(new BigDecimal("28000")); return value;
value.setId(11L); value.setContainerType("40HQ"); value.setContainerKind("DRY");
value.setQuantity(1); value.setCapacityCbm(new BigDecimal("68"));
value.setMaxWeightKg(new BigDecimal("19500")); return value;
}
private String hash(String action, Long id, Object request) throws Exception {
@@ -85,7 +85,7 @@ class ContainerServiceTest {
assertThat(saved.getChannelName()).isNull();
assertThat(saved.getWarehouseId()).isEqualTo(101L);
assertThat(saved.getWarehouseName()).isEqualTo("上海中转仓");
assertThat(saved.getCapacityCbm()).isEqualByComparingTo("76.0000");
assertThat(saved.getCapacityCbm()).isEqualByComparingTo("68.0000");
assertThat(saved.getPlanId()).isEqualTo(501L);
assertThat(saved.getPlanNo()).isEqualTo("LP-260818-0001");
assertThat(saved.getPlanStatus()).isEqualTo("EDITING");
@@ -96,11 +96,11 @@ class ContainerServiceTest {
@DisplayName("新增模拟柜拒绝柜型与固定规格不匹配")
void rejectsSimulationWithNonCanonicalSpecification() {
SimulationCreateRequest invalid = new SimulationCreateRequest(
"MATSON_CLX", 101L, "40HC", new BigDecimal("100"), new BigDecimal("28000"));
"MATSON_CLX", 101L, "40HQ", new BigDecimal("100"), new BigDecimal("19500"));
assertThatThrownBy(() -> service.createSimulation(9L, "admin", invalid))
.isInstanceOf(BizException.class)
.hasMessageContaining("容量和限重");
.hasMessageContaining("容量和货重上限");
verify(repository, never()).insert(any());
}
@@ -204,12 +204,12 @@ class ContainerServiceTest {
container.setPlanId(501L);
container.setPlanNo("LP-260818-0001");
container.setContainerNo(null);
container.setWarehouseId(101L); container.setChannelGroupCode("EXX"); container.setChannelCode(null); container.setContainerType("40HC");
container.setWarehouseId(101L); container.setChannelGroupCode("EXX"); container.setChannelCode(null); container.setContainerType("40HQ");
container.setCapacityCbm(new BigDecimal("85.5000")); container.setMaxWeightKg(new BigDecimal("27500.000"));
OceanBookingEntity booking = new OceanBookingEntity();
booking.setId(21L); booking.setStatus("RELEASED"); booking.setWarehouseId(101L); booking.setChannelCode("EXX_RAIL_NY");
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
requirement.setId(31L); requirement.setQuantity(1); requirement.setContainerType("40HC"); requirement.setCapacityCbm(new BigDecimal("76.0000")); requirement.setMaxWeightKg(new BigDecimal("28000.000"));
requirement.setId(31L); requirement.setQuantity(1); requirement.setContainerType("40HQ"); requirement.setCapacityCbm(new BigDecimal("68.0000")); requirement.setMaxWeightKg(new BigDecimal("19500.000"));
when(repository.find(11L)).thenReturn(container, container);
when(oceanBookings.find(21L)).thenReturn(booking);
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
@@ -313,15 +313,15 @@ class ContainerServiceTest {
void linksMultipleRealContainersToTheSameReleasedBookingRequirement() {
ContainerBookingEntity first = booking("WAIT_LOADING", 2);
first.setId(11L); first.setContainerNo(null); first.setWarehouseId(101L);
first.setChannelGroupCode("MATSON_CLX"); first.setChannelCode(null); first.setContainerType("40HC");
first.setChannelGroupCode("MATSON_CLX"); first.setChannelCode(null); first.setContainerType("40HQ");
ContainerBookingEntity second = booking("WAIT_LOADING", 4);
second.setId(12L); second.setContainerNo(null); second.setWarehouseId(101L);
second.setChannelGroupCode("MATSON_CLX"); second.setChannelCode(null); second.setContainerType("40HC");
second.setChannelGroupCode("MATSON_CLX"); second.setChannelCode(null); second.setContainerType("40HQ");
OceanBookingEntity oceanBooking = new OceanBookingEntity();
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
oceanBooking.setWarehouseId(101L); oceanBooking.setChannelCode("MATSON_CLX_TRUCK");
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HC");
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HQ");
when(repository.find(11L)).thenReturn(first);
when(repository.find(12L)).thenReturn(second);
when(oceanBookings.find(21L)).thenReturn(oceanBooking);
@@ -362,19 +362,19 @@ class ContainerServiceTest {
@Test
@DisplayName("首次关联拒绝装柜仓库不一致")
void rejectsLinkWhenWarehouseDiffers() {
assertThreeFieldMismatch(102L, "MATSON_CLX_TRUCK", "40HC");
assertThreeFieldMismatch(102L, "MATSON_CLX_TRUCK", "40HQ");
}
@Test
@DisplayName("首次关联拒绝物流渠道不一致")
void rejectsLinkWhenChannelDiffers() {
assertThreeFieldMismatch(101L, "HEDE_TRUCK", "40HC");
assertThreeFieldMismatch(101L, "HEDE_TRUCK", "40HQ");
}
@Test
@DisplayName("首次关联拒绝柜型不一致")
void rejectsLinkWhenContainerTypeDiffers() {
assertThreeFieldMismatch(101L, "MATSON_CLX_TRUCK", "45HC");
assertThreeFieldMismatch(101L, "MATSON_CLX_TRUCK", "45HQ");
}
@Test
@@ -418,7 +418,7 @@ class ContainerServiceTest {
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
container.setContainerNo(null);
container.setWarehouseId(101L); container.setChannelGroupCode("MATSON_CLX"); container.setChannelCode(null);
container.setContainerType("40HC");
container.setContainerType("40HQ");
OceanBookingEntity oceanBooking = new OceanBookingEntity();
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
oceanBooking.setWarehouseId(bookingWarehouseId); oceanBooking.setChannelCode(bookingChannel);
@@ -440,12 +440,12 @@ class ContainerServiceTest {
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
container.setContainerNo(null);
container.setWarehouseId(101L); container.setChannelGroupCode("MATSON_CLX"); container.setChannelCode(null);
container.setContainerType("40HC");
container.setContainerType("40HQ");
OceanBookingEntity oceanBooking = new OceanBookingEntity();
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
oceanBooking.setWarehouseId(101L); oceanBooking.setChannelCode("MATSON_CLX_TRUCK");
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HC");
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HQ");
when(repository.find(11L)).thenReturn(container);
when(oceanBookings.find(21L)).thenReturn(oceanBooking);
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
@@ -485,15 +485,15 @@ class ContainerServiceTest {
private static SaveRequest request() {
return new SaveRequest(101L, "上海仓", "BOL-001", "柏悦轮",
"V001", "40HC", 201L, "船司A", "CNSHA", "上海港", "USLAX", "洛杉矶港",
"ONT8", "ONT8", new BigDecimal("76.0000"), new BigDecimal("28000.000"), 202L, "拖车公司A",
"V001", "40HQ", 201L, "船司A", "CNSHA", "上海港", "USLAX", "洛杉矶港",
"ONT8", "ONT8", new BigDecimal("68.0000"), new BigDecimal("19500.000"), 202L, "拖车公司A",
LocalDateTime.of(2026, 8, 10, 8, 0), LocalDateTime.of(2026, 8, 12, 8, 0), null,
LocalDateTime.of(2026, 8, 25, 8, 0), null,
LocalDateTime.of(2026, 8, 27, 8, 0), null);
}
private static SimulationCreateRequest simulation() {
return new SimulationCreateRequest("MATSON_CLX", 101L, "40HC", new BigDecimal("76.0000"), new BigDecimal("28000.000"));
return new SimulationCreateRequest("MATSON_CLX", 101L, "40HQ", new BigDecimal("68.0000"), new BigDecimal("19500.000"));
}
private static ChannelRow channel() {
@@ -552,8 +552,8 @@ class SmartLoadingServiceTest {
@Test
void pagesWorkbenchContainersWithTheSameGlobalFilters() {
WorkbenchContainer container = new WorkbenchContainer(99L, "CB-1", "CAB-1", null,
100L, "义乌仓", "MATSON_CLX", null, null, "40HC",
new BigDecimal("68"), new BigDecimal("28000"),
100L, "义乌仓", "MATSON_CLX", null, null, "40HQ",
new BigDecimal("68"), new BigDecimal("19500"),
1L, "LP-1", "EDITING", 2);
WorkbenchContainerQuery query = new WorkbenchContainerQuery(List.of("MATSON_CLX"), 100L,
9L, "IN-10", LocalDate.parse("2026-09-01"), LocalDate.parse("2026-09-30"),