Test #1
@@ -3,6 +3,8 @@ package com.baiyue.firstleg.mapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -10,12 +12,15 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface LoadingCartonManifestMapper {
|
||||
@Select("""
|
||||
SELECT box_code FROM fl_inbound_carton_label
|
||||
WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND inbound_detail_id=#{inboundDetailId}
|
||||
AND batch_status='ACTIVE'
|
||||
SELECT inbound_detail_id, box_code FROM fl_inbound_carton_label
|
||||
WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND batch_status='ACTIVE'
|
||||
ORDER BY carton_index,id
|
||||
""")
|
||||
List<String> labelPool(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("inboundDetailId") long inboundDetailId);
|
||||
List<LabelRow> labelPool(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Getter @Setter
|
||||
class LabelRow {
|
||||
private Long inboundDetailId;
|
||||
private String boxCode;
|
||||
}
|
||||
}
|
||||
|
||||
+42
-18
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper.Row;
|
||||
import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper.PalletRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper.LabelRow;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -30,9 +31,11 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** 在装柜完成事务内按客户写跨区业务 Outbox。 */
|
||||
@Service
|
||||
@@ -106,33 +109,54 @@ public class LoadingCompletedOutboxService {
|
||||
/** 国内按实装箱数从可用标签池稳定随机抽取箱唛,重复发布同一计划时结果不变。 */
|
||||
private Map<Long, List<String>> cartonLabelPools(List<Row> rows) {
|
||||
Map<Long, List<String>> result = new java.util.LinkedHashMap<>();
|
||||
for (Row row : rows) {
|
||||
if (dropship(row.getBusinessType())) continue;
|
||||
if (row.getSkuCode() == null || row.getSkuCode().isBlank()) {
|
||||
Map<LabelPoolKey, List<Row>> byInbound = rows.stream().filter(row -> !dropship(row.getBusinessType()))
|
||||
.collect(java.util.stream.Collectors.groupingBy(
|
||||
row -> new LabelPoolKey(row.getAccountId(), row.getSourceInboundId()),
|
||||
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||
for (Map.Entry<LabelPoolKey, List<Row>> entry : byInbound.entrySet()) {
|
||||
List<Row> inboundRows = entry.getValue().stream()
|
||||
.sorted(Comparator.comparing(Row::getLoadingPlanItemId)).toList();
|
||||
if (inboundRows.stream().anyMatch(row -> row.getSkuCode() == null || row.getSkuCode().isBlank())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细缺少 SKU,不能建立箱唛与 SKU 的对应关系");
|
||||
}
|
||||
List<String> labels = Objects.requireNonNullElse(cartonManifestMapper.labelPool(row.getAccountId(),
|
||||
row.getSourceInboundId(), row.getSourceInboundDetailId()), List.of());
|
||||
if (labels.isEmpty()) throw new BizException(ResultCode.CONFLICT, "装柜明细没有已打印的可用箱唛池,不能生成海外交接事件");
|
||||
if (labels.stream().anyMatch(label -> label == null || label.isBlank())
|
||||
|| labels.stream().distinct().count() != labels.size()) {
|
||||
List<LabelRow> labels = Objects.requireNonNullElse(cartonManifestMapper.labelPool(
|
||||
entry.getKey().accountId(), entry.getKey().inboundId()), List.of());
|
||||
if (labels.isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细没有已打印的可用箱唛池,不能生成海外交接事件");
|
||||
}
|
||||
if (labels.stream().anyMatch(label -> label == null || label.getBoxCode() == null
|
||||
|| label.getBoxCode().isBlank())
|
||||
|| labels.stream().map(LabelRow::getBoxCode).distinct().count() != labels.size()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细箱唛池包含空值或重复箱号");
|
||||
}
|
||||
if (labels.size() < Objects.requireNonNullElse(row.getPackageCount(), 0)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细箱唛数量少于实装箱数,不能生成海外交接事件");
|
||||
Set<String> used = new HashSet<>();
|
||||
for (Row row : inboundRows) {
|
||||
int loadedPackages = Objects.requireNonNullElse(row.getPackageCount(), 0);
|
||||
String seed = row.getLoadingPlanId() + ":" + row.getLoadingPlanItemId();
|
||||
Comparator<LabelRow> stableOrder = Comparator.comparing(label -> EventPayloadHash.sha256(
|
||||
(seed + ":" + label.getBoxCode()).getBytes(StandardCharsets.UTF_8)));
|
||||
List<String> selected = new ArrayList<>();
|
||||
labels.stream().filter(label -> !used.contains(label.getBoxCode())
|
||||
&& Objects.equals(label.getInboundDetailId(), row.getSourceInboundDetailId()))
|
||||
.sorted(stableOrder).limit(loadedPackages).map(LabelRow::getBoxCode).forEach(selected::add);
|
||||
if (selected.size() < loadedPackages) {
|
||||
labels.stream().filter(label -> !used.contains(label.getBoxCode())
|
||||
&& !selected.contains(label.getBoxCode()))
|
||||
.sorted(stableOrder).limit(loadedPackages - selected.size())
|
||||
.map(LabelRow::getBoxCode).forEach(selected::add);
|
||||
}
|
||||
if (selected.size() < loadedPackages) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜明细箱唛数量少于实装箱数,不能生成海外交接事件");
|
||||
}
|
||||
used.addAll(selected);
|
||||
result.put(row.getLoadingPlanItemId(), List.copyOf(selected));
|
||||
}
|
||||
int loadedPackages = Objects.requireNonNullElse(row.getPackageCount(), 0);
|
||||
String seed = row.getLoadingPlanId() + ":" + row.getLoadingPlanItemId();
|
||||
List<String> selected = labels.stream()
|
||||
.sorted(Comparator.comparing(label -> EventPayloadHash.sha256(
|
||||
(seed + ":" + label).getBytes(StandardCharsets.UTF_8))))
|
||||
.limit(loadedPackages)
|
||||
.toList();
|
||||
result.put(row.getLoadingPlanItemId(), selected);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private record LabelPoolKey(long accountId, long inboundId) { }
|
||||
|
||||
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source) {
|
||||
if (source.isEmpty()) return List.of();
|
||||
Map<String, List<PalletRow>> byPallet = source.stream().collect(java.util.stream.Collectors.groupingBy(
|
||||
|
||||
+31
-4
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper.Row;
|
||||
import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper.PalletRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper.LabelRow;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -37,7 +38,9 @@ class LoadingCompletedOutboxServiceTest {
|
||||
ReflectionTestUtils.setField(service,"cartonManifestMapper",manifests);
|
||||
ReflectionTestUtils.setField(service,"outboxMapper",outbox);
|
||||
ReflectionTestUtils.setField(service,"objectMapper",mapper);
|
||||
when(manifests.labelPool(anyLong(), anyLong(), anyLong())).thenReturn(List.of("BOX-POOL-1"));
|
||||
when(manifests.labelPool(anyLong(), anyLong())).thenAnswer(invocation -> List.of(
|
||||
label(101, "BOX-101"), label(102, "BOX-102"),
|
||||
label(202, "BOX-202"), label(302, "BOX-302")));
|
||||
}
|
||||
|
||||
@Test void groupsByAccountAndKeepsAllOrdersAndItemsStable() throws Exception {
|
||||
@@ -54,7 +57,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(first.payload().at("/orders/0/sourceInboundId").asText()).isEqualTo("100");
|
||||
assertThat(first.payload().at("/orders/0/items/0/sourceInboundDetailId").asText()).isEqualTo("101");
|
||||
assertThat(first.payload().at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU101");
|
||||
assertThat(first.payload().at("/orders/0/items/0/cartonMarks/0").asText()).isEqualTo("BOX-POOL-1");
|
||||
assertThat(first.payload().at("/orders/0/items/0/cartonMarks/0").asText()).isEqualTo("BOX-101");
|
||||
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2");
|
||||
assertThat(first.eventVersion()).isEqualTo(2);
|
||||
}
|
||||
@@ -72,8 +75,9 @@ class LoadingCompletedOutboxServiceTest {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setPackageCount(2);
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
when(manifests.labelPool(11L, 100L, 101L)).thenReturn(
|
||||
List.of("BOX-001", "BOX-002", "BOX-003", "BOX-004"));
|
||||
when(manifests.labelPool(11L, 100L)).thenReturn(List.of(
|
||||
label(101, "BOX-001"), label(101, "BOX-002"),
|
||||
label(101, "BOX-003"), label(101, "BOX-004")));
|
||||
|
||||
service.publish(90L);
|
||||
service.publish(90L);
|
||||
@@ -87,6 +91,25 @@ class LoadingCompletedOutboxServiceTest {
|
||||
.isEqualTo(second.payload().at("/orders/0/items/0/cartonMarks"));
|
||||
}
|
||||
|
||||
@Test void fillsCollapsedMeasurementDetailFromUnusedInboundLabelsWithoutDuplicates() throws Exception {
|
||||
Row first = row(11, 100, 101); first.setPackageCount(1);
|
||||
Row second = row(11, 100, 101); second.setLoadingPlanItemId(102L); second.setPackageCount(2);
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(first, second));
|
||||
when(manifests.labelPool(11L, 100L)).thenReturn(List.of(
|
||||
label(201, "BOX-001"), label(202, "BOX-002"), label(203, "BOX-003")));
|
||||
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captor.capture());
|
||||
EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
List<String> firstMarks = mapper.convertValue(event.payload().at("/orders/0/items/0/cartonMarks"), List.class);
|
||||
List<String> secondMarks = mapper.convertValue(event.payload().at("/orders/0/items/1/cartonMarks"), List.class);
|
||||
assertThat(firstMarks).hasSize(1);
|
||||
assertThat(secondMarks).hasSize(2);
|
||||
assertThat(firstMarks).doesNotContainAnyElementsOf(secondMarks);
|
||||
}
|
||||
|
||||
@Test void snapshotOnlyPublishesFinalizedLoadedInbounds() throws Exception {
|
||||
String sql = String.join(" ", LoadingCompletedEventMapper.class.getMethod("rows", long.class)
|
||||
.getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
|
||||
@@ -137,4 +160,8 @@ class LoadingCompletedOutboxServiceTest {
|
||||
row.setSourceInboundDetailId(detail); row.setSkuCode(sku); row.setProductNameZh("产品");
|
||||
row.setPackageCount(packages); row.setProductQuantity(quantity); row.setBoxCode(boxCode); return row;
|
||||
}
|
||||
|
||||
private LabelRow label(long detailId, String boxCode) {
|
||||
LabelRow row = new LabelRow(); row.setInboundDetailId(detailId); row.setBoxCode(boxCode); return row;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user