fix(firstleg): allocate handover carton marks per inbound

This commit is contained in:
2026-08-24 14:40:51 +08:00
parent 2f32f1d827
commit a8326455fc
3 changed files with 83 additions and 27 deletions
@@ -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;
}
}
@@ -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(