From a5aecf6171b5a37895b82b3e3dea6c80505b5add Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 3 Sep 2026 21:34:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../baiyue/firstleg/dto/SmartLoadingDtos.java | 10 +- .../mapper/SmartLoadingCandidateMapper.java | 147 ++++++++----- .../repository/SmartLoadingRepository.java | 6 +- .../firstleg/service/SmartLoadingService.java | 205 ++++++++++++++++-- 4 files changed, 286 insertions(+), 82 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java index 2be703b5d..920750aed 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/SmartLoadingDtos.java @@ -7,10 +7,13 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} public record InboundCandidateQuery( @NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes, @Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode, - LocalDate createdStart, LocalDate createdEnd, CandidateOrderStatus orderStatus, + LocalDate createdStart, LocalDate createdEnd, + @Size(max=4) List<@NotNull CandidateOrderStatus> orderStatuses, @Min(1) long page, @Min(1) @Max(200) long size) { public InboundCandidateQuery { channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes); + orderStatuses = orderStatuses == null ? List.of() + : List.copyOf(new java.util.LinkedHashSet<>(orderStatuses)); if (page == 0) page = 1; if (size == 0) size = 20; } @@ -24,7 +27,8 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal averageUnitVolumeCbm, BigDecimal availableVolumeCbm, Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes, - Boolean locked, String blockReason, String orderStatusSnapshot, LocalDateTime expectedArrivalAt, + Boolean locked, Boolean canAssign, String blockReason, + String orderStatusSnapshot, LocalDateTime expectedArrivalAt, List groupMembers) { public InboundCandidate { groupMembers = groupMembers == null ? List.of() : List.copyOf(groupMembers); } public Long customerId() { return accountId; } @@ -32,7 +36,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){} public record WorkbenchContainerQuery( @NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes, @Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode, - LocalDate createdStart, LocalDate createdEnd, CandidateOrderStatus orderStatus, + LocalDate createdStart, LocalDate createdEnd, @Min(1) long page, @Min(1) @Max(200) long size) { public WorkbenchContainerQuery { channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java index fbc264aed..07cacb39c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/SmartLoadingCandidateMapper.java @@ -85,36 +85,48 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba 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,detail.createdAt, - SUM(detail.availablePackageCount) availablePackageCount - FROM ( - SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode, - o.expected_arrival_at expectedArrivalAt,o.created_at createdAt,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>0 - WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId}) - AND o.channel_code IN - #{channel} - 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,o.created_at,i.inbound_detail_id - HAVING availablePackageCount>0 - ) detail - GROUP BY detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,detail.createdAt + SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode, + o.expected_arrival_at expectedArrivalAt,o.created_at createdAt, + CASE WHEN o.process_status='WAIT_SPLIT' THEN + COALESCE((SELECT SUM(mi.package_count) FROM fl_inbound_measurement m + JOIN fl_inbound_measurement_item mi + ON mi.tenant_id=m.tenant_id AND mi.measurement_id=m.id + AND mi.inbound_detail_id IS NOT NULL AND mi.package_count>0 + WHERE 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')),0) + ELSE COALESCE(o.total_package_count,0) END + -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.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),0) availablePackageCount + FROM fl_inbound_order o + WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId}) + AND o.channel_code IN + #{channel} + AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT')) + AND ( + o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED', + 'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','WAIT_SPLIT','LOADING') + ) + + AND ( + + + o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED','WAIT_PRICING','FEE_CONFIRMING','INBOUNDING') + o.process_status='WAIT_SPLIT' + o.process_status='LOADING' + 1=0 + + + ) + + AND o.tenant_id=#{query.customerId} + AND LOWER(o.inbound_code) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') + AND o.created_at>=#{query.createdStart} + AND o.created_at<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY) ) eligible LEFT JOIN fl_coload_group_member current_member ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId @@ -125,21 +137,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba 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 + 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(*) )) - - AND SUM(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)>0 - - - AND SUM(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)>0 - - - AND DATE(MIN(eligible.createdAt))>=#{query.createdStart} - AND DATE(MIN(eligible.createdAt))<=#{query.createdEnd} - ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId """) @@ -169,8 +169,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba 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 + CASE WHEN current_group.locked_plan_id IS NOT NULL OR EXISTS ( + SELECT 1 FROM fl_loading_plan_item occupied + WHERE occupied.account_id=o.tenant_id AND occupied.inbound_id=o.id AND occupied.deleted=0 + ) THEN TRUE ELSE FALSE END locked,NULL blockReason, + 'WAIT_SPLIT' 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' @@ -182,7 +185,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba 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')) @@ -196,7 +198,45 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba ) GROUP BY o.id,m.id,i.inbound_detail_id,current_group.id,current_group.group_no HAVING availablePackageCount>0 - ORDER BY o.inbounded_at ASC,o.id ASC,i.inbound_detail_id ASC + UNION ALL + SELECT o.tenant_id accountId,o.id inboundId,NULL 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,NULL goodsName, + GREATEST(COALESCE(o.total_package_count,0)-COALESCE((SELECT SUM(used.package_count) + FROM fl_loading_plan_item used WHERE used.account_id=o.tenant_id + AND used.inbound_id=o.id AND used.deleted=0),0),0) availablePackageCount, + o.actual_gross_weight_kg/NULLIF(o.total_package_count,0) unitGrossWeightKg, + o.actual_net_weight_kg/NULLIF(o.total_package_count,0) unitNetWeightKg, + o.actual_volume_cbm/NULLIF(o.total_package_count,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.locked_plan_id IS NOT NULL OR EXISTS ( + SELECT 1 FROM fl_loading_plan_item occupied + WHERE occupied.account_id=o.tenant_id AND occupied.inbound_id=o.id AND occupied.deleted=0 + ) THEN TRUE ELSE FALSE END locked,NULL blockReason, + CASE WHEN o.process_status='LOADING' THEN 'LOADING' ELSE 'WAIT_MEASURE' END orderStatusSnapshot, + o.expected_arrival_at expectedArrivalAt + FROM fl_inbound_order o + 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 IN ('ACTIVE','LOCKED') + WHERE o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED', + 'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','LOADING') + AND ( + + + current_group.id=#{unit.groupId} + (current_group.id IS NULL AND o.tenant_id=#{unit.accountId} AND o.id=#{unit.inboundId}) + + + ) + ORDER BY inboundId ASC,inboundDetailId ASC """) List globalCandidatesByUnits(@Param("units") List units); @@ -216,7 +256,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba AND b.channel_code IN #{channel} AND b.warehouse_id=#{query.warehouseId} - + AND EXISTS ( SELECT 1 FROM fl_loading_plan_item filter_item JOIN fl_inbound_order filter_inbound ON filter_inbound.tenant_id=filter_item.account_id @@ -226,14 +266,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba AND filter_item.inbound_code LIKE CONCAT('%',#{query.inboundCode},'%') AND filter_inbound.created_at >= #{query.createdStart} AND filter_inbound.created_at < DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY) - - - AND filter_inbound.order_status_snapshot IN ('SUBMITTED','PENDING_REVIEW') - AND filter_inbound.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED','WAIT_PRICING','FEE_CONFIRMING','INBOUNDING') - AND filter_inbound.process_status='WAIT_SPLIT' - AND filter_inbound.process_status='LOADING' - - ) ORDER BY b.updated_at DESC,b.id DESC @@ -241,8 +273,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba """) Page workbenchContainers(Page page, @Param("query") WorkbenchContainerQuery query, - @Param("actualChannels") Set actualChannels, - @Param("orderStatus") String orderStatus); + @Param("actualChannels") Set actualChannels); @Select(""" SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java index e506de4f4..8cdaf8ba6 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/SmartLoadingRepository.java @@ -51,8 +51,7 @@ public class SmartLoadingRepository { public PageResult workbenchContainers(WorkbenchContainerQuery query, Set actualChannels) { return PageResult.from(candidates.workbenchContainers( - new Page<>(query.page(), query.size()), query, actualChannels, - query.orderStatus() == null ? null : query.orderStatus().name())); + new Page<>(query.page(), query.size()), query, actualChannels)); } public List channelContainerCounts(long warehouseId) { return candidates.channelContainerCounts(warehouseId); @@ -68,6 +67,9 @@ public class SmartLoadingRepository { .eq(InboundOrderEntity::getTenantId, accountId) .eq(InboundOrderEntity::getId, inboundId)); } + public InboundOrderEntity inbound(long inboundId) { + return inbounds.selectById(inboundId); + } public InboundOrderEntity inboundForUpdate(long accountId, long inboundId) { return inbounds.selectOne(new LambdaQueryWrapper() .eq(InboundOrderEntity::getTenantId, accountId) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index 7837693b7..5bfc3f196 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -121,12 +121,34 @@ public class SmartLoadingService { throw validation("创建开始日期不能晚于结束日期"); } Set actualChannels = actualChannels(query.channelGroupCodes()); - if (query.orderStatus() == CandidateOrderStatus.PENDING_REVIEW) { - return pendingReviewCandidates(query, actualChannels); + EnumSet selected = query.orderStatuses().isEmpty() + ? EnumSet.allOf(CandidateOrderStatus.class) + : EnumSet.copyOf(query.orderStatuses()); + boolean includePending = selected.remove(CandidateOrderStatus.PENDING_REVIEW); + InboundCandidateQuery firstlegQuery = withStatuses(query, List.copyOf(selected), 1, 1); + if (!includePending) return firstlegCandidates(withStatuses(query, List.copyOf(selected), + query.page(), query.size()), actualChannels); + if (selected.isEmpty()) return pendingReviewCandidates(query, actualChannels, query.page(), query.size()); + + PageResult pendingCount = pendingReviewCandidates(query, actualChannels, 1, 1); + PageResult firstlegCount = firstlegCandidates(firstlegQuery, actualChannels); + long total = pendingCount.total() + firstlegCount.total(); + long offset = Math.multiplyExact(query.page() - 1, query.size()); + List records = new ArrayList<>(); + if (offset < pendingCount.total()) { + records.addAll(pendingReviewRange(query, actualChannels, offset, query.size())); } - if (query.orderStatus() != null && query.orderStatus() != CandidateOrderStatus.WAIT_SPLIT) { - return new PageResult<>(List.of(), query.page(), query.size(), 0, 0); + long firstlegOffset = Math.max(0, offset - pendingCount.total()); + int remaining = Math.toIntExact(query.size() - records.size()); + if (remaining > 0 && firstlegOffset < firstlegCount.total()) { + records.addAll(firstlegRange(query, actualChannels, List.copyOf(selected), firstlegOffset, remaining)); } + long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size(); + return new PageResult<>(records, query.page(), query.size(), total, pages); + } + + private PageResult firstlegCandidates(InboundCandidateQuery query, + Set actualChannels) { PageResult unitPage = repository.globalCandidateUnits(query, actualChannels); if (unitPage.records().isEmpty()) { return new PageResult<>(List.of(), unitPage.page(), unitPage.size(), unitPage.total(), unitPage.pages()); @@ -156,12 +178,13 @@ public class SmartLoadingService { } private PageResult pendingReviewCandidates(InboundCandidateQuery query, - Set actualChannels) { + Set actualChannels, + long page, long size) { ApiResponse> response; try { response = pendingReviewOrders.page(internalToken, List.copyOf(actualChannels), query.warehouseId(), query.customerId(), query.inboundCode(), query.createdStart(), query.createdEnd(), - query.page(), query.size()); + page, size); } catch (RuntimeException exception) { throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "待审核订单服务暂时不可用"); } @@ -173,11 +196,49 @@ public class SmartLoadingService { new InboundCandidate(row.customerId(), null, row.orderId(), row.orderNo(), row.inboundCode(), row.warehouseId(), row.channelCode(), row.channelName(), null, null, null, row.customerName(), null, row.totalPackageCount(), null, null, null, null, - null, null, null, null, false, null, + null, null, null, null, false, true, null, "PENDING_REVIEW", null, List.of())).toList(); return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages()); } + private List pendingReviewRange(InboundCandidateQuery query, Set channels, + long offset, long limit) { + return pageRange(offset, limit, (page, size) -> pendingReviewCandidates(query, channels, page, size)); + } + + private List firstlegRange(InboundCandidateQuery query, Set channels, + List statuses, + long offset, long limit) { + return pageRange(offset, limit, (page, size) -> firstlegCandidates( + withStatuses(query, statuses, page, size), channels)); + } + + private static List pageRange(long offset, long limit, + java.util.function.BiFunction> source) { + if (limit <= 0) return List.of(); + final long chunk = 200; + long page = offset / chunk + 1; + int skip = Math.toIntExact(offset % chunk); + List result = new ArrayList<>(); + while (result.size() < limit) { + List rows = source.apply(page++, chunk).records(); + if (rows.isEmpty()) break; + int from = Math.min(skip, rows.size()); + int take = Math.min(Math.toIntExact(limit - result.size()), rows.size() - from); + if (take > 0) result.addAll(rows.subList(from, from + take)); + skip = 0; + if (rows.size() < chunk) break; + } + return List.copyOf(result); + } + + private static InboundCandidateQuery withStatuses(InboundCandidateQuery source, + List statuses, + long page, long size) { + return new InboundCandidateQuery(source.channelGroupCodes(), source.warehouseId(), source.customerId(), + source.inboundCode(), source.createdStart(), source.createdEnd(), statuses, page, size); + } + public PageResult containers(WorkbenchContainerQuery query) { if (query.createdStart() != null && query.createdEnd() != null && query.createdStart().isAfter(query.createdEnd())) { @@ -190,11 +251,12 @@ public class SmartLoadingService { Candidate first = details.get(0); int packages = details.stream().map(Candidate::availablePackageCount) .filter(Objects::nonNull).mapToInt(Integer::intValue).sum(); - BigDecimal volume = details.stream() - .map(detail -> unit(detail.unitVolumeCbm()).multiply(BigDecimal.valueOf( + boolean hasVolume = details.stream().anyMatch(detail -> detail.unitVolumeCbm() != null); + BigDecimal volume = hasVolume ? details.stream().filter(detail -> detail.unitVolumeCbm() != null) + .map(detail -> detail.unitVolumeCbm().multiply(BigDecimal.valueOf( Math.max(0, detail.availablePackageCount() == null ? 0 : detail.availablePackageCount())))) - .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP); - BigDecimal averageVolume = packages == 0 ? BigDecimal.ZERO : volume.divide( + .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP) : null; + BigDecimal averageVolume = packages == 0 || volume == null ? null : volume.divide( BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP); String goods = details.stream().map(Candidate::goodsName).filter(StringUtils::hasText) .map(String::trim).distinct().collect(java.util.stream.Collectors.joining("、")); @@ -203,7 +265,7 @@ public class SmartLoadingService { first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(), first.customerName(), goods, packages, first.unitGrossWeightKg(), first.unitNetWeightKg(), averageVolume, volume, first.coLoadGroupId(), first.groupNo(), first.memberCount(), - first.groupTotalBoxes(), first.locked(), first.blockReason(), first.orderStatusSnapshot(), + first.groupTotalBoxes(), first.locked(), canAssign(first), capabilityBlock(first), first.orderStatusSnapshot(), first.expectedArrivalAt(), List.of()); } @@ -217,11 +279,12 @@ public class SmartLoadingService { 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); + boolean hasVolume = ordered.stream().anyMatch(candidate -> candidate.availableVolumeCbm() != null); + BigDecimal volume = hasVolume ? ordered.stream().map(InboundCandidate::availableVolumeCbm) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP) : null; BigDecimal gross = weightedAverage(ordered, InboundCandidate::unitGrossWeightKg, packages); BigDecimal net = weightedAverage(ordered, InboundCandidate::unitNetWeightKg, packages); - BigDecimal averageVolume = packages == 0 ? BigDecimal.ZERO + BigDecimal averageVolume = packages == 0 || volume == null ? null : volume.divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP); List groupMembers = ordered.stream().map(member -> new GroupMemberCandidate( member.accountId(), member.inboundId(), member.orderId(), member.orderNo(), member.inboundCode(), @@ -235,11 +298,42 @@ public class SmartLoadingService { first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(), first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(), customers, goods, packages, gross, net, averageVolume, volume, metadata.coLoadGroupId(), metadata.groupNo(), - metadata.memberCount(), metadata.groupTotalBoxes(), false, null, first.orderStatusSnapshot(), + metadata.memberCount(), metadata.groupTotalBoxes(), + ordered.stream().anyMatch(candidate -> Boolean.TRUE.equals(candidate.locked())), + ordered.stream().allMatch(candidate -> Boolean.TRUE.equals(candidate.canAssign())), + ordered.stream().map(InboundCandidate::blockReason).filter(StringUtils::hasText).findFirst().orElse(null), + groupStatus(ordered), ordered.stream().map(InboundCandidate::expectedArrivalAt).filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null), groupMembers); } + private static boolean canAssign(Candidate candidate) { + return !Boolean.TRUE.equals(candidate.locked()) && !StringUtils.hasText(candidate.blockReason()) + && candidate.availablePackageCount() != null && candidate.availablePackageCount() > 0; + } + + private static String capabilityBlock(Candidate candidate) { + if (StringUtils.hasText(candidate.blockReason())) return candidate.blockReason(); + if (Boolean.TRUE.equals(candidate.locked())) return "该入仓码已被有效柜次占用"; + if (candidate.availablePackageCount() == null || candidate.availablePackageCount() <= 0) return "没有可加入的剩余箱数"; + return null; + } + + private static String groupStatus(List members) { + return members.stream().map(InboundCandidate::orderStatusSnapshot).filter(StringUtils::hasText) + .min(Comparator.comparingInt(SmartLoadingService::statusOrder)).orElse(null); + } + + private static int statusOrder(String status) { + return switch (status) { + case "PENDING_REVIEW" -> 0; + case "WAIT_MEASURE" -> 1; + case "WAIT_SPLIT" -> 2; + case "LOADING" -> 3; + default -> 4; + }; + } + private static String unitKey(Long groupId, Long accountId, Long inboundId) { return groupId == null ? "I|" + accountId + '|' + inboundId : "G|" + groupId; } @@ -247,8 +341,8 @@ public class SmartLoadingService { private static BigDecimal weightedAverage(List members, java.util.function.Function getter, int packages) { - if (packages == 0) return BigDecimal.ZERO; - return members.stream().map(candidate -> unit(getter.apply(candidate)).multiply( + if (packages == 0 || members.stream().map(getter).anyMatch(Objects::isNull)) return null; + return members.stream().map(candidate -> getter.apply(candidate).multiply( BigDecimal.valueOf(candidate.availablePackageCount()))) .reduce(BigDecimal.ZERO, BigDecimal::add) .divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP); @@ -466,6 +560,13 @@ public class SmartLoadingService { Plan replay = replay(operation, planId, "ADD_INBOUND", null, requestHash); if (replay != null) return replay; + InboundOrderEntity projection = repository.inbound(request.inboundId()); + if (!wholeGroupAssignment && projection != null && Set.of("PREPARING", "WAIT_MEASURE", + "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED", "WAIT_PRICING", + "FEE_CONFIRMING", "INBOUNDING").contains(projection.getProcessStatus())) { + return assignDeclaredInbound(planId, request, projection, operation, requestHash, + operatorId, operatorName); + } LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName); ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId()); List details = candidates(plan.getContainerBookingId(), booking.getWarehouseId()).stream() @@ -497,6 +598,66 @@ public class SmartLoadingService { return view(assigned, booking); } + private Plan assignDeclaredInbound(long planId, AssignInboundRequest request, + InboundOrderEntity projection, String operation, + String requestHash, long operatorId, String operatorName) { + LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName); + ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId()); + InboundOrderEntity locked = repository.inboundForUpdate(projection.getTenantId(), projection.getId()); + if (locked == null || !Set.of("PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", + "MEASURE_REJECTED", "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING") + .contains(locked.getProcessStatus())) { + throw conflict("入仓码状态已变化,请刷新后重试"); + } + if (!repository.activeItemsByInbound(locked.getTenantId(), locked.getId()).isEmpty()) { + throw conflict("该入仓码已被有效柜次占用"); + } + var order = loadInboundSnapshot(locked.getTenantId(), locked.getOrderId()); + if (!Objects.equals(order.inboundCode(), locked.getInboundCode()) + || !Objects.equals(order.warehouseId(), locked.getWarehouseId()) + || !Objects.equals(order.channelCode(), locked.getChannelCode())) { + throw conflict("订单履约快照已变化,请刷新后重试"); + } + List declared = loadDeclaredCargo(locked.getTenantId(), locked.getOrderId()); + List candidates = declared.stream().map(value -> declaredCandidate( + order, locked.getId(), value.cargo(), "WAIT_MEASURE")).toList(); + List allocations = proportionalAllocations(candidates, request.packageCount()); + List current = new ArrayList<>(repository.items(planId)); + List created = new ArrayList<>(); + int sequence = current.size(); + for (InboundAllocation allocation : allocations) { + if (allocation.packageCount() == 0) continue; + Candidate candidate = allocation.candidate(); + requireSameChannel(booking, candidate); + requireSameCustomerScope(booking, candidate); + checkRouteAndCapacity(booking, current, candidate, allocation.packageCount(), true); + OrderCargoClient.CargoBinding cargo = declared.stream().map(DeclaredCargo::cargo) + .filter(value -> Objects.equals(value.cargoId(), candidate.inboundDetailId())) + .findFirst().orElseThrow(() -> conflict("订单货物明细已变化,请刷新后重试")); + LoadingPlanItemEntity item = snapshot(planId, candidate, cargo, allocation.packageCount(), + operatorId, operatorName, ++sequence); + current.add(item); + created.add(item); + } + if (created.isEmpty()) throw validation("入仓码没有可加入装柜清单的箱数"); + splitPreloadService.ensurePendingForPreAllocation(locked.getTenantId(), locked.getId(), + operatorId, operatorName); + touch(plan, operatorId, operatorName); + try { + created.forEach(repository::insert); + } catch (DuplicateKeyException exception) { + throw conflict("该入仓码已被并发加入其他柜次"); + } + history(planId, null, "ADD_INBOUND", plan.getPlanStatus(), plan.getPlanStatus(), operation, + json(values("version", request.expectedVersion(), "inboundId", request.inboundId())), + json(values("requestHash", requestHash, "packageCount", request.packageCount(), + "specificationLineCount", created.size(), "provisional", true)), operatorId, operatorName); + loadingModelBridge.bindIncrementalAllocation(planId, locked.getTenantId(), locked.getId(), + operatorId, operatorName, operation); + created.forEach(item -> loadingPickService.sync(planId, item)); + return view(repository.plan(planId), booking); + } + List proportionalAllocations(List details, int selected) { int available = details.stream().map(Candidate::availablePackageCount).filter(Objects::nonNull) .mapToInt(Integer::intValue).sum(); @@ -1075,6 +1236,12 @@ public class SmartLoadingService { private Candidate declaredCandidate(PendingReviewOrderClient.Row pending, com.baiyue.firstleg.dto.OrderApprovedInboundCommand snapshot, long inboundId, OrderCargoClient.CargoBinding cargo) { + return declaredCandidate(snapshot, inboundId, cargo, pending.mainStatus()); + } + + private Candidate declaredCandidate(com.baiyue.firstleg.dto.OrderApprovedInboundCommand snapshot, + long inboundId, OrderCargoClient.CargoBinding cargo, + String candidateStatus) { int boxes = cargo.originalPackageCount() == null ? 0 : cargo.originalPackageCount(); if (boxes <= 0) throw conflict("订单货物箱数不完整,请先修正订单"); BigDecimal divisor = BigDecimal.valueOf(boxes); @@ -1086,7 +1253,7 @@ public class SmartLoadingService { snapshot.channelName(), snapshot.destinationWarehouseId(), snapshot.destinationCode(), snapshot.destinationName(), snapshot.customerName(), cargo.cargoName(), boxes, unitGross, unitNet, unitVolume, null, null, null, null, false, null, - pending.mainStatus(), snapshot.expectedArrivalAt()); + candidateStatus, snapshot.expectedArrivalAt()); } private record DeclaredCargo(OrderCargoClient.CargoBinding cargo) {}