diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java index f5e905ab8..62599bc9f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java @@ -171,8 +171,6 @@ public interface ContainerBookingMapper extends BaseMapper#{customerId}") int hasMismatchedCustomerItems(@Param("id") long id, @Param("customerId") long customerId); - @Update(""" UPDATE fl_container_booking SET container_no=#{containerNo}, voyage_no=#{voyageNo}, updated_by=#{operatorId}, updated_by_name=#{operatorName}, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java index b74f650d3..7481453cb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java @@ -12,7 +12,6 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; -import jakarta.validation.constraints.DecimalMin; import java.time.LocalDateTime; import java.math.BigDecimal; @@ -30,8 +29,6 @@ public final class OceanBookingDtos { public record LinkCandidateQuery(@NotNull @Positive Long warehouseId, @NotBlank @Size(max = 64) String channelCode, @NotBlank @Size(max = 32) String containerType, - @NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm, - @NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg, @Min(1) Long page, @Min(1) @Max(200) Long size) { public LinkCandidateQuery { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingContainerAllocationMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingContainerAllocationMapper.java index cca20bf30..77b31e191 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingContainerAllocationMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingContainerAllocationMapper.java @@ -3,8 +3,21 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; @Mapper public interface OceanBookingContainerAllocationMapper extends BaseMapper { @Update("UPDATE fl_ocean_booking_container_allocation SET container_booking_id=#{targetId} WHERE container_booking_id=#{sourceId}") int reassign(@Param("sourceId") long sourceId, @Param("targetId") long targetId); + + @Select(""" + SELECT COUNT(*) FROM fl_ocean_booking_container_allocation allocation + JOIN fl_container_booking container ON container.id=allocation.container_booking_id + AND container.record_status='ACTIVE' + AND container.ocean_booking_id=allocation.ocean_booking_id + AND container.ocean_booking_requirement_id=allocation.requirement_id + WHERE allocation.ocean_booking_id=#{oceanBookingId} + AND allocation.requirement_id=#{requirementId} + """) + long countActiveLinked(@Param("oceanBookingId") long oceanBookingId, + @Param("requirementId") long requirementId); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java index 519d10053..598aad9ef 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java @@ -26,9 +26,9 @@ public interface OceanBookingMapper extends BaseMapper { WHERE party.ocean_booking_id=booking.id AND party.party_type='SHIPPER' LIMIT 1) shipper_name, (SELECT party.party_name FROM fl_ocean_booking_party party WHERE party.ocean_booking_id=booking.id AND party.party_type='CONSIGNEE' LIMIT 1) consignee_name, - (SELECT COUNT(*) FROM fl_ocean_booking_container_allocation allocation - JOIN fl_container_booking container ON container.id=allocation.container_booking_id - WHERE allocation.ocean_booking_id=booking.id AND container.record_status='ACTIVE') container_count + COALESCE((SELECT SUM(requirement.quantity) + FROM fl_ocean_booking_container_requirement requirement + WHERE requirement.ocean_booking_id=booking.id),0) container_count FROM fl_ocean_booking booking WHERE (#{query.customerId} IS NULL OR booking.customer_id=#{query.customerId}) AND booking.status IN ('DRAFT','RELEASED') @@ -54,17 +54,19 @@ public interface OceanBookingMapper extends BaseMapper { JOIN fl_ocean_booking_container_requirement requirement ON requirement.ocean_booking_id=booking.id LEFT JOIN ( - SELECT ocean_booking_id,requirement_id,COUNT(*) allocated_quantity - FROM fl_ocean_booking_container_allocation - GROUP BY ocean_booking_id,requirement_id + SELECT allocation.ocean_booking_id,allocation.requirement_id,COUNT(*) allocated_quantity + FROM fl_ocean_booking_container_allocation allocation + JOIN fl_container_booking container ON container.id=allocation.container_booking_id + AND container.record_status='ACTIVE' + AND container.ocean_booking_id=allocation.ocean_booking_id + AND container.ocean_booking_requirement_id=allocation.requirement_id + GROUP BY allocation.ocean_booking_id,allocation.requirement_id ) allocated ON allocated.ocean_booking_id=booking.id AND allocated.requirement_id=requirement.id WHERE booking.status='RELEASED' AND booking.warehouse_id=#{query.warehouseId} AND booking.channel_code=#{query.channelCode} AND requirement.container_type=#{query.containerType} - AND requirement.capacity_cbm=#{query.capacityCbm} - AND requirement.max_weight_kg=#{query.maxWeightKg} AND requirement.quantity>COALESCE(allocated.allocated_quantity,0) ORDER BY booking.updated_at DESC,booking.id DESC,requirement.sort_no ASC,requirement.id ASC """) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingRepository.java index 747c91ebc..ca7f77216 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingRepository.java @@ -49,6 +49,9 @@ public class OceanBookingRepository { public List parties(long id) { return parties.selectList(new LambdaQueryWrapper().eq(OceanBookingPartyEntity::getOceanBookingId, id).orderByAsc(OceanBookingPartyEntity::getId)); } public List requirements(long id) { return requirements.selectList(new LambdaQueryWrapper().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerRequirementEntity::getSortNo)); } public List allocations(long id) { return allocations.selectList(new LambdaQueryWrapper().eq(OceanBookingContainerAllocationEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerAllocationEntity::getAllocatedAt)); } + public long activeLinkedCount(long oceanBookingId, long requirementId) { + return allocations.countActiveLinked(oceanBookingId, requirementId); + } public List containers(long id) { Map requirementById = requirements(id).stream() .collect(Collectors.toMap(OceanBookingContainerRequirementEntity::getId, Function.identity())); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingService.java b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingService.java index 2d68dfedb..a8b265082 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingService.java @@ -57,8 +57,7 @@ public class OceanBookingService { public PageResult linkCandidates(LinkCandidateQuery query) { return repository.linkCandidates(new LinkCandidateQuery(query.warehouseId(), - query.channelCode().trim(), query.containerType().trim(), query.capacityCbm(), - query.maxWeightKg(), query.page(), query.size())); + query.channelCode().trim(), query.containerType().trim(), query.page(), query.size())); } public List channels(long originWarehouseId) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java index 60fbe9d8c..9a425a5e9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java @@ -46,7 +46,6 @@ public class ContainerRepository { public boolean hasPickTask(long id) { return mapper.hasPickTask(id) > 0; } public boolean hasEvidence(long id) { return mapper.hasEvidence(id) > 0; } public boolean hasDocuments(long id) { return mapper.hasDocuments(id) > 0; } - public boolean hasMismatchedCustomerItems(long id, long customerId) { return mapper.hasMismatchedCustomerItems(id, customerId) > 0; } public int updateTransport(long id, String containerNo, String voyageNo, int version, long operatorId, String operatorName) { return mapper.updateTransport(id, containerNo, voyageNo, version, operatorId, operatorName); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java index 7b52c4c8e..8a9d5571e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java @@ -85,17 +85,14 @@ public class ContainerService { if (booking == null || !"RELEASED".equals(booking.getStatus())) throw conflict("只能关联已发布订舱"); OceanBookingContainerRequirementEntity requirement = oceanBookings.requirement(request.oceanBookingId(), request.requirementId()); if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "订舱箱型需求不存在"); - long allocated = oceanBookings.allocations(request.oceanBookingId()).stream().filter(a -> request.requirementId().equals(a.getRequirementId())).count(); + long allocated = oceanBookings.activeLinkedCount(request.oceanBookingId(), request.requirementId()); if (allocated >= requirement.getQuantity()) throw conflict("该订舱箱型需求已达到数量上限"); if (repository.containerNoExists(request.containerNo().trim(), id)) throw conflict("集装箱号已存在"); if (!java.util.Objects.equals(container.getWarehouseId(), booking.getWarehouseId()) || !java.util.Objects.equals(container.getChannelCode(), booking.getChannelCode()) - || !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType()) - || container.getCapacityCbm() == null || requirement.getCapacityCbm() == null - || container.getMaxWeightKg() == null || requirement.getMaxWeightKg() == null - || container.getCapacityCbm().compareTo(requirement.getCapacityCbm()) != 0 - || container.getMaxWeightKg().compareTo(requirement.getMaxWeightKg()) != 0) throw conflict("模拟柜与真实订舱柜仓库、渠道、箱型或容量不一致"); - if (booking.getCustomerId() != null && repository.hasMismatchedCustomerItems(id, booking.getCustomerId())) throw conflict("模拟柜已排货客户与真实订舱客户范围不一致"); + || !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType())) { + throw conflict("模拟柜与真实订舱柜仓库、渠道或柜型不一致"); + } if (repository.linkReleasedBooking(id, request.oceanBookingId(), request.requirementId(), request.containerNo().trim(), booking, request.expectedVersion(), operatorId, operatorName) != 1) { throw conflict("真实订舱柜已被消费或货柜版本发生变化,请刷新后重试"); diff --git a/firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml b/firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml index 5900284ee..8b7097c85 100644 --- a/firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml +++ b/firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml @@ -2,7 +2,12 @@ openapi: 3.0.3 info: {title: Ocean Booking API, version: 2.0.0} paths: /api/platform/v1/ocean-bookings: - get: {summary: List DRAFT and RELEASED bookings, responses: {'200': {description: Booking page}}} + get: + summary: List DRAFT and RELEASED bookings + description: containerCount is the sum of quantity across all container requirements for the booking. It is zero when no requirement exists and never represents linked or ACTIVE containers. + responses: + '200': + description: Booking page; each row uses the frozen requirement-total containerCount semantic post: summary: Create an editable DRAFT description: Creates only the booking aggregate and container requirements; no containerNo, container row, or allocation is created. @@ -31,14 +36,12 @@ paths: /api/platform/v1/ocean-bookings/link-candidates: get: summary: Page released booking requirement candidates for one container - description: Requires platform:firstleg:ocean-booking:list. Returns only exact warehouse, channel and fixed container specification matches with remainingQuantity greater than zero. Allocation counts are calculated by one bounded set query; booking detail calls are not used. + description: Requires platform:firstleg:ocean-booking:list. Eligibility matches only warehouseId, channelCode and containerType, with remainingQuantity greater than zero. Capacity and max weight are derived display snapshots and never eligibility fields. Allocated quantity counts only ACTIVE containers still pointing to the same booking requirement and is calculated by one bounded set query; booking detail calls are not used. x-permission: platform:firstleg:ocean-booking:list parameters: - {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}} - {name: channelCode, in: query, required: true, schema: {type: string, maxLength: 64}} - {name: containerType, in: query, required: true, schema: {type: string, maxLength: 32}} - - {name: capacityCbm, in: query, required: true, schema: {type: number, exclusiveMinimum: true, minimum: 0}} - - {name: maxWeightKg, in: query, required: true, schema: {type: number, exclusiveMinimum: true, minimum: 0}} - {name: page, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, default: 1}} - {name: size, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}} responses: @@ -145,6 +148,14 @@ components: code: {type: string, example: '200'} message: {type: string, nullable: true} data: {$ref: '#/components/schemas/LinkCandidatePage'} + OceanBookingListRow: + type: object + properties: + containerCount: + type: integer + format: int64 + minimum: 0 + description: Sum of all container requirement quantities; independent of allocation, linking or unlinking. Detail: type: object required: [oceanBooking, parties, containerRequirements, containers, histories] diff --git a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingControllerContractTest.java index 6bdfc820d..a7200cd27 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingControllerContractTest.java @@ -68,11 +68,21 @@ class PlatformOceanBookingControllerContractTest { assertThat(yaml) .contains("/api/platform/v1/ocean-bookings/link-candidates:") .contains("x-permission: platform:firstleg:ocean-booking:list") - .contains("warehouseId", "channelCode", "containerType", "capacityCbm", "maxWeightKg") + .contains("warehouseId", "channelCode", "containerType") .contains("LinkCandidateResponse", "requiredQuantity", "allocatedQuantity", "remainingQuantity") + .contains("Capacity and max weight are derived display snapshots and never eligibility fields") .contains("booking detail calls are not used"); } + @Test + void openApiDefinesContainerCountAsRequirementQuantityTotal() throws Exception { + String yaml = Files.readString(Path.of("src/main/resources/openapi/ocean-bookings-v1.yaml")); + assertThat(yaml) + .contains("containerCount is the sum of quantity across all container requirements") + .contains("never represents linked or ACTIVE containers") + .contains("Sum of all container requirement quantities; independent of allocation, linking or unlinking."); + } + private static Method method(String name) { return Arrays.stream(PlatformOceanBookingController.class.getDeclaredMethods()) .filter(method -> method.getName().equals(name)).findFirst().orElseThrow(); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingMapperContractTest.java index 835f826b8..c4dcec3dd 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/repository/OceanBookingMapperContractTest.java @@ -1,6 +1,7 @@ package com.baiyue.firstleg.oceanbooking.repository; import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingMapper; +import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingContainerAllocationMapper; import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.LinkCandidateQuery; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; @@ -21,7 +22,10 @@ class OceanBookingMapperContractTest { .contains("booking.status=#{query.status}") .contains("booking.booking_no LIKE") .contains("party.party_type='SHIPPER'", "party.party_type='CONSIGNEE'") - .contains("fl_ocean_booking_container_allocation", "container.record_status='ACTIVE'", "container_count") + .contains("COALESCE((SELECT SUM(requirement.quantity)") + .contains("FROM fl_ocean_booking_container_requirement requirement") + .contains("requirement.ocean_booking_id=booking.id),0) container_count") + .doesNotContain("fl_ocean_booking_container_allocation allocation\n JOIN fl_container_booking") .contains("ORDER BY booking.updated_at DESC,booking.id DESC"); } @@ -48,12 +52,28 @@ class OceanBookingMapperContractTest { .contains("booking.warehouse_id=#{query.warehouseId}") .contains("booking.channel_code=#{query.channelCode}") .contains("requirement.container_type=#{query.containerType}") - .contains("requirement.capacity_cbm=#{query.capacityCbm}") - .contains("requirement.max_weight_kg=#{query.maxWeightKg}") .contains("FROM fl_ocean_booking_container_allocation") - .contains("GROUP BY ocean_booking_id,requirement_id") + .contains("container.record_status='ACTIVE'") + .contains("container.ocean_booking_id=allocation.ocean_booking_id") + .contains("container.ocean_booking_requirement_id=allocation.requirement_id") + .contains("GROUP BY allocation.ocean_booking_id,allocation.requirement_id") .contains("requirement.quantity-COALESCE(allocated.allocated_quantity,0) remaining_quantity") .contains("requirement.quantity>COALESCE(allocated.allocated_quantity,0)") - .doesNotContain("fl_container_booking", "SELECT booking.*"); + .doesNotContain("fl_container_booking", "SELECT booking.*", + "requirement.capacity_cbm=#{query.capacityCbm}", + "requirement.max_weight_kg=#{query.maxWeightKg}"); + } + + @Test + void finalLinkLimitCountsOnlyActiveContainersStillPointingAtTheSameRequirement() throws Exception { + String sql = String.join("\n", OceanBookingContainerAllocationMapper.class + .getDeclaredMethod("countActiveLinked", long.class, long.class) + .getAnnotation(Select.class).value()); + assertThat(sql) + .contains("container.record_status='ACTIVE'") + .contains("container.ocean_booking_id=allocation.ocean_booking_id") + .contains("container.ocean_booking_requirement_id=allocation.requirement_id") + .contains("allocation.ocean_booking_id=#{oceanBookingId}") + .contains("allocation.requirement_id=#{requirementId}"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java index 2bea449df..3af6f71b7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingServiceTest.java @@ -171,6 +171,22 @@ class OceanBookingServiceTest { verify(repository, times(2)).page(any()); } + @Test + void pagePreservesRequirementTotalContainerCountsForSingleMultipleAndEmptyDrafts() { + OceanBookingEntity one45Hc = booking("RELEASED", 1); one45Hc.setContainerCount(1L); + OceanBookingEntity multipleTypes = booking("RELEASED", 1); multipleTypes.setId(2L); + multipleTypes.setContainerCount(4L); + OceanBookingEntity emptyDraft = booking("DRAFT", 0); emptyDraft.setId(3L); + emptyDraft.setContainerCount(0L); + when(repository.page(any())).thenReturn(new PageResult<>( + List.of(one45Hc, multipleTypes, emptyDraft), 1, 20, 3, 1)); + + PageResult result = service.page(new Query(null, null, null, 1, 20)); + + assertThat(result.records()).extracting(OceanBookingEntity::getContainerCount) + .containsExactly(1L, 4L, 0L); + } + @Test void linkCandidatesDefaultsPaginationAndDelegatesOneNormalizedSetQuery() { LinkCandidate candidate = new LinkCandidate(101L, "OB-260818-0001", 201L, "40HC", @@ -179,7 +195,7 @@ class OceanBookingServiceTest { when(repository.linkCandidates(any())).thenReturn(new PageResult<>(List.of(candidate), 1, 20, 1, 1)); PageResult result = service.linkCandidates(new LinkCandidateQuery(701L, - " SEA-US ", " 40HC ", new BigDecimal("76.0"), new BigDecimal("28000.00"), null, null)); + " SEA-US ", " 40HC ", null, null)); assertThat(result.records()).containsExactly(candidate); assertThat(result.records().get(0).remainingQuantity()).isEqualTo(2); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/ContainerBookingMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/ContainerBookingMapperContractTest.java index 68a9ec0f3..16944ca9c 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/ContainerBookingMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/ContainerBookingMapperContractTest.java @@ -53,15 +53,6 @@ class ContainerBookingMapperContractTest { .contains("customer_item.account_id = #{query.customerId}"); } - @Test - @DisplayName("首次关联客户隔离查询使用原生不等号") - void customerMismatchQueryUsesNativeSqlOperator() throws Exception { - String sql = ContainerBookingMapper.class - .getDeclaredMethod("hasMismatchedCustomerItems", long.class, long.class) - .getAnnotation(Select.class).value()[0]; - assertThat(sql).contains("i.account_id<>#{customerId}").doesNotContain("<>"); - } - @Test @DisplayName("关联订舱 SQL 的预计到港参数与 Mapper 参数名一致") void linkReleasedBookingUsesDeclaredEstimatedArrivalParameter() { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java index 8c6f0a5b3..4dba3b293 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java @@ -288,7 +288,7 @@ class ContainerServiceTest { void linksReleasedBookingAfterPreloading() { ContainerBookingEntity container = booking("EDITING", 2); container.setWarehouseId(101L); container.setChannelCode("SEA-US"); container.setContainerType("40HC"); - container.setCapacityCbm(new BigDecimal("76.0000")); container.setMaxWeightKg(new BigDecimal("28000.000")); + 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("SEA-US"); OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity(); @@ -296,7 +296,7 @@ class ContainerServiceTest { when(repository.find(11L)).thenReturn(container, container); when(oceanBookings.find(21L)).thenReturn(booking); when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement); - when(oceanBookings.allocations(21L)).thenReturn(List.of()); + when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L); when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU7654321"), same(booking), eq(2), eq(9L), eq("admin"))).thenReturn(1); @@ -311,6 +311,24 @@ class ContainerServiceTest { verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class)); } + @Test + @DisplayName("首次关联拒绝装柜仓库不一致") + void rejectsLinkWhenWarehouseDiffers() { + assertThreeFieldMismatch(102L, "SEA-US", "40HC"); + } + + @Test + @DisplayName("首次关联拒绝物流渠道不一致") + void rejectsLinkWhenChannelDiffers() { + assertThreeFieldMismatch(101L, "SEA-EU", "40HC"); + } + + @Test + @DisplayName("首次关联拒绝柜型不一致") + void rejectsLinkWhenContainerTypeDiffers() { + assertThreeFieldMismatch(101L, "SEA-US", "45HC"); + } + @Test @DisplayName("未发布订舱不可关联") void rejectsDraftBooking() { @@ -333,12 +351,10 @@ class ContainerServiceTest { booking.setId(21L); booking.setStatus("RELEASED"); OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity(); requirement.setId(31L); requirement.setQuantity(1); - OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity(); - allocation.setRequirementId(31L); when(repository.find(11L)).thenReturn(container); when(oceanBookings.find(21L)).thenReturn(booking); when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement); - when(oceanBookings.allocations(21L)).thenReturn(List.of(allocation)); + when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(1L); assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin", new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2))) @@ -347,6 +363,28 @@ class ContainerServiceTest { any(), anyInt(), anyLong(), anyString()); } + private void assertThreeFieldMismatch(long bookingWarehouseId, String bookingChannel, + String requirementType) { + ContainerBookingEntity container = booking("WAIT_LOADING", 2); + container.setWarehouseId(101L); container.setChannelCode("SEA-US"); + container.setContainerType("40HC"); + OceanBookingEntity oceanBooking = new OceanBookingEntity(); + oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED"); + oceanBooking.setWarehouseId(bookingWarehouseId); oceanBooking.setChannelCode(bookingChannel); + OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity(); + requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType(requirementType); + when(repository.find(11L)).thenReturn(container); + when(oceanBookings.find(21L)).thenReturn(oceanBooking); + when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement); + when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L); + + assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin", + new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2))) + .isInstanceOf(BizException.class).hasMessageContaining("仓库、渠道或柜型不一致"); + verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(), + any(), anyInt(), anyLong(), anyString()); + } + @Test @DisplayName("完全空的模拟柜直接解绑并删除当前allocation,不依赖MERGED来源柜") void unlinksEmptySimulationDirectly() {