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
@@ -168,9 +168,6 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
@Param("version") int version, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Select("SELECT * FROM fl_container_booking WHERE container_no=#{containerNo} AND record_status='ACTIVE'")
ContainerBookingEntity selectActiveByContainerNo(@Param("containerNo") String containerNo);
@Update("""
UPDATE fl_container_booking SET unloading_completed_at=#{unloadingCompletedAt},
unloading_photo_uploaded_at=#{photoUploadedAt}, estimated_empty_reported_at=#{estimatedEmptyReportedAt},
@@ -39,7 +39,7 @@ public final class OceanBookingDtos {
}
}
public record LinkCandidate(Long oceanBookingId, String bookingNo, Long requirementId,
public record LinkCandidate(Long oceanBookingId, String bookingNo, String billOfLadingNo, Long requirementId,
String containerType, BigDecimal capacityCbm, BigDecimal maxWeightKg,
int requiredQuantity, long allocatedQuantity, long remainingQuantity,
String customerName, String warehouseName, String channelName) {}
@@ -55,7 +55,7 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
@Select("""
<script>
SELECT booking.id ocean_booking_id,booking.booking_no,requirement.id requirement_id,
SELECT booking.id ocean_booking_id,booking.booking_no,booking.bill_of_lading_no,requirement.id requirement_id,
requirement.container_type,requirement.capacity_cbm,requirement.max_weight_kg,
requirement.quantity required_quantity,
COALESCE(allocated.allocated_quantity,0) allocated_quantity,
@@ -38,6 +38,12 @@ public class OceanBookingRepository {
}
public OceanBookingEntity find(long id) { return bookings.selectById(id); }
public OceanBookingEntity findForUpdate(long id) { return bookings.selectForUpdate(id); }
public boolean billOfLadingNoExists(String billOfLadingNo, Long excludedId) {
if (billOfLadingNo == null) return false;
return bookings.selectCount(new LambdaQueryWrapper<OceanBookingEntity>()
.eq(OceanBookingEntity::getBillOfLadingNo, billOfLadingNo)
.ne(excludedId != null, OceanBookingEntity::getId, excludedId)) > 0;
}
public void insert(OceanBookingEntity value) { bookings.insert(value); }
public int transition(long id, String from, String to, int version, long operatorId, String operatorName) {
return bookings.transition(id, from, to, version, operatorId, operatorName);
@@ -102,9 +108,6 @@ public class OceanBookingRepository {
public void replaceParties(long id, List<OceanBookingPartyEntity> values) { parties.delete(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id)); values.forEach(parties::insert); }
public void replaceRequirements(long id, List<OceanBookingContainerRequirementEntity> values) { requirements.delete(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id)); values.forEach(requirements::insert); }
public OceanBookingContainerRequirementEntity requirement(long id, long requirementId) { return requirements.selectOne(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).eq(OceanBookingContainerRequirementEntity::getId, requirementId)); }
public boolean containerNoExists(String containerNo) {
return containerBookings.containerNoExists(containerNo, null);
}
public void insertContainer(ContainerBookingEntity value) { containerBookings.insert(value); }
public void insert(OceanBookingContainerAllocationEntity value) { allocations.insert(value); }
public int reassignAllocation(long sourceId, long targetId) {
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import feign.FeignException;
@@ -110,9 +111,11 @@ public class OceanBookingService {
value.setBookingNo(documentNumbers.next("OB"));
value.setStatus(OceanBookingStatus.DRAFT.name());
copy(value, request.data(), resolved);
requireUniqueBillOfLadingNo(value.getBillOfLadingNo(), null);
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
repository.insert(value);
try { repository.insert(value); }
catch (DuplicateKeyException exception) { throw duplicateBillOfLadingNo(); }
repository.replaceParties(value.getId(), parties(value.getId(), request.data().parties()));
List<OceanBookingContainerRequirementEntity> requirements = requirements(
value.getId(), request.data().containerRequirements());
@@ -132,7 +135,10 @@ public class OceanBookingService {
validateDraftStructure(request.data());
ResolvedDraft resolved = resolveDraft(request.data());
copy(before, request.data(), resolved);
if (repository.updateDraft(before, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
requireUniqueBillOfLadingNo(before.getBillOfLadingNo(), id);
try {
if (repository.updateDraft(before, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
} catch (DuplicateKeyException exception) { throw duplicateBillOfLadingNo(); }
repository.replaceParties(id, parties(id, request.data().parties()));
repository.replaceRequirements(id, requirements(id, request.data().containerRequirements()));
OceanBookingEntity after = required(id);
@@ -294,6 +300,14 @@ public class OceanBookingService {
v.setRemark(trim(d.remark()));
}
private void requireUniqueBillOfLadingNo(String billOfLadingNo, Long excludedId) {
if (repository.billOfLadingNoExists(billOfLadingNo, excludedId)) throw duplicateBillOfLadingNo();
}
private static BizException duplicateBillOfLadingNo() {
return conflict("提单号已存在,请检查是否重复创建订舱");
}
private void validateMasterData(OceanBookingEntity booking) {
resolveCustomer(booking.getCustomerId(), booking.getCustomerName());
resolveCarrier(booking.getShippingCompanyId(), booking.getShippingCompanyName(), booking.getCarrierCode());
@@ -22,9 +22,10 @@ public class ContainerRepository {
public ContainerBookingEntity findForUpdate(long id) { return mapper.selectBookingForUpdate(id); }
public ContainerBookingEntity findActiveForUpdate(long id) { return mapper.selectActiveBookingForUpdate(id); }
public boolean departed(long id) { return mapper.hasActiveOutboundDeparture(id) > 0; }
public boolean containerNoExists(String no, Long excludedId) {
public boolean containerNoExists(long oceanBookingId, String no, Long excludedId) {
if (no == null) return false;
return mapper.selectCount(new LambdaQueryWrapper<ContainerBookingEntity>()
.eq(ContainerBookingEntity::getOceanBookingId, oceanBookingId)
.eq(ContainerBookingEntity::getContainerNo, no)
.ne(excludedId != null, ContainerBookingEntity::getId, excludedId)) > 0;
}
@@ -93,7 +93,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("该订舱箱型需求已达到数量上限");
if (containerNo != null && repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
if (containerNo != null && repository.containerNoExists(request.oceanBookingId(), containerNo, id)) {
throw conflict("当前提单下集装箱号已存在");
}
if (!java.util.Objects.equals(container.getWarehouseId(), booking.getWarehouseId())
|| !java.util.Objects.equals(container.getChannelGroupCode(), booking.getChannelCode())
|| !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType())) {
@@ -105,7 +107,7 @@ public class ContainerService {
throw conflict("真实订舱柜已被消费或货柜版本发生变化,请刷新后重试");
}
} catch (DuplicateKeyException exception) {
throw conflict("集装箱号已存在");
throw conflict("当前提单下集装箱号已存在");
}
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity(); allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(request.oceanBookingId()); allocation.setRequirementId(request.requirementId()); allocation.setContainerBookingId(id); allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(operatorName); allocation.setAllocatedAt(LocalDateTime.now()); oceanBookings.insert(allocation);
return required(id);
@@ -126,13 +128,15 @@ public class ContainerService {
throw conflict("已进入PDA、证据、单证或不可逆装柜阶段,不能补录真实柜号");
}
String containerNo = Iso6346ContainerNumber.require(requestedContainerNo);
if (repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
if (repository.containerNoExists(container.getOceanBookingId(), containerNo, id)) {
throw conflict("当前提单下集装箱号已存在");
}
try {
if (repository.assignRealContainerNo(id, containerNo, expectedVersion, operatorId, operatorName) != 1) {
throw conflict("真实柜号已被补录或货柜版本发生变化,请刷新后重试");
}
} catch (DuplicateKeyException exception) {
throw conflict("集装箱号已存在");
throw conflict("当前提单下集装箱号已存在");
}
return required(id);
}
@@ -463,9 +463,10 @@ public class LoadingListService {
&& java.util.Objects.equals(before.getVoyageNo(), voyageNo)) {
return repository.preloadDetail(before);
}
if (containerNo != null
&& containerRepository.containerNoExists(containerNo, before.getContainerBookingId())) {
throw conflict("集装箱号已存在");
if (containerNo != null && before.getOceanBookingId() != null
&& containerRepository.containerNoExists(before.getOceanBookingId(), containerNo,
before.getContainerBookingId())) {
throw conflict("当前提单下集装箱号已存在");
}
try {
int updated = initialContainerFill
@@ -477,7 +478,7 @@ public class LoadingListService {
throw conflict("预装计划、PDA领取或物理作业状态已变化,请刷新后重试");
}
} catch (DuplicateKeyException exception) {
throw conflict("集装箱号已存在");
throw conflict("当前提单下集装箱号已存在");
}
repository.appendHistory(planId, operationId, "UPDATE_PRELOAD_TRANSPORT",
before.getPlanStatus(), before.getPlanStatus(), json(snapshot(before)),