This commit is contained in:
2026-09-15 18:17:06 +08:00
parent 45018987ef
commit 4b671bb0ab
13 changed files with 174 additions and 15 deletions
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.mapper;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.PendingTask;
import com.baiyue.firstleg.dto.SmartLoadingDtos.PalletCandidate;
import com.baiyue.firstleg.dto.SmartLoadingDtos.GroupMemberCandidate;
import lombok.Getter;
import lombok.Setter;
import org.apache.ibatis.annotations.*;
@@ -254,6 +255,17 @@ public interface DropshipPalletizationMapper {
""")
List<PrintSkuRow> printSkuRows(@Param("palletId") long palletId);
@Select("""
SELECT o.tenant_id accountId,o.id inboundId,o.order_id orderId,o.order_no orderNo,
o.inbound_code inboundCode,o.customer_name customerName,COUNT(DISTINCT c.id) availablePackageCount,
o.process_status orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt
FROM fl_dropship_pallet_carton c
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
WHERE c.pallet_id=#{palletId}
GROUP BY o.tenant_id,o.id,o.order_id,o.order_no,o.inbound_code,o.customer_name,o.process_status,o.expected_arrival_at
ORDER BY o.inbound_code,o.id
""") List<GroupMemberCandidate> palletInboundMembers(@Param("palletId") long palletId);
@Select("""
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(DISTINCT c.id) carton_count,
(SELECT COUNT(DISTINCT sku.sku_code) FROM fl_dropship_pallet_carton pc
@@ -142,7 +142,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
<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(CASE WHEN current_group.id IS NULL THEN eligible.inboundId END) inboundId,
CASE WHEN current_group.id IS NULL THEN eligible.palletId END palletId,
MIN(eligible.expectedArrivalAt) sortArrivalAt,MIN(eligible.inboundId) sortInboundId
FROM (
@@ -202,7 +202,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
WHERE eligible.candidateStatus&lt;&gt;'WAIT_SPLIT' OR eligible.availablePackageCount&gt;0
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,
CASE WHEN current_group.id IS NULL AND eligible.palletId IS NULL THEN eligible.inboundId END,
CASE WHEN current_group.id IS NULL THEN eligible.palletId END
HAVING (current_group.id IS NULL OR (
COUNT(DISTINCT eligible.accountId,eligible.inboundId)=(SELECT COUNT(*) FROM fl_coload_group_member group_member_count
@@ -230,7 +230,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
<if test="query.createdEnd != null">AND MIN(eligible.createdAt)&lt;DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)</if>
<if test="query.expectedArrivalStart != null">AND MIN(eligible.expectedArrivalAt)&gt;=#{query.expectedArrivalStart}</if>
<if test="query.expectedArrivalEnd != null">AND MIN(eligible.expectedArrivalAt)&lt;DATE_ADD(#{query.expectedArrivalEnd},INTERVAL 1 DAY)</if>
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId,palletId
</script>
""")
Page<CandidateUnit> globalCandidateUnits(Page<CandidateUnit> page,
@@ -103,7 +103,7 @@ public class SmartLoadingService {
Map<Long, List<Candidate>> grouped = candidates(bookingId, warehouseId).stream()
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
java.util.stream.Collectors.toList()));
return grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
return uniqueCandidateUnits(grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList());
}
private List<InboundCandidate> recommendationCandidates(ContainerBookingEntity booking,
@@ -289,7 +289,7 @@ public class SmartLoadingService {
first.unitNetWeightKg(), pallet.cartonCount()==null||pallet.cartonCount()==0?null:pallet.volumeCbm().divide(BigDecimal.valueOf(pallet.cartonCount()),6,RoundingMode.HALF_UP),
pallet.volumeCbm(),"SEALED_PALLET",pallet.palletId(),pallet.palletNo(),true,
first.coLoadGroupId(), first.groupNo(), first.memberCount(),first.groupTotalBoxes(), false, !StringUtils.hasText(palletBlock), palletBlock,
first.orderStatusSnapshot(),first.expectedArrivalAt(),List.of());
first.orderStatusSnapshot(),first.expectedArrivalAt(),dropshipPallets.palletInboundMembers(pallet.palletId()));
}).toList();
return List.of(new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
@@ -367,7 +367,17 @@ public class SmartLoadingService {
}
private static String unitKey(Long groupId, Long accountId, Long inboundId, Long palletId) {
return groupId == null ? "I|" + accountId + '|' + inboundId + '|' + palletId : "G|" + groupId;
return groupId != null ? "G|" + groupId : palletId != null ? "P|" + palletId : "I|" + accountId + '|' + inboundId + '|' + palletId;
}
private static List<InboundCandidate> uniqueCandidateUnits(List<InboundCandidate> candidates) {
Map<String, InboundCandidate> units = new LinkedHashMap<>();
for (InboundCandidate candidate : candidates) {
String key = candidate.palletId() != null ? "P|" + candidate.palletId()
: "I|" + candidate.accountId() + '|' + candidate.inboundId();
units.putIfAbsent(key, candidate);
}
return List.copyOf(units.values());
}
private static BigDecimal weightedAverage(List<InboundCandidate> members,
@@ -571,7 +571,8 @@ paths:
description: >-
需要 platform:firstleg:smart-loading:list 权限。查询不绑定柜次;渠道、仓库、客户、入仓码、
订单创建日期、预计到货时间和订单状态共同作用于全局候选。每个普通入库单返回一行;同柜不可拆组作为一个
分页单元返回,groupMembers 包含完整成员,不会跨页或因单个成员命中搜索条件而拆散。
分页单元返回;一件代发每个已封托托盘仅返回一行,合托不按入仓码重复分页。
groupMembers 包含同柜组或托盘的完整成员,不会跨页或因单个成员命中搜索条件而拆散;托盘行箱数为整托箱数,成员箱数为本票在本托的实际箱数。
平均单箱毛重、净重按整票实测重量除以总箱数,平均单箱体积按剩余规格数量加权。
orderStatuses 可重复传参且最多四项;未传或空数组表示 PENDING_REVIEW、WAIT_MEASURE、WAIT_SPLIT、
LOADING 全部未完成状态,已完成装柜永不返回。状态映射为:Order SUBMITTED=PENDING_REVIEW;
@@ -2443,7 +2444,7 @@ components:
blockReason: {type: string, nullable: true}
groupMembers:
type: array
description: 同柜组聚合行的完整成员;普通候选为空数组
description: 同柜组或封托托盘的完整入仓成员;SEALED_PALLET 行按托盘唯一分页,成员 availablePackageCount 为该入仓码在本托的箱数,行 availablePackageCount 为整托总箱数。搜索命中任一成员仍返回整托完整组成;普通箱件候选为空数组。
items:
type: object
required: [accountId, inboundId, orderId, orderNo, inboundCode, customerName, availablePackageCount]
@@ -20,6 +20,21 @@ import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
class SmartLoadingCandidateMapperContractTest {
@Test
void sharedPalletPaginationGroupsByPalletRatherThanInbound() throws Exception {
String sql = String.join(" ", SmartLoadingCandidateMapper.class
.getDeclaredMethod("globalCandidateUnits", Page.class, InboundCandidateQuery.class, Set.class)
.getAnnotation(Select.class).value());
assertThat(sql).contains("MIN(CASE WHEN current_group.id IS NULL THEN eligible.inboundId END) inboundId")
.contains("CASE WHEN current_group.id IS NULL AND eligible.palletId IS NULL THEN eligible.inboundId END")
.contains("sortInboundId,palletId");
String members = String.join(" ", com.baiyue.firstleg.mapper.DropshipPalletizationMapper.class
.getDeclaredMethod("palletInboundMembers", long.class).getAnnotation(Select.class).value());
assertThat(members).contains("COUNT(DISTINCT c.id) availablePackageCount")
.contains("WHERE c.pallet_id=#{palletId}")
.contains("GROUP BY o.tenant_id,o.id")
.doesNotContain("#{inboundId}");
}
@Test
void candidateHasOnlyTheCanonicalMybatisConstructor() {
assertThat(Candidate.class.getDeclaredConstructors()).singleElement()
@@ -165,6 +180,10 @@ class SmartLoadingCandidateMapperContractTest {
.contains("eligible.candidateStatus='WAIT_SPLIT'")
.contains("MAX(CASE WHEN LOWER(eligible.inboundCode)")
.contains("MAX(CASE WHEN eligible.accountId=");
assertThat(renderedUnits.indexOf("MAX(CASE WHEN LOWER(eligible.inboundCode)"))
.as("inbound search must match any member after grouping, without cutting pallet composition")
.isGreaterThan(renderedUnits.indexOf("HAVING"));
assertThat(renderedUnits).contains("LIKE CONCAT('%',LOWER(");
Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod(
"globalCandidatesByUnits", List.class);
@@ -261,6 +261,30 @@ class SmartLoadingServiceTest {
});
}
@Test
void sharedPalletIsOneUnitWithExactInboundComposition() {
Candidate dropship = withStatus(withChannel(candidate(9L, 10L, 20L, 100L, 20), "EXX_TRUCK", "EXX海卡"), "WAIT_SPLIT", false);
// Searching a non-representative member must still return the whole pallet.
InboundCandidateQuery query = new InboundCandidateQuery(List.of("EXX"), null, null, "I11",
null, null, null, null, List.of(CandidateOrderStatus.WAIT_SPLIT), 1, 1);
var unit = new CandidateUnit(null, 9L, 10L, 801L, null, 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(dropship));
when(dropshipPallets.palletCandidates(10L)).thenReturn(List.of(
new PalletCandidate(801L, "DP-0025", "SEALED_PALLET", true, 13, 1, new BigDecimal("130"), new BigDecimal("1.3"))));
var members = List.of(
new com.baiyue.firstleg.dto.SmartLoadingDtos.GroupMemberCandidate(9L, 10L, 100L, "O10", "I10", "客户", 3, "WAIT_SPLIT", null),
new com.baiyue.firstleg.dto.SmartLoadingDtos.GroupMemberCandidate(9L, 11L, 101L, "O11", "I11", "客户", 10, "WAIT_SPLIT", null));
when(dropshipPallets.palletInboundMembers(801L)).thenReturn(members);
var result = service.inboundCandidates(query);
assertThat(result.total()).isEqualTo(1);
assertThat(result.records()).singleElement().satisfies(row -> {
assertThat(row.availablePackageCount()).isEqualTo(13);
assertThat(row.groupMembers()).containsExactlyElementsOf(members);
assertThat(row.unitGrossWeightKg()).isEqualByComparingTo("10");
});
}
@Test
void combinesAllUnfinishedStatusesBeforeReturningTheRequestedPage() {
InboundCandidateQuery query = new InboundCandidateQuery(