优化
This commit is contained in:
@@ -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<GroupMemberCandidate> 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);
|
||||
|
||||
+89
-58
@@ -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
|
||||
<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,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
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
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')
|
||||
)
|
||||
<if test="query.orderStatuses != null and !query.orderStatuses.isEmpty()">
|
||||
AND (
|
||||
<foreach collection="query.orderStatuses" item="status" separator=" OR ">
|
||||
<choose>
|
||||
<when test="status.name() == 'WAIT_MEASURE'">o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED','WAIT_PRICING','FEE_CONFIRMING','INBOUNDING')</when>
|
||||
<when test="status.name() == 'WAIT_SPLIT'">o.process_status='WAIT_SPLIT'</when>
|
||||
<when test="status.name() == 'LOADING'">o.process_status='LOADING'</when>
|
||||
<otherwise>1=0</otherwise>
|
||||
</choose>
|
||||
</foreach>
|
||||
)
|
||||
</if>
|
||||
<if test="query.customerId != null">AND o.tenant_id=#{query.customerId}</if>
|
||||
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">AND LOWER(o.inbound_code) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%')</if>
|
||||
<if test="query.createdStart != null">AND o.created_at>=#{query.createdStart}</if>
|
||||
<if test="query.createdEnd != null">AND o.created_at<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)</if>
|
||||
) 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(*)
|
||||
))
|
||||
<if test="query.customerId != null">
|
||||
AND SUM(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)>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)>0
|
||||
</if>
|
||||
<if test="query.createdStart != null or query.createdEnd != null">
|
||||
<if test="query.createdStart != null">AND DATE(MIN(eligible.createdAt))>=#{query.createdStart}</if>
|
||||
<if test="query.createdEnd != null">AND DATE(MIN(eligible.createdAt))<=#{query.createdEnd}</if>
|
||||
</if>
|
||||
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
|
||||
</script>
|
||||
""")
|
||||
@@ -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 (
|
||||
<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>
|
||||
)
|
||||
ORDER BY inboundId ASC,inboundDetailId ASC
|
||||
</script>
|
||||
""")
|
||||
List<Candidate> globalCandidatesByUnits(@Param("units") List<CandidateUnit> units);
|
||||
@@ -216,7 +256,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
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.createdStart != null or query.createdEnd != null or query.orderStatus != null">
|
||||
<if test="query.customerId != null or query.inboundCode != null or query.createdStart != null or query.createdEnd != 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
|
||||
@@ -226,14 +266,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
<if test="query.inboundCode != null and query.inboundCode != ''">AND filter_item.inbound_code LIKE CONCAT('%',#{query.inboundCode},'%')</if>
|
||||
<if test="query.createdStart != null">AND filter_inbound.created_at >= #{query.createdStart}</if>
|
||||
<if test="query.createdEnd != null">AND filter_inbound.created_at < DATE_ADD(#{query.createdEnd},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
|
||||
@@ -241,8 +273,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
""")
|
||||
Page<WorkbenchContainer> workbenchContainers(Page<WorkbenchContainer> page,
|
||||
@Param("query") WorkbenchContainerQuery query,
|
||||
@Param("actualChannels") Set<String> actualChannels,
|
||||
@Param("orderStatus") String orderStatus);
|
||||
@Param("actualChannels") Set<String> actualChannels);
|
||||
|
||||
@Select("""
|
||||
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
|
||||
|
||||
@@ -51,8 +51,7 @@ public class SmartLoadingRepository {
|
||||
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()));
|
||||
new Page<>(query.page(), query.size()), query, actualChannels));
|
||||
}
|
||||
public List<ChannelContainerCountRow> 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<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, accountId)
|
||||
|
||||
@@ -121,12 +121,34 @@ public class SmartLoadingService {
|
||||
throw validation("创建开始日期不能晚于结束日期");
|
||||
}
|
||||
Set<String> actualChannels = actualChannels(query.channelGroupCodes());
|
||||
if (query.orderStatus() == CandidateOrderStatus.PENDING_REVIEW) {
|
||||
return pendingReviewCandidates(query, actualChannels);
|
||||
EnumSet<CandidateOrderStatus> 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<InboundCandidate> pendingCount = pendingReviewCandidates(query, actualChannels, 1, 1);
|
||||
PageResult<InboundCandidate> firstlegCount = firstlegCandidates(firstlegQuery, actualChannels);
|
||||
long total = pendingCount.total() + firstlegCount.total();
|
||||
long offset = Math.multiplyExact(query.page() - 1, query.size());
|
||||
List<InboundCandidate> 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<InboundCandidate> firstlegCandidates(InboundCandidateQuery query,
|
||||
Set<String> actualChannels) {
|
||||
PageResult<CandidateUnit> 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<InboundCandidate> pendingReviewCandidates(InboundCandidateQuery query,
|
||||
Set<String> actualChannels) {
|
||||
Set<String> actualChannels,
|
||||
long page, long size) {
|
||||
ApiResponse<PageResult<PendingReviewOrderClient.Row>> 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<InboundCandidate> pendingReviewRange(InboundCandidateQuery query, Set<String> channels,
|
||||
long offset, long limit) {
|
||||
return pageRange(offset, limit, (page, size) -> pendingReviewCandidates(query, channels, page, size));
|
||||
}
|
||||
|
||||
private List<InboundCandidate> firstlegRange(InboundCandidateQuery query, Set<String> channels,
|
||||
List<CandidateOrderStatus> statuses,
|
||||
long offset, long limit) {
|
||||
return pageRange(offset, limit, (page, size) -> firstlegCandidates(
|
||||
withStatuses(query, statuses, page, size), channels));
|
||||
}
|
||||
|
||||
private static List<InboundCandidate> pageRange(long offset, long limit,
|
||||
java.util.function.BiFunction<Long, Long, PageResult<InboundCandidate>> source) {
|
||||
if (limit <= 0) return List.of();
|
||||
final long chunk = 200;
|
||||
long page = offset / chunk + 1;
|
||||
int skip = Math.toIntExact(offset % chunk);
|
||||
List<InboundCandidate> result = new ArrayList<>();
|
||||
while (result.size() < limit) {
|
||||
List<InboundCandidate> 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<CandidateOrderStatus> 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<WorkbenchContainer> 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<GroupMemberCandidate> 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<InboundCandidate> 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<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(
|
||||
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<Candidate> 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<DeclaredCargo> declared = loadDeclaredCargo(locked.getTenantId(), locked.getOrderId());
|
||||
List<Candidate> candidates = declared.stream().map(value -> declaredCandidate(
|
||||
order, locked.getId(), value.cargo(), "WAIT_MEASURE")).toList();
|
||||
List<InboundAllocation> allocations = proportionalAllocations(candidates, request.packageCount());
|
||||
List<LoadingPlanItemEntity> current = new ArrayList<>(repository.items(planId));
|
||||
List<LoadingPlanItemEntity> 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<InboundAllocation> proportionalAllocations(List<Candidate> 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) {}
|
||||
|
||||
Reference in New Issue
Block a user