优化
This commit is contained in:
@@ -2541,6 +2541,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
KEY idx_inbound_arrival (tenant_id, expected_arrival_at),
|
||||
KEY idx_inbound_destination (tenant_id, destination_code, created_at),
|
||||
KEY idx_inbound_channel (tenant_id, channel_code, created_at),
|
||||
KEY idx_inbound_smart_loading (warehouse_id, process_status, channel_code, expected_arrival_at, id),
|
||||
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
||||
|
||||
@@ -2721,6 +2722,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement (
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_measure_external_event (tenant_id, source_code, external_event_no),
|
||||
KEY idx_measure_inbound_time (tenant_id, inbound_id, created_at),
|
||||
KEY idx_measure_inbound_status_id (tenant_id, inbound_id, status, id),
|
||||
CONSTRAINT fk_measure_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='入库长宽高测量批次和第三方原始数据';
|
||||
|
||||
@@ -2763,6 +2765,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
UNIQUE KEY uk_measure_item_no (tenant_id, measurement_id, item_no),
|
||||
UNIQUE KEY uk_measure_external_detail (tenant_id, inbound_detail_id, external_detail_no),
|
||||
UNIQUE KEY uk_measure_box_code (tenant_id, box_code),
|
||||
KEY idx_measure_item_candidate (tenant_id, measurement_id, inbound_detail_id),
|
||||
CONSTRAINT fk_measure_item_batch FOREIGN KEY (measurement_id) REFERENCES fl_inbound_measurement (id)
|
||||
) ENGINE=InnoDB COMMENT='入库长宽高和箱数测量明细';
|
||||
|
||||
@@ -5136,6 +5139,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
KEY idx_loading_item_plan (loading_plan_id, deleted, load_sequence, id),
|
||||
KEY idx_loading_item_account (account_id, created_at, id),
|
||||
KEY idx_loading_item_inbound (account_id, inbound_id, deleted),
|
||||
KEY idx_loading_item_candidate (account_id, inbound_id, inbound_detail_id, deleted),
|
||||
KEY idx_loading_item_order (account_id, order_id, deleted),
|
||||
CONSTRAINT fk_loading_item_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||
CONSTRAINT chk_loading_item_account CHECK (account_id > 0),
|
||||
@@ -8455,6 +8459,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item (
|
||||
UNIQUE KEY uk_inbound_split_batch_line (tenant_id, split_batch_id, line_no),
|
||||
UNIQUE KEY uk_inbound_split_batch_source_child (tenant_id, split_batch_id, source_measurement_item_id, child_inbound_id),
|
||||
KEY idx_inbound_split_detail (tenant_id, source_inbound_detail_id, created_at),
|
||||
KEY idx_inbound_split_candidate (tenant_id, parent_inbound_id, source_inbound_detail_id),
|
||||
CONSTRAINT chk_inbound_split_item_package CHECK (package_count > 0),
|
||||
CONSTRAINT fk_inbound_split_item_batch FOREIGN KEY (split_batch_id) REFERENCES fl_inbound_split_batch (id),
|
||||
CONSTRAINT fk_inbound_split_item_parent FOREIGN KEY (parent_inbound_id) REFERENCES fl_inbound_order (id),
|
||||
@@ -16756,6 +16761,40 @@ DROP PROCEDURE IF EXISTS queue_table_comment;
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_column_comment_target;
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_table_comment_target;
|
||||
|
||||
DROP PROCEDURE IF EXISTS baiyue_firstleg.ensure_smart_loading_candidate_indexes;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE baiyue_firstleg.ensure_smart_loading_candidate_indexes()
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema='baiyue_firstleg'
|
||||
AND table_name='fl_inbound_order' AND index_name='idx_inbound_smart_loading') THEN
|
||||
ALTER TABLE baiyue_firstleg.fl_inbound_order
|
||||
ADD KEY idx_inbound_smart_loading (warehouse_id,process_status,channel_code,expected_arrival_at,id);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema='baiyue_firstleg'
|
||||
AND table_name='fl_inbound_measurement' AND index_name='idx_measure_inbound_status_id') THEN
|
||||
ALTER TABLE baiyue_firstleg.fl_inbound_measurement
|
||||
ADD KEY idx_measure_inbound_status_id (tenant_id,inbound_id,status,id);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema='baiyue_firstleg'
|
||||
AND table_name='fl_inbound_measurement_item' AND index_name='idx_measure_item_candidate') THEN
|
||||
ALTER TABLE baiyue_firstleg.fl_inbound_measurement_item
|
||||
ADD KEY idx_measure_item_candidate (tenant_id,measurement_id,inbound_detail_id);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema='baiyue_firstleg'
|
||||
AND table_name='fl_loading_plan_item' AND index_name='idx_loading_item_candidate') THEN
|
||||
ALTER TABLE baiyue_firstleg.fl_loading_plan_item
|
||||
ADD KEY idx_loading_item_candidate (account_id,inbound_id,inbound_detail_id,deleted);
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema='baiyue_firstleg'
|
||||
AND table_name='fl_inbound_split_item' AND index_name='idx_inbound_split_candidate') THEN
|
||||
ALTER TABLE baiyue_firstleg.fl_inbound_split_item
|
||||
ADD KEY idx_inbound_split_candidate (tenant_id,parent_inbound_id,source_inbound_detail_id);
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL baiyue_firstleg.ensure_smart_loading_candidate_indexes();
|
||||
DROP PROCEDURE IF EXISTS baiyue_firstleg.ensure_smart_loading_candidate_indexes;
|
||||
|
||||
CALL baiyue_firstleg.assert_unified_final_schema();
|
||||
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
|
||||
SELECT 'PASS' AS migration_status,
|
||||
|
||||
+105
-24
@@ -1,5 +1,5 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery; import org.apache.ibatis.annotations.*; import java.util.List; import java.util.Set;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer; import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery; import org.apache.ibatis.annotations.*; import java.time.LocalDateTime; import java.util.List; import java.util.Set;
|
||||
@Mapper public interface SmartLoadingCandidateMapper {
|
||||
@Select("""
|
||||
SELECT b.channel_code channelCode,COUNT(DISTINCT b.id) availableContainerCount
|
||||
@@ -63,12 +63,83 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
List<Candidate> candidates(@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT current_group.id groupId,
|
||||
CASE WHEN current_group.id IS NULL THEN eligible.accountId END accountId,
|
||||
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END inboundId,
|
||||
MIN(eligible.expectedArrivalAt) sortArrivalAt,MIN(eligible.inboundId) sortInboundId
|
||||
FROM (
|
||||
SELECT detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,
|
||||
SUM(detail.availablePackageCount) availablePackageCount
|
||||
FROM (
|
||||
SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode,
|
||||
o.expected_arrival_at expectedArrivalAt,i.inbound_detail_id,
|
||||
SUM(i.package_count)
|
||||
-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
|
||||
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
|
||||
-COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
|
||||
WHERE o.inbound_role='MASTER' AND split_item.tenant_id=o.tenant_id
|
||||
AND split_item.parent_inbound_id=o.id
|
||||
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0) availablePackageCount
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
|
||||
AND m.status='APPROVED'
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
AND i.inbound_detail_id IS NOT NULL AND i.package_count>0
|
||||
WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId})
|
||||
AND o.channel_code IN
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
AND o.process_status='WAIT_SPLIT'
|
||||
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
GROUP BY o.tenant_id,o.id,o.inbound_code,o.expected_arrival_at,i.inbound_detail_id
|
||||
HAVING availablePackageCount>0
|
||||
) detail
|
||||
GROUP BY detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt
|
||||
) eligible
|
||||
LEFT JOIN fl_coload_group_member current_member
|
||||
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
|
||||
AND current_member.active_flag=1
|
||||
LEFT JOIN fl_coload_group current_group
|
||||
ON current_group.id=current_member.group_id AND current_group.group_status IN ('ACTIVE','LOCKED')
|
||||
GROUP BY current_group.id,
|
||||
CASE WHEN current_group.id IS NULL THEN eligible.accountId END,
|
||||
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END
|
||||
HAVING (current_group.id IS NULL OR (
|
||||
MIN(current_group.group_status)='ACTIVE' AND MAX(current_group.locked_plan_id) IS NULL
|
||||
AND COUNT(*)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
|
||||
WHERE group_member_count.group_id=current_group.id AND group_member_count.active_flag=1)
|
||||
AND SUM(CASE WHEN eligible.availablePackageCount=current_member.full_package_count THEN 1 ELSE 0 END)=COUNT(*)
|
||||
))
|
||||
<if test="query.customerId != null">
|
||||
AND SUM(CASE WHEN eligible.accountId=#{query.customerId} THEN 1 ELSE 0 END)>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.expectedArrivalStart != null or query.expectedArrivalEnd != null">
|
||||
AND SUM(CASE WHEN eligible.expectedArrivalAt IS NOT NULL
|
||||
<if test="query.expectedArrivalStart != null">AND DATE(eligible.expectedArrivalAt)>=#{query.expectedArrivalStart}</if>
|
||||
<if test="query.expectedArrivalEnd != null">AND DATE(eligible.expectedArrivalAt)<=#{query.expectedArrivalEnd}</if>
|
||||
THEN 1 ELSE 0 END)>0
|
||||
</if>
|
||||
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
|
||||
</script>
|
||||
""")
|
||||
Page<CandidateUnit> globalCandidateUnits(Page<CandidateUnit> page,
|
||||
@Param("query") InboundCandidateQuery query,@Param("actualChannels") Set<String> actualChannels);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
|
||||
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
|
||||
o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId,
|
||||
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
|
||||
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
|
||||
i.package_count-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
SUM(i.package_count)-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
|
||||
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
|
||||
-COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
|
||||
@@ -77,36 +148,44 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0) availablePackageCount,
|
||||
COALESCE(o.actual_gross_weight_kg / NULLIF(o.total_package_count, 0), 0) unitGrossWeightKg,
|
||||
COALESCE(o.actual_net_weight_kg / NULLIF(o.total_package_count, 0), 0) unitNetWeightKg,
|
||||
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm,
|
||||
NULL coLoadGroupId,NULL groupNo,NULL memberCount,NULL groupTotalBoxes,
|
||||
NULL locked,NULL blockReason,o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt
|
||||
COALESCE(SUM(COALESCE(i.total_volume_cbm,COALESCE(i.unit_volume_cbm,0)*i.package_count))
|
||||
/ NULLIF(SUM(i.package_count),0),0) unitVolumeCbm,
|
||||
current_group.id coLoadGroupId,current_group.group_no groupNo,
|
||||
(SELECT COUNT(*) FROM fl_coload_group_member member_count
|
||||
WHERE member_count.group_id=current_group.id AND member_count.active_flag=1) memberCount,
|
||||
(SELECT SUM(member_boxes.full_package_count) FROM fl_coload_group_member member_boxes
|
||||
WHERE member_boxes.group_id=current_group.id AND member_boxes.active_flag=1) groupTotalBoxes,
|
||||
CASE WHEN current_group.id IS NULL THEN NULL ELSE FALSE END locked,NULL blockReason,
|
||||
o.order_status_snapshot orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
|
||||
AND m.status='APPROVED'
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN (
|
||||
SELECT tenant_id,measurement_id,inbound_detail_id,SUM(package_count) package_count,
|
||||
COALESCE(SUM(COALESCE(total_volume_cbm,COALESCE(unit_volume_cbm,0)*package_count))
|
||||
/ NULLIF(SUM(package_count),0),0) unit_volume_cbm
|
||||
FROM fl_inbound_measurement_item
|
||||
WHERE inbound_detail_id IS NOT NULL AND package_count>0
|
||||
GROUP BY tenant_id,measurement_id,inbound_detail_id
|
||||
) i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE (#{warehouseId} IS NULL OR o.warehouse_id=#{warehouseId})
|
||||
AND o.process_status='WAIT_SPLIT'
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
AND i.inbound_detail_id IS NOT NULL AND i.package_count>0
|
||||
LEFT JOIN fl_coload_group_member current_member
|
||||
ON current_member.tenant_id=o.tenant_id AND current_member.inbound_id=o.id AND current_member.active_flag=1
|
||||
LEFT JOIN fl_coload_group current_group
|
||||
ON current_group.id=current_member.group_id AND current_group.group_status='ACTIVE'
|
||||
AND current_group.locked_plan_id IS NULL
|
||||
WHERE o.process_status='WAIT_SPLIT'
|
||||
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
|
||||
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
|
||||
+COALESCE((SELECT SUM(split_item.package_count) FROM fl_inbound_split_item split_item
|
||||
WHERE o.inbound_role='MASTER' AND split_item.tenant_id=o.tenant_id
|
||||
AND split_item.parent_inbound_id=o.id
|
||||
AND split_item.source_inbound_detail_id=i.inbound_detail_id),0)
|
||||
ORDER BY o.inbounded_at ASC, o.id ASC, i.inbound_detail_id ASC
|
||||
AND (
|
||||
<foreach collection="units" item="unit" separator=" OR ">
|
||||
<choose>
|
||||
<when test="unit.groupId != null">current_group.id=#{unit.groupId}</when>
|
||||
<otherwise>(current_group.id IS NULL AND o.tenant_id=#{unit.accountId} AND o.id=#{unit.inboundId})</otherwise>
|
||||
</choose>
|
||||
</foreach>
|
||||
)
|
||||
GROUP BY o.id,m.id,i.inbound_detail_id,current_group.id,current_group.group_no
|
||||
HAVING availablePackageCount>0
|
||||
ORDER BY o.inbounded_at ASC,o.id ASC,i.inbound_detail_id ASC
|
||||
</script>
|
||||
""")
|
||||
List<Candidate> globalCandidates(@Param("warehouseId") Long warehouseId);
|
||||
List<Candidate> globalCandidatesByUnits(@Param("units") List<CandidateUnit> units);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
@@ -205,4 +284,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
private String channelCode;
|
||||
private Long availableContainerCount;
|
||||
}
|
||||
record CandidateUnit(Long groupId,Long accountId,Long inboundId,
|
||||
LocalDateTime sortArrivalAt,Long sortInboundId) {}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
@@ -17,6 +18,7 @@ import com.baiyue.firstleg.mapper.LoadingPlanItemMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanMapper;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -37,7 +39,14 @@ public class SmartLoadingRepository {
|
||||
@Resource private InboundOrderMapper inbounds;
|
||||
|
||||
public List<Candidate> candidates(long warehouseId) { return candidates.candidates(warehouseId); }
|
||||
public List<Candidate> globalCandidates(Long warehouseId) { return candidates.globalCandidates(warehouseId); }
|
||||
public PageResult<CandidateUnit> globalCandidateUnits(InboundCandidateQuery query,
|
||||
Set<String> actualChannels) {
|
||||
return PageResult.from(candidates.globalCandidateUnits(
|
||||
new Page<>(query.page(), query.size()), query, actualChannels));
|
||||
}
|
||||
public List<Candidate> globalCandidatesByUnits(List<CandidateUnit> units) {
|
||||
return units.isEmpty() ? List.of() : candidates.globalCandidatesByUnits(units);
|
||||
}
|
||||
public PageResult<WorkbenchContainer> workbenchContainers(WorkbenchContainerQuery query,
|
||||
Set<String> actualChannels) {
|
||||
return PageResult.from(candidates.workbenchContainers(
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
|
||||
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
|
||||
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
||||
import com.baiyue.firstleg.split.service.SplitPreloadService;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
@@ -109,46 +110,42 @@ public class SmartLoadingService {
|
||||
}
|
||||
|
||||
/** Global workbench candidates. A co-load group is one paging unit and can never be split across pages. */
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<InboundCandidate> inboundCandidates(InboundCandidateQuery query) {
|
||||
if (query.expectedArrivalStart() != null && query.expectedArrivalEnd() != null
|
||||
&& query.expectedArrivalStart().isAfter(query.expectedArrivalEnd())) {
|
||||
throw validation("预计到货开始日期不能晚于结束日期");
|
||||
}
|
||||
Set<String> actualChannels = actualChannels(query.channelGroupCodes());
|
||||
List<InboundCandidate> all = repository.globalCandidates(query.warehouseId()).stream()
|
||||
if (query.orderStatus() != null && query.orderStatus() != CandidateOrderStatus.WAIT_SPLIT) {
|
||||
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
|
||||
}
|
||||
PageResult<CandidateUnit> unitPage = repository.globalCandidateUnits(query, actualChannels);
|
||||
if (unitPage.records().isEmpty()) {
|
||||
return new PageResult<>(List.of(), unitPage.page(), unitPage.size(), unitPage.total(), unitPage.pages());
|
||||
}
|
||||
List<InboundCandidate> all = repository.globalCandidatesByUnits(unitPage.records()).stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
|
||||
java.util.stream.Collectors.toList()))
|
||||
.values().stream().map(this::inboundCandidate).toList();
|
||||
Map<String, GroupSnapshot> snapshots = new HashMap<>();
|
||||
Map<Long, List<InboundCandidate>> grouped = new LinkedHashMap<>();
|
||||
List<InboundCandidate> standalone = new ArrayList<>();
|
||||
Map<String, InboundCandidate> units = new HashMap<>();
|
||||
Map<Long, List<InboundCandidate>> grouped = all.stream()
|
||||
.filter(candidate -> candidate.coLoadGroupId() != null)
|
||||
.collect(java.util.stream.Collectors.groupingBy(InboundCandidate::coLoadGroupId,
|
||||
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||
for (InboundCandidate candidate : all) {
|
||||
GroupSnapshot group = snapshots.computeIfAbsent(candidate.accountId() + "|" + candidate.inboundId(),
|
||||
ignored -> group(candidate.accountId(), candidate.inboundId()));
|
||||
if (group == null) {
|
||||
standalone.add(candidate);
|
||||
} else {
|
||||
grouped.computeIfAbsent(group.groupId(), ignored -> new ArrayList<>()).add(candidate);
|
||||
if (candidate.coLoadGroupId() == null) {
|
||||
units.put(unitKey(null, candidate.accountId(), candidate.inboundId()), candidate);
|
||||
}
|
||||
}
|
||||
List<InboundCandidate> units = new ArrayList<>(standalone);
|
||||
for (List<InboundCandidate> members : grouped.values()) {
|
||||
GroupSnapshot group = snapshots.get(members.get(0).accountId() + "|" + members.get(0).inboundId());
|
||||
InboundCandidate aggregate = globalGroupCandidate(group, members);
|
||||
if (aggregate != null) units.add(aggregate);
|
||||
for (var entry : grouped.entrySet()) {
|
||||
InboundCandidate aggregate = globalGroupCandidate(entry.getValue());
|
||||
if (aggregate != null) units.put(unitKey(entry.getKey(), null, null), aggregate);
|
||||
}
|
||||
units = units.stream().filter(candidate -> matches(query, actualChannels, candidate))
|
||||
.sorted(Comparator.comparing(InboundCandidate::expectedArrivalAt,
|
||||
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(InboundCandidate::inboundId, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.toList();
|
||||
long total = units.size();
|
||||
long offset = query.page() - 1 > Long.MAX_VALUE / query.size()
|
||||
? Long.MAX_VALUE : (query.page() - 1) * query.size();
|
||||
long from = Math.min(total, offset);
|
||||
long to = Math.min(total, from + query.size());
|
||||
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
|
||||
return new PageResult<>(units.subList((int) from, (int) to), query.page(), query.size(), total, pages);
|
||||
List<InboundCandidate> records = unitPage.records().stream()
|
||||
.map(unit -> units.get(unitKey(unit.groupId(), unit.accountId(), unit.inboundId())))
|
||||
.filter(Objects::nonNull).toList();
|
||||
return new PageResult<>(records, unitPage.page(), unitPage.size(), unitPage.total(), unitPage.pages());
|
||||
}
|
||||
|
||||
public PageResult<WorkbenchContainer> containers(WorkbenchContainerQuery query) {
|
||||
@@ -180,18 +177,16 @@ public class SmartLoadingService {
|
||||
first.expectedArrivalAt(), List.of());
|
||||
}
|
||||
|
||||
private InboundCandidate globalGroupCandidate(GroupSnapshot group, List<InboundCandidate> availableMembers) {
|
||||
if (group == null || !"ACTIVE".equals(group.status()) || group.lockedPlanId() != null) return null;
|
||||
Map<Long, InboundCandidate> byInbound = availableMembers.stream()
|
||||
.collect(java.util.stream.Collectors.toMap(InboundCandidate::inboundId, value -> value, (a, b) -> a));
|
||||
for (Member member : group.members()) {
|
||||
InboundCandidate candidate = byInbound.get(member.inboundId());
|
||||
if (candidate == null || !Objects.equals(candidate.accountId(), member.tenantId())
|
||||
|| !Objects.equals(candidate.availablePackageCount(), member.fullPackageCount())) return null;
|
||||
}
|
||||
List<InboundCandidate> ordered = group.members().stream().map(member -> byInbound.get(member.inboundId())).toList();
|
||||
private InboundCandidate globalGroupCandidate(List<InboundCandidate> availableMembers) {
|
||||
if (availableMembers.isEmpty()) return null;
|
||||
InboundCandidate metadata = availableMembers.get(0);
|
||||
if (metadata.coLoadGroupId() == null || metadata.memberCount() == null
|
||||
|| metadata.groupTotalBoxes() == null || availableMembers.size() != metadata.memberCount()) return null;
|
||||
List<InboundCandidate> ordered = availableMembers.stream()
|
||||
.sorted(Comparator.comparing(InboundCandidate::inboundId)).toList();
|
||||
InboundCandidate first = ordered.get(0);
|
||||
int packages = ordered.stream().map(InboundCandidate::availablePackageCount).mapToInt(Integer::intValue).sum();
|
||||
if (packages != metadata.groupTotalBoxes()) return null;
|
||||
BigDecimal volume = ordered.stream().map(InboundCandidate::availableVolumeCbm)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.HALF_UP);
|
||||
BigDecimal gross = weightedAverage(ordered, InboundCandidate::unitGrossWeightKg, packages);
|
||||
@@ -209,12 +204,16 @@ public class SmartLoadingService {
|
||||
return new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
|
||||
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
|
||||
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
|
||||
customers, goods, packages, gross, net, averageVolume, volume, group.groupId(), group.groupNo(),
|
||||
group.members().size(), group.fullPackageCount(), false, null, first.orderStatusSnapshot(),
|
||||
customers, goods, packages, gross, net, averageVolume, volume, metadata.coLoadGroupId(), metadata.groupNo(),
|
||||
metadata.memberCount(), metadata.groupTotalBoxes(), false, null, first.orderStatusSnapshot(),
|
||||
ordered.stream().map(InboundCandidate::expectedArrivalAt).filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null),
|
||||
groupMembers);
|
||||
}
|
||||
|
||||
private static String unitKey(Long groupId, Long accountId, Long inboundId) {
|
||||
return groupId == null ? "I|" + accountId + '|' + inboundId : "G|" + groupId;
|
||||
}
|
||||
|
||||
private static BigDecimal weightedAverage(List<InboundCandidate> members,
|
||||
java.util.function.Function<InboundCandidate, BigDecimal> getter,
|
||||
int packages) {
|
||||
@@ -236,36 +235,6 @@ public class SmartLoadingService {
|
||||
return actual;
|
||||
}
|
||||
|
||||
private static boolean matches(InboundCandidateQuery query, Set<String> actualChannels,
|
||||
InboundCandidate candidate) {
|
||||
if (!actualChannels.contains(candidate.channelCode())) return false;
|
||||
if (query.customerId() != null && !Objects.equals(candidate.accountId(), query.customerId())
|
||||
&& candidate.groupMembers().stream().noneMatch(
|
||||
member -> Objects.equals(member.accountId(), query.customerId()))) return false;
|
||||
String inboundCode = trim(query.inboundCode());
|
||||
if (StringUtils.hasText(inboundCode) && candidate.groupMembers().stream()
|
||||
.map(GroupMemberCandidate::inboundCode).filter(Objects::nonNull)
|
||||
.noneMatch(value -> value.toLowerCase(Locale.ROOT).contains(inboundCode.toLowerCase(Locale.ROOT)))
|
||||
&& (candidate.inboundCode() == null || !candidate.inboundCode().toLowerCase(Locale.ROOT)
|
||||
.contains(inboundCode.toLowerCase(Locale.ROOT)))) return false;
|
||||
if (query.expectedArrivalStart() != null || query.expectedArrivalEnd() != null) {
|
||||
boolean arrivalMatches = candidate.groupMembers().isEmpty()
|
||||
? inArrivalRange(candidate.expectedArrivalAt(), query)
|
||||
: candidate.groupMembers().stream().anyMatch(member -> inArrivalRange(
|
||||
member.expectedArrivalAt(), query));
|
||||
if (!arrivalMatches) return false;
|
||||
}
|
||||
return query.orderStatus() == null || query.orderStatus() == CandidateOrderStatus.WAIT_SPLIT;
|
||||
}
|
||||
|
||||
private static boolean inArrivalRange(LocalDateTime arrival, InboundCandidateQuery query) {
|
||||
if (arrival == null) return false;
|
||||
return (query.expectedArrivalStart() == null
|
||||
|| !arrival.toLocalDate().isBefore(query.expectedArrivalStart()))
|
||||
&& (query.expectedArrivalEnd() == null
|
||||
|| !arrival.toLocalDate().isAfter(query.expectedArrivalEnd()));
|
||||
}
|
||||
|
||||
public Plan draftByBooking(long bookingId) {
|
||||
ContainerBookingEntity booking = requireBooking(bookingId);
|
||||
LoadingPlanEntity plan = repository.planByBooking(bookingId);
|
||||
|
||||
@@ -75,6 +75,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
KEY idx_inbound_arrival (tenant_id, expected_arrival_at),
|
||||
KEY idx_inbound_destination (tenant_id, destination_code, created_at),
|
||||
KEY idx_inbound_channel (tenant_id, channel_code, created_at),
|
||||
KEY idx_inbound_smart_loading (warehouse_id, process_status, channel_code, expected_arrival_at, id),
|
||||
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
||||
|
||||
@@ -245,6 +246,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement (
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_measure_external_event (tenant_id, source_code, external_event_no),
|
||||
KEY idx_measure_inbound_time (tenant_id, inbound_id, created_at),
|
||||
KEY idx_measure_inbound_status_id (tenant_id, inbound_id, status, id),
|
||||
CONSTRAINT fk_measure_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
) ENGINE=InnoDB COMMENT='入库长宽高测量批次和第三方原始数据';
|
||||
|
||||
@@ -285,6 +287,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
UNIQUE KEY uk_measure_item_no (tenant_id, measurement_id, item_no),
|
||||
UNIQUE KEY uk_measure_external_detail (tenant_id, inbound_detail_id, external_detail_no),
|
||||
UNIQUE KEY uk_measure_box_code (tenant_id, box_code),
|
||||
KEY idx_measure_item_candidate (tenant_id, measurement_id, inbound_detail_id),
|
||||
CONSTRAINT fk_measure_item_batch FOREIGN KEY (measurement_id) REFERENCES fl_inbound_measurement (id)
|
||||
) ENGINE=InnoDB COMMENT='入库长宽高和箱数测量明细';
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item (
|
||||
UNIQUE KEY uk_inbound_split_batch_line (tenant_id, split_batch_id, line_no),
|
||||
UNIQUE KEY uk_inbound_split_batch_source_child (tenant_id, split_batch_id, source_measurement_item_id, child_inbound_id),
|
||||
KEY idx_inbound_split_detail (tenant_id, source_inbound_detail_id, created_at),
|
||||
KEY idx_inbound_split_candidate (tenant_id, parent_inbound_id, source_inbound_detail_id),
|
||||
CONSTRAINT chk_inbound_split_item_package CHECK (package_count > 0),
|
||||
CONSTRAINT fk_inbound_split_item_batch FOREIGN KEY (split_batch_id) REFERENCES fl_inbound_split_batch (id),
|
||||
CONSTRAINT fk_inbound_split_item_parent FOREIGN KEY (parent_inbound_id) REFERENCES fl_inbound_order (id),
|
||||
|
||||
@@ -301,6 +301,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item (
|
||||
KEY idx_loading_item_plan (loading_plan_id, deleted, load_sequence, id),
|
||||
KEY idx_loading_item_account (account_id, created_at, id),
|
||||
KEY idx_loading_item_inbound (account_id, inbound_id, deleted),
|
||||
KEY idx_loading_item_candidate (account_id, inbound_id, inbound_detail_id, deleted),
|
||||
KEY idx_loading_item_order (account_id, order_id, deleted),
|
||||
CONSTRAINT fk_loading_item_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||
CONSTRAINT chk_loading_item_account CHECK (account_id > 0),
|
||||
|
||||
+49
-5
@@ -3,12 +3,17 @@ package com.baiyue.firstleg.repository;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -53,15 +58,54 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalCandidateQueryHasOptionalWarehouseAndNoContainerJoin() throws Exception {
|
||||
Method list = SmartLoadingCandidateMapper.class.getDeclaredMethod("globalCandidates", Long.class);
|
||||
String sql = String.join(" ", list.getAnnotation(Select.class).value());
|
||||
void globalCandidateQueryPagesUnitsBeforeLoadingDetailsAndHasNoContainerJoin() throws Exception {
|
||||
Method units = SmartLoadingCandidateMapper.class.getDeclaredMethod("globalCandidateUnits",
|
||||
Page.class, InboundCandidateQuery.class, Set.class);
|
||||
String unitSql = String.join(" ", units.getAnnotation(Select.class).value());
|
||||
Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod(
|
||||
"globalCandidatesByUnits", List.class);
|
||||
String detailSql = String.join(" ", details.getAnnotation(Select.class).value());
|
||||
|
||||
assertThat(sql)
|
||||
.contains("#{warehouseId} IS NULL OR o.warehouse_id=#{warehouseId}")
|
||||
assertThat(unitSql)
|
||||
.contains("#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId}")
|
||||
.contains("o.channel_code IN")
|
||||
.contains("o.process_status='WAIT_SPLIT'")
|
||||
.contains("current_group.id groupId")
|
||||
.contains("group_member_count.group_id=current_group.id")
|
||||
.contains("availablePackageCount=current_member.full_package_count")
|
||||
.doesNotContain("fl_container_booking")
|
||||
.doesNotContain("bookingId");
|
||||
assertThat(detailSql)
|
||||
.contains("collection=\"units\"")
|
||||
.contains("current_group.id=#{unit.groupId}")
|
||||
.contains("o.tenant_id=#{unit.accountId}")
|
||||
.contains("o.id=#{unit.inboundId}")
|
||||
.doesNotContain("fl_container_booking");
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalCandidateDynamicSqlRendersForStandaloneAndGroupedUnits() throws Exception {
|
||||
Configuration configuration = new Configuration();
|
||||
XMLLanguageDriver driver = new XMLLanguageDriver();
|
||||
Method units = SmartLoadingCandidateMapper.class.getDeclaredMethod("globalCandidateUnits",
|
||||
Page.class, InboundCandidateQuery.class, Set.class);
|
||||
String unitScript = String.join(" ", units.getAnnotation(Select.class).value());
|
||||
var query = new InboundCandidateQuery(List.of("MATSON_CLX"), 100L, 9L, "IN-",
|
||||
java.time.LocalDate.parse("2026-09-01"), java.time.LocalDate.parse("2026-09-30"),
|
||||
null, 1, 20);
|
||||
String renderedUnits = driver.createSqlSource(configuration, unitScript, Map.class)
|
||||
.getBoundSql(Map.of("query", query, "actualChannels", Set.of("MATSON_CLX_TRUCK"))).getSql();
|
||||
assertThat(renderedUnits).contains("o.channel_code IN", "DATE(eligible.expectedArrivalAt)");
|
||||
|
||||
Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod(
|
||||
"globalCandidatesByUnits", List.class);
|
||||
String detailScript = String.join(" ", details.getAnnotation(Select.class).value());
|
||||
List<SmartLoadingCandidateMapper.CandidateUnit> page = List.of(
|
||||
new SmartLoadingCandidateMapper.CandidateUnit(null, 9L, 10L, null, 10L),
|
||||
new SmartLoadingCandidateMapper.CandidateUnit(700L, null, null, null, 11L));
|
||||
String renderedDetails = driver.createSqlSource(configuration, detailScript, Map.class)
|
||||
.getBoundSql(Map.of("units", page)).getSql();
|
||||
assertThat(renderedDetails).contains("current_group.id=?", "o.tenant_id=?", "o.id=?");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+20
@@ -100,6 +100,26 @@ class SmartLoadingSqlContractTest {
|
||||
.doesNotContain("split_item.source_measurement_item_id=i.id");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("全局候选分页具备平台维度和关联明细索引")
|
||||
void shouldProvideGlobalCandidatePagingIndexes() throws Exception {
|
||||
assertThat(new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8))
|
||||
.contains("idx_inbound_smart_loading (warehouse_id, process_status, channel_code, expected_arrival_at, id)")
|
||||
.contains("idx_measure_inbound_status_id (tenant_id, inbound_id, status, id)")
|
||||
.contains("idx_measure_item_candidate (tenant_id, measurement_id, inbound_detail_id)");
|
||||
assertThat(migration())
|
||||
.contains("idx_loading_item_candidate (account_id, inbound_id, inbound_detail_id, deleted)");
|
||||
String unified = unifiedMigration();
|
||||
assertThat(unified)
|
||||
.contains("CREATE PROCEDURE baiyue_firstleg.ensure_smart_loading_candidate_indexes()")
|
||||
.contains("ADD KEY idx_inbound_smart_loading")
|
||||
.contains("ADD KEY idx_measure_inbound_status_id")
|
||||
.contains("ADD KEY idx_measure_item_candidate")
|
||||
.contains("ADD KEY idx_loading_item_candidate")
|
||||
.contains("ADD KEY idx_inbound_split_candidate");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("仓库备注使用计划先行与明细单表CAS并允许已拣明细仅改备注")
|
||||
void shouldUpdateWarehouseRemarkWithoutMultiTableTriggerConflict() throws Exception {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
@@ -29,6 +30,7 @@ import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
|
||||
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
|
||||
import com.baiyue.firstleg.split.service.SplitPreloadService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
@@ -62,6 +64,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anySet;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
@@ -156,10 +159,14 @@ class SmartLoadingServiceTest {
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡");
|
||||
Candidate max = withChannel(candidate(19L, 11L, 21L, 31L, 6),
|
||||
"MATSON_MAX_TRUCK", "美森加班海卡");
|
||||
when(repository.globalCandidates(null)).thenReturn(List.of(clx, max));
|
||||
InboundCandidateQuery query = new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), null, null, null, null, null, null, 1, 20);
|
||||
CandidateUnit unit = new CandidateUnit(null, 9L, 10L, null, 10L);
|
||||
when(repository.globalCandidateUnits(eq(query), anySet()))
|
||||
.thenReturn(new PageResult<>(List.of(unit), 1, 20, 1, 1));
|
||||
when(repository.globalCandidatesByUnits(List.of(unit))).thenReturn(List.of(clx));
|
||||
|
||||
var result = service.inboundCandidates(new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), null, null, null, null, null, null, 1, 20));
|
||||
var result = service.inboundCandidates(query);
|
||||
|
||||
assertThat(result.total()).isEqualTo(1);
|
||||
assertThat(result.pages()).isEqualTo(1);
|
||||
@@ -167,7 +174,9 @@ class SmartLoadingServiceTest {
|
||||
assertThat(candidate.inboundId()).isEqualTo(10L);
|
||||
assertThat(candidate.channelCode()).isEqualTo("MATSON_CLX_TRUCK");
|
||||
});
|
||||
verify(repository).globalCandidates(null);
|
||||
verify(repository).globalCandidateUnits(eq(query), anySet());
|
||||
verify(repository).globalCandidatesByUnits(List.of(unit));
|
||||
verify(coloadGroups, never()).findByInbound(anyLong(), anyLong());
|
||||
verify(repository, never()).booking(anyLong());
|
||||
}
|
||||
|
||||
@@ -194,19 +203,20 @@ class SmartLoadingServiceTest {
|
||||
|
||||
@Test
|
||||
void treatsWholeColoadGroupAsOneStablePagingUnit() {
|
||||
Candidate first = withArrival(withChannel(candidate(9L, 10L, 20L, 30L, 4),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-01T09:00:00");
|
||||
Candidate second = withArrival(withChannel(candidate(19L, 11L, 21L, 31L, 6),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-10T09:00:00");
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, "I10", 4), new Member(19L, 31L, 11L, "I11", 6));
|
||||
when(repository.globalCandidates(100L)).thenReturn(List.of(first, second));
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(group);
|
||||
|
||||
var result = service.inboundCandidates(new InboundCandidateQuery(
|
||||
Candidate first = withGroup(withArrival(withChannel(candidate(9L, 10L, 20L, 30L, 4),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-01T09:00:00"), 2, 10);
|
||||
Candidate second = withGroup(withArrival(withChannel(candidate(19L, 11L, 21L, 31L, 6),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-10T09:00:00"), 2, 10);
|
||||
InboundCandidateQuery query = new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), 100L, null, null, LocalDate.parse("2026-09-10"),
|
||||
LocalDate.parse("2026-09-10"), null, 1, 1));
|
||||
LocalDate.parse("2026-09-10"), null, 1, 1);
|
||||
CandidateUnit unit = new CandidateUnit(700L, null, null,
|
||||
LocalDateTime.parse("2026-09-01T09:00:00"), 10L);
|
||||
when(repository.globalCandidateUnits(eq(query), anySet()))
|
||||
.thenReturn(new PageResult<>(List.of(unit), 1, 1, 1, 1));
|
||||
when(repository.globalCandidatesByUnits(List.of(unit))).thenReturn(List.of(first, second));
|
||||
|
||||
var result = service.inboundCandidates(query);
|
||||
|
||||
assertThat(result.total()).isEqualTo(1);
|
||||
assertThat(result.records()).singleElement().satisfies(candidate -> {
|
||||
@@ -219,18 +229,16 @@ class SmartLoadingServiceTest {
|
||||
|
||||
@Test
|
||||
void excludesIncompleteColoadGroupInsteadOfPublishingPartialMember() {
|
||||
Candidate first = withChannel(candidate(9L, 10L, 20L, 30L, 4),
|
||||
"MATSON_CLX_TRUCK", "美森正班海卡");
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 30L, 10L, "I10", 4), new Member(19L, 31L, 11L, "I11", 6));
|
||||
when(repository.globalCandidates(100L)).thenReturn(List.of(first));
|
||||
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
|
||||
InboundCandidateQuery query = new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), 100L, null, null, null, null, null, 1, 20);
|
||||
when(repository.globalCandidateUnits(eq(query), anySet()))
|
||||
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
var result = service.inboundCandidates(new InboundCandidateQuery(
|
||||
List.of("MATSON_CLX"), 100L, null, null, null, null, null, 1, 20));
|
||||
var result = service.inboundCandidates(query);
|
||||
|
||||
assertThat(result.records()).isEmpty();
|
||||
assertThat(result.total()).isZero();
|
||||
verify(repository, never()).globalCandidatesByUnits(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1223,6 +1231,15 @@ class SmartLoadingServiceTest {
|
||||
value.orderStatusSnapshot(), LocalDateTime.parse(expectedArrivalAt));
|
||||
}
|
||||
|
||||
private static Candidate withGroup(Candidate value, int memberCount, int groupTotalBoxes) {
|
||||
return new Candidate(value.accountId(), value.inboundId(), value.inboundDetailId(), value.orderId(),
|
||||
value.orderNo(), value.inboundCode(), value.warehouseId(), value.channelCode(), value.channelName(),
|
||||
value.destinationWarehouseId(), value.destinationWarehouseCode(), value.destinationWarehouseName(),
|
||||
value.customerName(), value.goodsName(), value.availablePackageCount(), value.unitGrossWeightKg(),
|
||||
value.unitNetWeightKg(), value.unitVolumeCbm(), 700L, "CLG-700", memberCount, groupTotalBoxes,
|
||||
false, null, value.orderStatusSnapshot(), value.expectedArrivalAt());
|
||||
}
|
||||
|
||||
private static ChannelRow channel(String channelCode, String channelName) {
|
||||
ChannelRow value = new ChannelRow();
|
||||
value.setChannelCode(channelCode); value.setChannelName(channelName);
|
||||
|
||||
Reference in New Issue
Block a user