fix(firstleg): allow booking link before container pickup

This commit is contained in:
2026-09-01 15:06:03 +08:00
parent a10e32a012
commit 1e8f504f15
10 changed files with 124 additions and 15 deletions
@@ -85,9 +85,9 @@ public final class ContainerDtos {
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg) {}
@Schema(name = "ContainerOceanBookingLinkRequest", description = "将现有模拟柜原地关联到已发布订舱箱型需求并补录真实柜号")
@Schema(name = "ContainerOceanBookingLinkRequest", description = "将现有模拟柜原地关联到已发布订舱箱型需求;真实柜号可在提柜后补录")
public record OceanBookingLinkRequest(@NotNull @Positive Long oceanBookingId,
@NotNull @Positive Long requirementId, @NotBlank @Size(max = 64) String containerNo,
@NotNull @Positive Long requirementId, @Size(max = 64) String containerNo,
@NotNull @PositiveOrZero Integer expectedVersion) {}
}
@@ -13,6 +13,20 @@ import org.apache.ibatis.annotations.Update;
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
@Update("UPDATE fl_container_booking SET container_no=#{containerNo},ocean_booking_id=#{oceanBookingId},ocean_booking_requirement_id=#{requirementId},record_status='ACTIVE',bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},voyage_no=#{voyageNo},shipping_company_id=#{shippingCompanyId},shipping_company_name=#{shippingCompanyName},departure_port_code=#{departurePortCode},departure_port_name=#{departurePortName},destination_port_code=#{destinationPortCode},destination_port_name=#{destinationPortName},channel_code=#{channelCode},channel_name=#{channelName},customer_id=#{customerId},customer_name=#{customerName},estimated_sailing_at=#{estimatedSailingAt},estimated_port_arrival_at=#{estimatedPortArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=NOW(),version=version+1 WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE' AND ocean_booking_id IS NULL")
int linkReleasedBooking(@Param("id") long id, @Param("oceanBookingId") long oceanBookingId, @Param("requirementId") long requirementId, @Param("containerNo") String containerNo, @Param("billOfLadingNo") String billOfLadingNo, @Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo, @Param("shippingCompanyId") Long shippingCompanyId, @Param("shippingCompanyName") String shippingCompanyName, @Param("departurePortCode") String departurePortCode, @Param("departurePortName") String departurePortName, @Param("destinationPortCode") String destinationPortCode, @Param("destinationPortName") String destinationPortName, @Param("channelCode") String channelCode, @Param("channelName") String channelName, @Param("customerId") Long customerId, @Param("customerName") String customerName, @Param("estimatedSailingAt") java.time.LocalDateTime estimatedSailingAt, @Param("estimatedPortArrivalAt") java.time.LocalDateTime estimatedPortArrivalAt, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_container_booking SET container_no=#{containerNo},updated_by=#{operatorId},
updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE'
AND container_no IS NULL AND ocean_booking_id IS NOT NULL AND ocean_booking_requirement_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM fl_loading_plan p WHERE p.container_booking_id=fl_container_booking.id
AND p.plan_status NOT IN ('EDITING'))
AND NOT EXISTS (SELECT 1 FROM fl_loading_pick_task t WHERE t.container_booking_id=fl_container_booking.id)
AND NOT EXISTS (SELECT 1 FROM fl_loading_cabinet_evidence e WHERE e.container_booking_id=fl_container_booking.id)
AND NOT EXISTS (SELECT 1 FROM fl_container_document d WHERE d.container_booking_id=fl_container_booking.id)
""")
int assignRealContainerNo(@Param("id") long id, @Param("containerNo") String containerNo,
@Param("version") int version, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Select("""
<script>
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
@@ -38,6 +38,10 @@ public class ContainerRepository {
int version, long operatorId, String operatorName) {
return mapper.linkReleasedBooking(id, oceanBookingId, requirementId, containerNo, booking.getBillOfLadingNo(), booking.getVesselName(), booking.getVoyageNo(), booking.getShippingCompanyId(), booking.getShippingCompanyName(), booking.getDeparturePortCode(), booking.getDeparturePortName(), booking.getDestinationPortCode(), booking.getDestinationPortName(), booking.getChannelCode(), booking.getChannelName(), booking.getCustomerId(), booking.getCustomerName(), booking.getEstimatedSailingAt(), booking.getEstimatedArrivalAt(), version, operatorId, operatorName);
}
public int assignRealContainerNo(long id, String containerNo, int version,
long operatorId, String operatorName) {
return mapper.assignRealContainerNo(id, containerNo, version, operatorId, operatorName);
}
public int updateDraft(ContainerBookingEntity value, int version, long operatorId, String operatorName) {
return mapper.updateDraft(value, version, operatorId, operatorName);
}
@@ -76,8 +76,9 @@ public class ContainerService {
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "订舱箱型需求不存在");
long allocated = oceanBookings.activeLinkedCount(request.oceanBookingId(), request.requirementId());
if (allocated >= requirement.getQuantity()) throw conflict("该订舱箱型需求已达到数量上限");
String containerNo = Iso6346ContainerNumber.require(request.containerNo());
if (repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
String requestedContainerNo = trim(request.containerNo());
String containerNo = requestedContainerNo == null ? null : Iso6346ContainerNumber.require(requestedContainerNo);
if (containerNo != null && repository.containerNoExists(containerNo, 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())) {
@@ -95,6 +96,32 @@ public class ContainerService {
return required(id);
}
@Transactional
public ContainerBookingEntity assignRealContainerNo(long id, String requestedContainerNo, int expectedVersion,
long operatorId, String operatorName) {
ContainerBookingEntity container = required(id);
requireVersion(container, expectedVersion);
if (container.getOceanBookingId() == null || container.getOceanBookingRequirementId() == null) {
throw conflict("模拟柜尚未关联已发布订舱需求");
}
if (trim(container.getContainerNo()) != null) throw conflict("真实集装箱号已补录");
SmartLoadingStatus value = status(container);
if ((value != SmartLoadingStatus.WAIT_LOADING && value != SmartLoadingStatus.EDITING)
|| repository.hasPickTask(id) || repository.hasEvidence(id) || repository.hasDocuments(id)) {
throw conflict("已进入PDA、证据、单证或不可逆装柜阶段,不能补录真实柜号");
}
String containerNo = Iso6346ContainerNumber.require(requestedContainerNo);
if (repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
try {
if (repository.assignRealContainerNo(id, containerNo, expectedVersion, operatorId, operatorName) != 1) {
throw conflict("真实柜号已被补录或货柜版本发生变化,请刷新后重试");
}
} catch (DuplicateKeyException exception) {
throw conflict("集装箱号已存在");
}
return required(id);
}
private void requireFirstLinkMutable(ContainerBookingEntity container) {
SmartLoadingStatus value = status(container);
if ((value != SmartLoadingStatus.WAIT_LOADING && value != SmartLoadingStatus.EDITING)