Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
This commit is contained in:
2026-09-03 16:19:48 +08:00
10 changed files with 307 additions and 123 deletions
@@ -1,5 +1,5 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery; import org.apache.ibatis.annotations.*; import java.util.List; import java.util.Set;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery; import org.apache.ibatis.annotations.*; import java.time.LocalDateTime; import java.util.List; import java.util.Set;
@Mapper public interface SmartLoadingCandidateMapper {
@Select("""
SELECT b.channel_code channelCode,COUNT(DISTINCT b.id) availableContainerCount
@@ -84,12 +84,83 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
List<Candidate> candidates(@Param("warehouseId") long warehouseId);
@Select("""
<script>
SELECT current_group.id groupId,
CASE WHEN current_group.id IS NULL THEN eligible.accountId END accountId,
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END inboundId,
MIN(eligible.expectedArrivalAt) sortArrivalAt,MIN(eligible.inboundId) sortInboundId
FROM (
SELECT detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,
SUM(detail.availablePackageCount) availablePackageCount
FROM (
SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode,
o.expected_arrival_at expectedArrivalAt,i.inbound_detail_id,
SUM(i.package_count)
-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
-COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
WHERE o.inbound_role='MASTER' AND split_item.tenant_id=o.tenant_id
AND split_item.parent_inbound_id=o.id
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0) availablePackageCount
FROM fl_inbound_order o
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
AND m.status='APPROVED'
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
AND i.inbound_detail_id IS NOT NULL AND i.package_count&gt;0
WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId})
AND o.channel_code IN
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
AND o.process_status='WAIT_SPLIT'
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
GROUP BY o.tenant_id,o.id,o.inbound_code,o.expected_arrival_at,i.inbound_detail_id
HAVING availablePackageCount&gt;0
) detail
GROUP BY detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt
) eligible
LEFT JOIN fl_coload_group_member current_member
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
AND current_member.active_flag=1
LEFT JOIN fl_coload_group current_group
ON current_group.id=current_member.group_id AND current_group.group_status IN ('ACTIVE','LOCKED')
GROUP BY current_group.id,
CASE WHEN current_group.id IS NULL THEN eligible.accountId END,
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END
HAVING (current_group.id IS NULL OR (
MIN(current_group.group_status)='ACTIVE' AND MAX(current_group.locked_plan_id) IS NULL
AND COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1)
AND SUM(CASE WHEN eligible.availablePackageCount=current_member.full_package_count THEN 1 ELSE 0 END)=COUNT(*)
))
<if test="query.customerId != null">
AND SUM(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)&gt;0
</if>
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">
AND SUM(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)&gt;0
</if>
<if test="query.expectedArrivalStart != null or query.expectedArrivalEnd != null">
AND SUM(CASE WHEN eligible.expectedArrivalAt IS NOT NULL
<if test="query.expectedArrivalStart != null">AND DATE(eligible.expectedArrivalAt)&gt;=#{query.expectedArrivalStart}</if>
<if test="query.expectedArrivalEnd != null">AND DATE(eligible.expectedArrivalAt)&lt;=#{query.expectedArrivalEnd}</if>
THEN 1 ELSE 0 END)&gt;0
</if>
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
</script>
""")
Page<CandidateUnit> globalCandidateUnits(Page<CandidateUnit> page,
@Param("query") InboundCandidateQuery query,@Param("actualChannels") Set<String> actualChannels);
@Select("""
<script>
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId,
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
i.package_count-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
SUM(i.package_count)-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
-COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
@@ -98,36 +169,44 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0) availablePackageCount,
COALESCE(o.actual_gross_weight_kg / NULLIF(o.total_package_count, 0), 0) unitGrossWeightKg,
COALESCE(o.actual_net_weight_kg / NULLIF(o.total_package_count, 0), 0) unitNetWeightKg,
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm,
NULL coLoadGroupId,NULL groupNo,NULL memberCount,NULL groupTotalBoxes,
NULL locked,NULL blockReason,o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt
COALESCE(SUM(COALESCE(i.total_volume_cbm,COALESCE(i.unit_volume_cbm,0)*i.package_count))
/ NULLIF(SUM(i.package_count),0),0) unitVolumeCbm,
current_group.id coLoadGroupId,current_group.group_no groupNo,
(SELECT COUNT(*) FROM fl_coload_group_member member_count
WHERE member_count.group_id=current_group.id AND member_count.active_flag=1) memberCount,
(SELECT SUM(member_boxes.full_package_count) FROM fl_coload_group_member member_boxes
WHERE member_boxes.group_id=current_group.id AND member_boxes.active_flag=1) groupTotalBoxes,
CASE WHEN current_group.id IS NULL THEN NULL ELSE FALSE END locked,NULL blockReason,
o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt
FROM fl_inbound_order o
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
AND m.status='APPROVED'
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
JOIN (
SELECT tenant_id,measurement_id,inbound_detail_id,SUM(package_count) package_count,
COALESCE(SUM(COALESCE(total_volume_cbm,COALESCE(unit_volume_cbm,0)*package_count))
/ NULLIF(SUM(package_count),0),0) unit_volume_cbm
FROM fl_inbound_measurement_item
WHERE inbound_detail_id IS NOT NULL AND package_count>0
GROUP BY tenant_id,measurement_id,inbound_detail_id
) i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
WHERE (#{warehouseId} IS NULL OR o.warehouse_id=#{warehouseId})
AND o.process_status='WAIT_SPLIT'
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
AND i.inbound_detail_id IS NOT NULL AND i.package_count&gt;0
LEFT JOIN fl_coload_group_member current_member
ON current_member.tenant_id=o.tenant_id AND current_member.inbound_id=o.id AND current_member.active_flag=1
LEFT JOIN fl_coload_group current_group
ON current_group.id=current_member.group_id AND current_group.group_status='ACTIVE'
AND current_group.locked_plan_id IS NULL
WHERE o.process_status='WAIT_SPLIT'
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
+COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
WHERE o.inbound_role='MASTER' AND split_item.tenant_id=o.tenant_id
AND split_item.parent_inbound_id=o.id
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0)
ORDER BY o.inbounded_at ASC, o.id ASC, i.inbound_detail_id ASC
AND (
<foreach collection="units" item="unit" separator=" OR ">
<choose>
<when test="unit.groupId != null">current_group.id=#{unit.groupId}</when>
<otherwise>(current_group.id IS NULL AND o.tenant_id=#{unit.accountId} AND o.id=#{unit.inboundId})</otherwise>
</choose>
</foreach>
)
GROUP BY o.id,m.id,i.inbound_detail_id,current_group.id,current_group.group_no
HAVING availablePackageCount&gt;0
ORDER BY o.inbounded_at ASC,o.id ASC,i.inbound_detail_id ASC
</script>
""")
List<Candidate> globalCandidates(@Param("warehouseId") Long warehouseId);
List<Candidate> globalCandidatesByUnits(@Param("units") List<CandidateUnit> units);
@Select("""
<script>
@@ -247,4 +326,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
private String channelCode;
private Long availableContainerCount;
}
record CandidateUnit(Long groupId,Long accountId,Long inboundId,
LocalDateTime sortArrivalAt,Long sortInboundId) {}
}
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery;
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer;
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
import com.baiyue.common.core.api.PageResult;
@@ -17,6 +18,7 @@ import com.baiyue.firstleg.mapper.LoadingPlanItemMapper;
import com.baiyue.firstleg.mapper.LoadingPlanMapper;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import org.springframework.stereotype.Repository;
import jakarta.annotation.Resource;
@@ -37,7 +39,14 @@ public class SmartLoadingRepository {
@Resource private InboundOrderMapper inbounds;
public List<Candidate> candidates(long warehouseId) { return candidates.candidates(warehouseId); }
public List<Candidate> globalCandidates(Long warehouseId) { return candidates.globalCandidates(warehouseId); }
public PageResult<CandidateUnit> globalCandidateUnits(InboundCandidateQuery query,
Set<String> actualChannels) {
return PageResult.from(candidates.globalCandidateUnits(
new Page<>(query.page(), query.size()), query, actualChannels));
}
public List<Candidate> globalCandidatesByUnits(List<CandidateUnit> units) {
return units.isEmpty() ? List.of() : candidates.globalCandidatesByUnits(units);
}
public PageResult<WorkbenchContainer> workbenchContainers(WorkbenchContainerQuery query,
Set<String> actualChannels) {
return PageResult.from(candidates.workbenchContainers(
@@ -13,6 +13,7 @@ import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.split.service.SplitPreloadService;
import org.springframework.dao.DuplicateKeyException;
@@ -109,46 +110,42 @@ public class SmartLoadingService {
}
/** Global workbench candidates. A co-load group is one paging unit and can never be split across pages. */
@Transactional(readOnly = true)
public PageResult<InboundCandidate> inboundCandidates(InboundCandidateQuery query) {
if (query.expectedArrivalStart() != null && query.expectedArrivalEnd() != null
&& query.expectedArrivalStart().isAfter(query.expectedArrivalEnd())) {
throw validation("预计到货开始日期不能晚于结束日期");
}
Set<String> actualChannels = actualChannels(query.channelGroupCodes());
List<InboundCandidate> all = repository.globalCandidates(query.warehouseId()).stream()
if (query.orderStatus() != null && query.orderStatus() != CandidateOrderStatus.WAIT_SPLIT) {
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
}
PageResult<CandidateUnit> unitPage = repository.globalCandidateUnits(query, actualChannels);
if (unitPage.records().isEmpty()) {
return new PageResult<>(List.of(), unitPage.page(), unitPage.size(), unitPage.total(), unitPage.pages());
}
List<InboundCandidate> all = repository.globalCandidatesByUnits(unitPage.records()).stream()
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
java.util.stream.Collectors.toList()))
.values().stream().map(this::inboundCandidate).toList();
Map<String, GroupSnapshot> snapshots = new HashMap<>();
Map<Long, List<InboundCandidate>> grouped = new LinkedHashMap<>();
List<InboundCandidate> standalone = new ArrayList<>();
Map<String, InboundCandidate> units = new HashMap<>();
Map<Long, List<InboundCandidate>> grouped = all.stream()
.filter(candidate -> candidate.coLoadGroupId() != null)
.collect(java.util.stream.Collectors.groupingBy(InboundCandidate::coLoadGroupId,
LinkedHashMap::new, java.util.stream.Collectors.toList()));
for (InboundCandidate candidate : all) {
GroupSnapshot group = snapshots.computeIfAbsent(candidate.accountId() + "|" + candidate.inboundId(),
ignored -> group(candidate.accountId(), candidate.inboundId()));
if (group == null) {
standalone.add(candidate);
} else {
grouped.computeIfAbsent(group.groupId(), ignored -> new ArrayList<>()).add(candidate);
if (candidate.coLoadGroupId() == null) {
units.put(unitKey(null, candidate.accountId(), candidate.inboundId()), candidate);
}
}
List<InboundCandidate> units = new ArrayList<>(standalone);
for (List<InboundCandidate> members : grouped.values()) {
GroupSnapshot group = snapshots.get(members.get(0).accountId() + "|" + members.get(0).inboundId());
InboundCandidate aggregate = globalGroupCandidate(group, members);
if (aggregate != null) units.add(aggregate);
for (var entry : grouped.entrySet()) {
InboundCandidate aggregate = globalGroupCandidate(entry.getValue());
if (aggregate != null) units.put(unitKey(entry.getKey(), null, null), aggregate);
}
units = units.stream().filter(candidate -> matches(query, actualChannels, candidate))
.sorted(Comparator.comparing(InboundCandidate::expectedArrivalAt,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(InboundCandidate::inboundId, Comparator.nullsLast(Comparator.naturalOrder())))
.toList();
long total = units.size();
long offset = query.page() - 1 > Long.MAX_VALUE / query.size()
? Long.MAX_VALUE : (query.page() - 1) * query.size();
long from = Math.min(total, offset);
long to = Math.min(total, from + query.size());
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
return new PageResult<>(units.subList((int) from, (int) to), query.page(), query.size(), total, pages);
List<InboundCandidate> records = unitPage.records().stream()
.map(unit -> units.get(unitKey(unit.groupId(), unit.accountId(), unit.inboundId())))
.filter(Objects::nonNull).toList();
return new PageResult<>(records, unitPage.page(), unitPage.size(), unitPage.total(), unitPage.pages());
}
public PageResult<WorkbenchContainer> containers(WorkbenchContainerQuery query) {
@@ -180,18 +177,16 @@ public class SmartLoadingService {
first.expectedArrivalAt(), List.of());
}
private InboundCandidate globalGroupCandidate(GroupSnapshot group, List<InboundCandidate> availableMembers) {
if (group == null || !"ACTIVE".equals(group.status()) || group.lockedPlanId() != null) return null;
Map<Long, InboundCandidate> byInbound = availableMembers.stream()
.collect(java.util.stream.Collectors.toMap(InboundCandidate::inboundId, value -> value, (a, b) -> a));
for (Member member : group.members()) {
InboundCandidate candidate = byInbound.get(member.inboundId());
if (candidate == null || !Objects.equals(candidate.accountId(), member.tenantId())
|| !Objects.equals(candidate.availablePackageCount(), member.fullPackageCount())) return null;
}
List<InboundCandidate> ordered = group.members().stream().map(member -> byInbound.get(member.inboundId())).toList();
private InboundCandidate globalGroupCandidate(List<InboundCandidate> availableMembers) {
if (availableMembers.isEmpty()) return null;
InboundCandidate metadata = availableMembers.get(0);
if (metadata.coLoadGroupId() == null || metadata.memberCount() == null
|| metadata.groupTotalBoxes() == null || availableMembers.size() != metadata.memberCount()) return null;
List<InboundCandidate> ordered = availableMembers.stream()
.sorted(Comparator.comparing(InboundCandidate::inboundId)).toList();
InboundCandidate first = ordered.get(0);
int packages = ordered.stream().map(InboundCandidate::availablePackageCount).mapToInt(Integer::intValue).sum();
if (packages != metadata.groupTotalBoxes()) return null;
BigDecimal volume = ordered.stream().map(InboundCandidate::availableVolumeCbm)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP);
BigDecimal gross = weightedAverage(ordered, InboundCandidate::unitGrossWeightKg, packages);
@@ -209,12 +204,16 @@ public class SmartLoadingService {
return new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
customers, goods, packages, gross, net, averageVolume, volume, group.groupId(), group.groupNo(),
group.members().size(), group.fullPackageCount(), false, null, first.orderStatusSnapshot(),
customers, goods, packages, gross, net, averageVolume, volume, metadata.coLoadGroupId(), metadata.groupNo(),
metadata.memberCount(), metadata.groupTotalBoxes(), false, null, first.orderStatusSnapshot(),
ordered.stream().map(InboundCandidate::expectedArrivalAt).filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null),
groupMembers);
}
private static String unitKey(Long groupId, Long accountId, Long inboundId) {
return groupId == null ? "I|" + accountId + '|' + inboundId : "G|" + groupId;
}
private static BigDecimal weightedAverage(List<InboundCandidate> members,
java.util.function.Function<InboundCandidate, BigDecimal> getter,
int packages) {
@@ -236,36 +235,6 @@ public class SmartLoadingService {
return actual;
}
private static boolean matches(InboundCandidateQuery query, Set<String> actualChannels,
InboundCandidate candidate) {
if (!actualChannels.contains(candidate.channelCode())) return false;
if (query.customerId() != null && !Objects.equals(candidate.accountId(), query.customerId())
&& candidate.groupMembers().stream().noneMatch(
member -> Objects.equals(member.accountId(), query.customerId()))) return false;
String inboundCode = trim(query.inboundCode());
if (StringUtils.hasText(inboundCode) && candidate.groupMembers().stream()
.map(GroupMemberCandidate::inboundCode).filter(Objects::nonNull)
.noneMatch(value -> value.toLowerCase(Locale.ROOT).contains(inboundCode.toLowerCase(Locale.ROOT)))
&& (candidate.inboundCode() == null || !candidate.inboundCode().toLowerCase(Locale.ROOT)
.contains(inboundCode.toLowerCase(Locale.ROOT)))) return false;
if (query.expectedArrivalStart() != null || query.expectedArrivalEnd() != null) {
boolean arrivalMatches = candidate.groupMembers().isEmpty()
? inArrivalRange(candidate.expectedArrivalAt(), query)
: candidate.groupMembers().stream().anyMatch(member -> inArrivalRange(
member.expectedArrivalAt(), query));
if (!arrivalMatches) return false;
}
return query.orderStatus() == null || query.orderStatus() == CandidateOrderStatus.WAIT_SPLIT;
}
private static boolean inArrivalRange(LocalDateTime arrival, InboundCandidateQuery query) {
if (arrival == null) return false;
return (query.expectedArrivalStart() == null
|| !arrival.toLocalDate().isBefore(query.expectedArrivalStart()))
&& (query.expectedArrivalEnd() == null
|| !arrival.toLocalDate().isAfter(query.expectedArrivalEnd()));
}
public Plan draftByBooking(long bookingId) {
ContainerBookingEntity booking = requireBooking(bookingId);
LoadingPlanEntity plan = repository.planByBooking(bookingId);