优化
This commit is contained in:
+4
-2
@@ -1,6 +1,6 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.PageResult; import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.*; import com.baiyue.firstleg.service.FirstlegScopeGuard; import com.baiyue.firstleg.service.SmartLoadingService;
|
||||
import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import jakarta.annotation.Resource; import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List;
|
||||
@@ -13,7 +13,9 @@ public class PlatformSmartLoadingController {
|
||||
@GetMapping("/candidates") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<List<Candidate>> candidates(@AuthenticationPrincipal AuthenticatedUser u,@RequestParam @Positive long bookingId,@RequestParam @Positive long warehouseId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.candidates(bookingId,warehouseId));}
|
||||
@GetMapping("/inbound-candidates") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<List<InboundCandidate>> inboundCandidates(@AuthenticationPrincipal AuthenticatedUser u,@RequestParam @Positive long bookingId,@RequestParam @Positive long warehouseId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.inboundCandidates(bookingId,warehouseId));}
|
||||
public ApiResponse<PageResult<InboundCandidate>> inboundCandidates(@AuthenticationPrincipal AuthenticatedUser u,@Valid InboundCandidateQuery q){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.inboundCandidates(q));}
|
||||
@GetMapping("/containers") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<PageResult<WorkbenchContainer>> containers(@AuthenticationPrincipal AuthenticatedUser u,@Valid WorkbenchContainerQuery q){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.containers(q));}
|
||||
@GetMapping("/plans/by-booking/{bookingId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<Plan> draftByBooking(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long bookingId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.draftByBooking(bookingId));}
|
||||
@GetMapping("/plans/{planId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List;
|
||||
public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
public record ChannelSummary(String channelCode, String channelName,
|
||||
long candidateInboundCount, long availablePackageCount, long availableContainerCount) {}
|
||||
public enum CandidateOrderStatus { PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING }
|
||||
public record InboundCandidateQuery(
|
||||
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
|
||||
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode,
|
||||
LocalDate expectedArrivalStart, LocalDate expectedArrivalEnd, CandidateOrderStatus orderStatus,
|
||||
@Min(1) long page, @Min(1) @Max(200) long size) {
|
||||
public InboundCandidateQuery {
|
||||
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes);
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
public record GroupMemberCandidate(Long accountId, Long inboundId, Long orderId, String orderNo,
|
||||
String inboundCode, String customerName, Integer availablePackageCount,
|
||||
String orderStatusSnapshot, LocalDateTime expectedArrivalAt) {}
|
||||
public record InboundCandidate(Long accountId, Long inboundId, Long orderId, String orderNo, String inboundCode,
|
||||
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsSummary,
|
||||
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, String blockReason, String orderStatusSnapshot, LocalDateTime expectedArrivalAt,
|
||||
List<GroupMemberCandidate> groupMembers) {
|
||||
public InboundCandidate { groupMembers = groupMembers == null ? List.of() : List.copyOf(groupMembers); }
|
||||
public Long customerId() { return accountId; }
|
||||
}
|
||||
public record WorkbenchContainerQuery(
|
||||
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
|
||||
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode,
|
||||
LocalDate expectedArrivalStart, LocalDate expectedArrivalEnd, CandidateOrderStatus orderStatus,
|
||||
@Min(1) long page, @Min(1) @Max(200) long size) {
|
||||
public WorkbenchContainerQuery {
|
||||
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes);
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
public record WorkbenchContainer(Long bookingId, String bookingNo, String cabinetNo, String containerNo,
|
||||
Long warehouseId, String warehouseName, String channelCode, String channelName, String containerType,
|
||||
Long planId, String planNo, String planStatus, Integer version) {}
|
||||
public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
|
||||
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount,
|
||||
BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.ibatis.annotations.*; import java.util.List;
|
||||
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;
|
||||
@Mapper public interface SmartLoadingCandidateMapper {
|
||||
@Select("""
|
||||
SELECT b.channel_code channelCode,COUNT(DISTINCT b.id) availableContainerCount
|
||||
@@ -62,6 +62,95 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
""")
|
||||
List<Candidate> candidates(@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("""
|
||||
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
|
||||
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,
|
||||
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
|
||||
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'
|
||||
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
|
||||
""")
|
||||
List<Candidate> globalCandidates(@Param("warehouseId") Long warehouseId);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT b.id bookingId,b.booking_no bookingNo,b.cabinet_no cabinetNo,b.container_no containerNo,
|
||||
b.warehouse_id warehouseId,b.warehouse_name warehouseName,b.channel_code channelCode,
|
||||
b.channel_name channelName,b.container_type containerType,p.id planId,p.plan_no planNo,
|
||||
CASE WHEN p.id IS NULL OR NOT EXISTS (SELECT 1 FROM fl_loading_plan_item state_item
|
||||
WHERE state_item.loading_plan_id=p.id AND state_item.deleted=0)
|
||||
THEN 'WAIT_LOADING' ELSE p.plan_status END planStatus,b.version version
|
||||
FROM fl_container_booking b
|
||||
LEFT JOIN fl_loading_plan p ON p.container_booking_id=b.id
|
||||
WHERE b.record_status='ACTIVE'
|
||||
AND (p.id IS NULL OR p.plan_status IN ('EDITING','LOADING'))
|
||||
AND b.channel_code IN
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
<if test="query.warehouseId != null">AND b.warehouse_id=#{query.warehouseId}</if>
|
||||
<if test="query.customerId != null or query.inboundCode != null or query.expectedArrivalStart != null or query.expectedArrivalEnd != null or query.orderStatus != null">
|
||||
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
|
||||
AND filter_inbound.id=filter_item.inbound_id
|
||||
WHERE filter_item.loading_plan_id=p.id AND filter_item.deleted=0
|
||||
<if test="query.customerId != null">AND filter_item.account_id=#{query.customerId}</if>
|
||||
<if test="query.inboundCode != null and query.inboundCode != ''">AND filter_item.inbound_code LIKE CONCAT('%',#{query.inboundCode},'%')</if>
|
||||
<if test="query.expectedArrivalStart != null">AND filter_inbound.expected_arrival_at >= #{query.expectedArrivalStart}</if>
|
||||
<if test="query.expectedArrivalEnd != null">AND filter_inbound.expected_arrival_at < DATE_ADD(#{query.expectedArrivalEnd},INTERVAL 1 DAY)</if>
|
||||
<if test="orderStatus != null">
|
||||
<choose>
|
||||
<when test="orderStatus == 'PENDING_REVIEW'">AND filter_inbound.order_status_snapshot IN ('SUBMITTED','PENDING_REVIEW')</when>
|
||||
<when test="orderStatus == 'WAIT_MEASURE'">AND filter_inbound.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED','WAIT_PRICING','FEE_CONFIRMING','INBOUNDING')</when>
|
||||
<when test="orderStatus == 'WAIT_SPLIT'">AND filter_inbound.process_status='WAIT_SPLIT'</when>
|
||||
<when test="orderStatus == 'LOADING'">AND filter_inbound.process_status='LOADING'</when>
|
||||
</choose>
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
ORDER BY b.updated_at DESC,b.id DESC
|
||||
</script>
|
||||
""")
|
||||
Page<WorkbenchContainer> workbenchContainers(Page<WorkbenchContainer> page,
|
||||
@Param("query") WorkbenchContainerQuery query,
|
||||
@Param("actualChannels") Set<String> actualChannels,
|
||||
@Param("orderStatus") String orderStatus);
|
||||
|
||||
@Select("""
|
||||
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,
|
||||
|
||||
@@ -2,6 +2,10 @@ 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.WorkbenchContainer;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
@@ -19,6 +23,7 @@ import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** 智能装柜聚合的真实 MySQL 持久化边界。 */
|
||||
@Repository
|
||||
@@ -32,6 +37,13 @@ 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<WorkbenchContainer> workbenchContainers(WorkbenchContainerQuery query,
|
||||
Set<String> actualChannels) {
|
||||
return PageResult.from(candidates.workbenchContainers(
|
||||
new Page<>(query.page(), query.size()), query, actualChannels,
|
||||
query.orderStatus() == null ? null : query.orderStatus().name()));
|
||||
}
|
||||
public List<ChannelContainerCountRow> channelContainerCounts(long warehouseId) {
|
||||
return candidates.channelContainerCounts(warehouseId);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.*;
|
||||
@@ -38,6 +39,17 @@ import java.util.*;
|
||||
@Service
|
||||
public class SmartLoadingService {
|
||||
private static final BigDecimal CAPACITY_LIMIT_RATIO = new BigDecimal("1.10");
|
||||
private static final Map<String, Set<String>> WORKBENCH_CHANNEL_GROUPS = Map.of(
|
||||
"MATSON_CLX", Set.of("MATSON_CLX_TRUCK", "MATSON_CLX_PARCEL"),
|
||||
"MATSON_MAX", Set.of("MATSON_MAX_TRUCK", "MATSON_MAX_PARCEL"),
|
||||
"EXX", Set.of("EXX_TRUCK", "EXX_RAIL_NY"),
|
||||
"HEDE", Set.of("HEDE_TRUCK", "HEDE_PARCEL"),
|
||||
"OA_LA", Set.of("OA_LA_TRUCK", "OA_LA_FULL_TRUCK", "OA_PARCEL"),
|
||||
"OA_CHICAGO", Set.of("OA_CHICAGO_RAIL"),
|
||||
"OA_HOUSTON", Set.of("OA_HOUSTON_TRUCK"),
|
||||
"OA_SAVANNAH", Set.of("OA_SAVANNAH_TRUCK"),
|
||||
"OA_NEW_YORK_TRUCK", Set.of("OA_NEW_YORK_TRUCK")
|
||||
);
|
||||
@Resource private SmartLoadingRepository repository;
|
||||
@Resource private OrderCargoClient orderCargoClient;
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
@@ -96,6 +108,57 @@ public class SmartLoadingService {
|
||||
return grouped.values().stream().map(this::inboundCandidate).toList();
|
||||
}
|
||||
|
||||
/** Global workbench candidates. A co-load group is one paging unit and can never be split across pages. */
|
||||
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()
|
||||
.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<>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
public PageResult<WorkbenchContainer> containers(WorkbenchContainerQuery query) {
|
||||
if (query.expectedArrivalStart() != null && query.expectedArrivalEnd() != null
|
||||
&& query.expectedArrivalStart().isAfter(query.expectedArrivalEnd())) {
|
||||
throw validation("预计到货开始日期不能晚于结束日期");
|
||||
}
|
||||
return repository.workbenchContainers(query, actualChannels(query.channelGroupCodes()));
|
||||
}
|
||||
|
||||
private InboundCandidate inboundCandidate(List<Candidate> details) {
|
||||
Candidate first = details.get(0);
|
||||
int packages = details.stream().map(Candidate::availablePackageCount)
|
||||
@@ -114,7 +177,93 @@ public class SmartLoadingService {
|
||||
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.expectedArrivalAt());
|
||||
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();
|
||||
InboundCandidate first = ordered.get(0);
|
||||
int packages = ordered.stream().map(InboundCandidate::availablePackageCount).mapToInt(Integer::intValue).sum();
|
||||
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);
|
||||
BigDecimal net = weightedAverage(ordered, InboundCandidate::unitNetWeightKg, packages);
|
||||
BigDecimal averageVolume = packages == 0 ? BigDecimal.ZERO
|
||||
: volume.divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
|
||||
List<GroupMemberCandidate> groupMembers = ordered.stream().map(member -> new GroupMemberCandidate(
|
||||
member.accountId(), member.inboundId(), member.orderId(), member.orderNo(), member.inboundCode(),
|
||||
member.customerName(), member.availablePackageCount(), member.orderStatusSnapshot(),
|
||||
member.expectedArrivalAt())).toList();
|
||||
String customers = ordered.stream().map(InboundCandidate::customerName).filter(StringUtils::hasText)
|
||||
.distinct().collect(java.util.stream.Collectors.joining("、"));
|
||||
String goods = ordered.stream().map(InboundCandidate::goodsSummary).filter(StringUtils::hasText)
|
||||
.distinct().collect(java.util.stream.Collectors.joining("、"));
|
||||
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(),
|
||||
ordered.stream().map(InboundCandidate::expectedArrivalAt).filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null),
|
||||
groupMembers);
|
||||
}
|
||||
|
||||
private static BigDecimal weightedAverage(List<InboundCandidate> members,
|
||||
java.util.function.Function<InboundCandidate, BigDecimal> getter,
|
||||
int packages) {
|
||||
if (packages == 0) return BigDecimal.ZERO;
|
||||
return members.stream().map(candidate -> unit(getter.apply(candidate)).multiply(
|
||||
BigDecimal.valueOf(candidate.availablePackageCount())))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private static Set<String> actualChannels(List<String> groups) {
|
||||
Set<String> actual = new LinkedHashSet<>();
|
||||
for (String value : groups) {
|
||||
String normalized = value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
Set<String> mapped = WORKBENCH_CHANNEL_GROUPS.get(normalized);
|
||||
if (mapped == null) throw validation("不支持的渠道分组: " + value);
|
||||
actual.addAll(mapped);
|
||||
}
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user