This commit is contained in:
2026-09-03 21:34:27 +08:00
parent 26c2d7aacd
commit a5aecf6171
4 changed files with 286 additions and 82 deletions
@@ -7,10 +7,13 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record InboundCandidateQuery( public record InboundCandidateQuery(
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes, @NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode, @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) { @Min(1) long page, @Min(1) @Max(200) long size) {
public InboundCandidateQuery { public InboundCandidateQuery {
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes); 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 (page == 0) page = 1;
if (size == 0) size = 20; if (size == 0) size = 20;
} }
@@ -24,7 +27,8 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg,
BigDecimal averageUnitVolumeCbm, BigDecimal availableVolumeCbm, BigDecimal averageUnitVolumeCbm, BigDecimal availableVolumeCbm,
Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes, 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) { List<GroupMemberCandidate> groupMembers) {
public InboundCandidate { groupMembers = groupMembers == null ? List.of() : List.copyOf(groupMembers); } public InboundCandidate { groupMembers = groupMembers == null ? List.of() : List.copyOf(groupMembers); }
public Long customerId() { return accountId; } public Long customerId() { return accountId; }
@@ -32,7 +36,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record WorkbenchContainerQuery( public record WorkbenchContainerQuery(
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes, @NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode, @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) { @Min(1) long page, @Min(1) @Max(200) long size) {
public WorkbenchContainerQuery { public WorkbenchContainerQuery {
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes); channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes);
@@ -84,37 +84,49 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
CASE WHEN current_group.id IS NULL THEN eligible.accountId END accountId, CASE WHEN current_group.id IS NULL THEN eligible.accountId END accountId,
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END inboundId, CASE WHEN current_group.id IS NULL THEN eligible.inboundId END inboundId,
MIN(eligible.expectedArrivalAt) sortArrivalAt,MIN(eligible.inboundId) sortInboundId 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 ( FROM (
SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode, 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, o.expected_arrival_at expectedArrivalAt,o.created_at createdAt,
SUM(i.package_count) 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&gt;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 -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 WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id AND li.deleted=0),0)
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 -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 WHERE o.inbound_role='MASTER' AND split_item.tenant_id=o.tenant_id
AND split_item.parent_inbound_id=o.id AND split_item.parent_inbound_id=o.id),0) availablePackageCount
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0) availablePackageCount
FROM fl_inbound_order o FROM fl_inbound_order o
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
AND m.status='APPROVED'
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
AND i.inbound_detail_id IS NOT NULL AND i.package_count&gt;0
WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId}) WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId})
AND o.channel_code IN AND o.channel_code IN
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach> <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')) 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 AND (
HAVING availablePackageCount&gt;0 o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED',
) detail 'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','WAIT_SPLIT','LOADING')
GROUP BY detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,detail.createdAt )
<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&gt;=#{query.createdStart}</if>
<if test="query.createdEnd != null">AND o.created_at&lt;DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)</if>
) eligible ) eligible
LEFT JOIN fl_coload_group_member current_member LEFT JOIN fl_coload_group_member current_member
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId 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.accountId END,
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END CASE WHEN current_group.id IS NULL THEN eligible.inboundId END
HAVING (current_group.id IS NULL OR ( HAVING (current_group.id IS NULL OR (
MIN(current_group.group_status)='ACTIVE' AND MAX(current_group.locked_plan_id) IS NULL COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
AND COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1) WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1)
AND SUM(CASE WHEN eligible.availablePackageCount=current_member.full_package_count THEN 1 ELSE 0 END)=COUNT(*)
)) ))
<if test="query.customerId != null">
AND SUM(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)&gt;0
</if>
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">
AND SUM(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)&gt;0
</if>
<if test="query.createdStart != null or query.createdEnd != null">
<if test="query.createdStart != null">AND DATE(MIN(eligible.createdAt))&gt;=#{query.createdStart}</if>
<if test="query.createdEnd != null">AND DATE(MIN(eligible.createdAt))&lt;=#{query.createdEnd}</if>
</if>
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
</script> </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, 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 (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, 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, CASE WHEN current_group.locked_plan_id IS NOT NULL OR EXISTS (
o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt 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 FROM fl_inbound_order o
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
AND m.status='APPROVED' 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 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 LEFT JOIN fl_coload_group current_group
ON current_group.id=current_member.group_id AND current_group.group_status='ACTIVE' 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' 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.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 (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 GROUP BY o.id,m.id,i.inbound_detail_id,current_group.id,current_group.group_no
HAVING availablePackageCount&gt;0 HAVING availablePackageCount&gt;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> </script>
""") """)
List<Candidate> globalCandidatesByUnits(@Param("units") List<CandidateUnit> units); 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 AND b.channel_code IN
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach> <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.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 ( AND EXISTS (
SELECT 1 FROM fl_loading_plan_item filter_item SELECT 1 FROM fl_loading_plan_item filter_item
JOIN fl_inbound_order filter_inbound ON filter_inbound.tenant_id=filter_item.account_id 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.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 &gt;= #{query.createdStart}</if> <if test="query.createdStart != null">AND filter_inbound.created_at &gt;= #{query.createdStart}</if>
<if test="query.createdEnd != null">AND filter_inbound.created_at &lt; DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)</if> <if test="query.createdEnd != null">AND filter_inbound.created_at &lt; 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> </if>
ORDER BY b.updated_at DESC,b.id DESC 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, Page<WorkbenchContainer> workbenchContainers(Page<WorkbenchContainer> page,
@Param("query") WorkbenchContainerQuery query, @Param("query") WorkbenchContainerQuery query,
@Param("actualChannels") Set<String> actualChannels, @Param("actualChannels") Set<String> actualChannels);
@Param("orderStatus") String orderStatus);
@Select(""" @Select("""
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId, 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, public PageResult<WorkbenchContainer> workbenchContainers(WorkbenchContainerQuery query,
Set<String> actualChannels) { Set<String> actualChannels) {
return PageResult.from(candidates.workbenchContainers( return PageResult.from(candidates.workbenchContainers(
new Page<>(query.page(), query.size()), query, actualChannels, new Page<>(query.page(), query.size()), query, actualChannels));
query.orderStatus() == null ? null : query.orderStatus().name()));
} }
public List<ChannelContainerCountRow> channelContainerCounts(long warehouseId) { public List<ChannelContainerCountRow> channelContainerCounts(long warehouseId) {
return candidates.channelContainerCounts(warehouseId); return candidates.channelContainerCounts(warehouseId);
@@ -68,6 +67,9 @@ public class SmartLoadingRepository {
.eq(InboundOrderEntity::getTenantId, accountId) .eq(InboundOrderEntity::getTenantId, accountId)
.eq(InboundOrderEntity::getId, inboundId)); .eq(InboundOrderEntity::getId, inboundId));
} }
public InboundOrderEntity inbound(long inboundId) {
return inbounds.selectById(inboundId);
}
public InboundOrderEntity inboundForUpdate(long accountId, long inboundId) { public InboundOrderEntity inboundForUpdate(long accountId, long inboundId) {
return inbounds.selectOne(new LambdaQueryWrapper<InboundOrderEntity>() return inbounds.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, accountId) .eq(InboundOrderEntity::getTenantId, accountId)
@@ -121,12 +121,34 @@ public class SmartLoadingService {
throw validation("创建开始日期不能晚于结束日期"); throw validation("创建开始日期不能晚于结束日期");
} }
Set<String> actualChannels = actualChannels(query.channelGroupCodes()); Set<String> actualChannels = actualChannels(query.channelGroupCodes());
if (query.orderStatus() == CandidateOrderStatus.PENDING_REVIEW) { EnumSet<CandidateOrderStatus> selected = query.orderStatuses().isEmpty()
return pendingReviewCandidates(query, actualChannels); ? 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) { long firstlegOffset = Math.max(0, offset - pendingCount.total());
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0); 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); PageResult<CandidateUnit> unitPage = repository.globalCandidateUnits(query, actualChannels);
if (unitPage.records().isEmpty()) { if (unitPage.records().isEmpty()) {
return new PageResult<>(List.of(), unitPage.page(), unitPage.size(), unitPage.total(), unitPage.pages()); 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, private PageResult<InboundCandidate> pendingReviewCandidates(InboundCandidateQuery query,
Set<String> actualChannels) { Set<String> actualChannels,
long page, long size) {
ApiResponse<PageResult<PendingReviewOrderClient.Row>> response; ApiResponse<PageResult<PendingReviewOrderClient.Row>> response;
try { try {
response = pendingReviewOrders.page(internalToken, List.copyOf(actualChannels), query.warehouseId(), response = pendingReviewOrders.page(internalToken, List.copyOf(actualChannels), query.warehouseId(),
query.customerId(), query.inboundCode(), query.createdStart(), query.createdEnd(), query.customerId(), query.inboundCode(), query.createdStart(), query.createdEnd(),
query.page(), query.size()); page, size);
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "待审核订单服务暂时不可用"); throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "待审核订单服务暂时不可用");
} }
@@ -173,11 +196,49 @@ public class SmartLoadingService {
new InboundCandidate(row.customerId(), null, row.orderId(), row.orderNo(), row.inboundCode(), new InboundCandidate(row.customerId(), null, row.orderId(), row.orderNo(), row.inboundCode(),
row.warehouseId(), row.channelCode(), row.channelName(), null, null, null, row.warehouseId(), row.channelCode(), row.channelName(), null, null, null,
row.customerName(), null, row.totalPackageCount(), null, 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(); "PENDING_REVIEW", null, List.of())).toList();
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages()); 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) { public PageResult<WorkbenchContainer> containers(WorkbenchContainerQuery query) {
if (query.createdStart() != null && query.createdEnd() != null if (query.createdStart() != null && query.createdEnd() != null
&& query.createdStart().isAfter(query.createdEnd())) { && query.createdStart().isAfter(query.createdEnd())) {
@@ -190,11 +251,12 @@ public class SmartLoadingService {
Candidate first = details.get(0); Candidate first = details.get(0);
int packages = details.stream().map(Candidate::availablePackageCount) int packages = details.stream().map(Candidate::availablePackageCount)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum(); .filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
BigDecimal volume = details.stream() boolean hasVolume = details.stream().anyMatch(detail -> detail.unitVolumeCbm() != null);
.map(detail -> unit(detail.unitVolumeCbm()).multiply(BigDecimal.valueOf( 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())))) Math.max(0, detail.availablePackageCount() == null ? 0 : detail.availablePackageCount()))))
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP); .reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP) : null;
BigDecimal averageVolume = packages == 0 ? BigDecimal.ZERO : volume.divide( BigDecimal averageVolume = packages == 0 || volume == null ? null : volume.divide(
BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP); BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
String goods = details.stream().map(Candidate::goodsName).filter(StringUtils::hasText) String goods = details.stream().map(Candidate::goodsName).filter(StringUtils::hasText)
.map(String::trim).distinct().collect(java.util.stream.Collectors.joining("、")); .map(String::trim).distinct().collect(java.util.stream.Collectors.joining("、"));
@@ -203,7 +265,7 @@ public class SmartLoadingService {
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(), first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
first.customerName(), goods, packages, first.unitGrossWeightKg(), first.unitNetWeightKg(), first.customerName(), goods, packages, first.unitGrossWeightKg(), first.unitNetWeightKg(),
averageVolume, volume, first.coLoadGroupId(), first.groupNo(), first.memberCount(), 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()); first.expectedArrivalAt(), List.of());
} }
@@ -217,11 +279,12 @@ public class SmartLoadingService {
InboundCandidate first = ordered.get(0); InboundCandidate first = ordered.get(0);
int packages = ordered.stream().map(InboundCandidate::availablePackageCount).mapToInt(Integer::intValue).sum(); int packages = ordered.stream().map(InboundCandidate::availablePackageCount).mapToInt(Integer::intValue).sum();
if (packages != metadata.groupTotalBoxes()) return null; if (packages != metadata.groupTotalBoxes()) return null;
BigDecimal volume = ordered.stream().map(InboundCandidate::availableVolumeCbm) boolean hasVolume = ordered.stream().anyMatch(candidate -> candidate.availableVolumeCbm() != null);
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP); 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 gross = weightedAverage(ordered, InboundCandidate::unitGrossWeightKg, packages);
BigDecimal net = weightedAverage(ordered, InboundCandidate::unitNetWeightKg, 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); : volume.divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
List<GroupMemberCandidate> groupMembers = ordered.stream().map(member -> new GroupMemberCandidate( List<GroupMemberCandidate> groupMembers = ordered.stream().map(member -> new GroupMemberCandidate(
member.accountId(), member.inboundId(), member.orderId(), member.orderNo(), member.inboundCode(), 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.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(), first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
customers, goods, packages, gross, net, averageVolume, volume, metadata.coLoadGroupId(), metadata.groupNo(), 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), ordered.stream().map(InboundCandidate::expectedArrivalAt).filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null),
groupMembers); 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) { private static String unitKey(Long groupId, Long accountId, Long inboundId) {
return groupId == null ? "I|" + accountId + '|' + inboundId : "G|" + groupId; return groupId == null ? "I|" + accountId + '|' + inboundId : "G|" + groupId;
} }
@@ -247,8 +341,8 @@ public class SmartLoadingService {
private static BigDecimal weightedAverage(List<InboundCandidate> members, private static BigDecimal weightedAverage(List<InboundCandidate> members,
java.util.function.Function<InboundCandidate, BigDecimal> getter, java.util.function.Function<InboundCandidate, BigDecimal> getter,
int packages) { int packages) {
if (packages == 0) return BigDecimal.ZERO; if (packages == 0 || members.stream().map(getter).anyMatch(Objects::isNull)) return null;
return members.stream().map(candidate -> unit(getter.apply(candidate)).multiply( return members.stream().map(candidate -> getter.apply(candidate).multiply(
BigDecimal.valueOf(candidate.availablePackageCount()))) BigDecimal.valueOf(candidate.availablePackageCount())))
.reduce(BigDecimal.ZERO, BigDecimal::add) .reduce(BigDecimal.ZERO, BigDecimal::add)
.divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP); .divide(BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
@@ -466,6 +560,13 @@ public class SmartLoadingService {
Plan replay = replay(operation, planId, "ADD_INBOUND", null, requestHash); Plan replay = replay(operation, planId, "ADD_INBOUND", null, requestHash);
if (replay != null) return replay; 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); LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId()); ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
List<Candidate> details = candidates(plan.getContainerBookingId(), booking.getWarehouseId()).stream() List<Candidate> details = candidates(plan.getContainerBookingId(), booking.getWarehouseId()).stream()
@@ -497,6 +598,66 @@ public class SmartLoadingService {
return view(assigned, booking); 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) { List<InboundAllocation> proportionalAllocations(List<Candidate> details, int selected) {
int available = details.stream().map(Candidate::availablePackageCount).filter(Objects::nonNull) int available = details.stream().map(Candidate::availablePackageCount).filter(Objects::nonNull)
.mapToInt(Integer::intValue).sum(); .mapToInt(Integer::intValue).sum();
@@ -1075,6 +1236,12 @@ public class SmartLoadingService {
private Candidate declaredCandidate(PendingReviewOrderClient.Row pending, private Candidate declaredCandidate(PendingReviewOrderClient.Row pending,
com.baiyue.firstleg.dto.OrderApprovedInboundCommand snapshot, com.baiyue.firstleg.dto.OrderApprovedInboundCommand snapshot,
long inboundId, OrderCargoClient.CargoBinding cargo) { 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(); int boxes = cargo.originalPackageCount() == null ? 0 : cargo.originalPackageCount();
if (boxes <= 0) throw conflict("订单货物箱数不完整,请先修正订单"); if (boxes <= 0) throw conflict("订单货物箱数不完整,请先修正订单");
BigDecimal divisor = BigDecimal.valueOf(boxes); BigDecimal divisor = BigDecimal.valueOf(boxes);
@@ -1086,7 +1253,7 @@ public class SmartLoadingService {
snapshot.channelName(), snapshot.destinationWarehouseId(), snapshot.destinationCode(), snapshot.channelName(), snapshot.destinationWarehouseId(), snapshot.destinationCode(),
snapshot.destinationName(), snapshot.customerName(), cargo.cargoName(), boxes, snapshot.destinationName(), snapshot.customerName(), cargo.cargoName(), boxes,
unitGross, unitNet, unitVolume, null, null, null, null, false, null, unitGross, unitNet, unitVolume, null, null, null, null, false, null,
pending.mainStatus(), snapshot.expectedArrivalAt()); candidateStatus, snapshot.expectedArrivalAt());
} }
private record DeclaredCargo(OrderCargoClient.CargoBinding cargo) {} private record DeclaredCargo(OrderCargoClient.CargoBinding cargo) {}