fix(loading): remove ISO container validation across PDA fulfillment

This commit is contained in:
2026-10-23 11:12:36 +08:00
parent c38f1758c5
commit 9c19980736
22 changed files with 215 additions and 162 deletions
@@ -1,49 +0,0 @@
package com.baiyue.firstleg.domain;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import java.util.Locale;
/** ISO 6346 真实集装箱号的唯一规范化与校验入口。 */
public final class Iso6346ContainerNumber {
private static final int[] LETTER_VALUES = {
10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32, 34, 35, 36, 37, 38
};
private Iso6346ContainerNumber() {
}
public static String require(String raw) {
String value = normalizeWhitespace(raw);
if (value == null || !value.matches("[A-Z]{3}[UJZ][0-9]{7}")) {
throw invalid();
}
int sum = 0;
for (int index = 0; index < 10; index++) {
char current = value.charAt(index);
int number = Character.isDigit(current)
? current - '0' : LETTER_VALUES[current - 'A'];
sum += number * (1 << index);
}
int expected = sum % 11 % 10;
if (value.charAt(10) - '0' != expected) throw invalid();
return value;
}
private static String normalizeWhitespace(String raw) {
if (raw == null) return null;
StringBuilder value = new StringBuilder(raw.length());
raw.codePoints().filter(codePoint -> !Character.isWhitespace(codePoint)
&& !Character.isSpaceChar(codePoint))
.forEach(value::appendCodePoint);
String normalized = value.toString().toUpperCase(Locale.ROOT);
return normalized.isEmpty() ? null : normalized;
}
private static BizException invalid() {
return new BizException(ResultCode.BAD_REQUEST,
"真实集装箱号必须符合ISO 6346结构、设备类别和校验位");
}
}
@@ -0,0 +1,19 @@
package com.baiyue.firstleg.domain;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.event.ContainerNumbers;
/** 真实集装箱号录入与作业门禁,与跨服务事件使用同一规范化规则。 */
public final class RealContainerNumber {
private RealContainerNumber() { }
/** 保留柜号必填要求,不再检查 ISO 长度、设备类别和校验位。 */
public static String require(String raw) {
try {
return ContainerNumbers.normalizeRequired(raw);
} catch (IllegalArgumentException exception) {
throw new BizException(ResultCode.BAD_REQUEST, "真实集装箱号不能为空");
}
}
}
@@ -12,7 +12,7 @@ import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
import com.baiyue.firstleg.domain.RealContainerNumber;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.repository.ContainerRepository;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
@@ -124,6 +124,7 @@ public class ContainerService {
return required(id);
}
/** 提柜后补录规范化真实柜号,保留同订舱唯一及作业冻结门禁。 */
@Transactional
public ContainerBookingEntity assignRealContainerNo(long id, String requestedContainerNo, int expectedVersion,
long operatorId, String operatorName) {
@@ -138,7 +139,7 @@ public class ContainerService {
|| repository.hasPickTask(id) || repository.hasEvidence(id) || repository.hasDocuments(id)) {
throw conflict("已进入PDA、证据、单证或不可逆装柜阶段,不能补录真实柜号");
}
String containerNo = Iso6346ContainerNumber.require(requestedContainerNo);
String containerNo = RealContainerNumber.require(requestedContainerNo);
if (repository.containerNoExists(container.getOceanBookingId(), containerNo, id)) {
throw conflict("当前提单下集装箱号已存在");
}
@@ -26,7 +26,7 @@ import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
import com.baiyue.firstleg.repository.LoadingListRepository;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.ContainerRepository;
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
import com.baiyue.firstleg.domain.RealContainerNumber;
import com.baiyue.firstleg.domain.PriceBookChannelCatalog;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
@@ -493,15 +493,10 @@ public class LoadingListService {
return repository.preloadDetail(requirePlan(planId));
}
/** 保留关联入口已接受的柜号,运输入口首次录入或纠错仍执行原有校验。 */
/** 未提交柜号时保留原值,首次录入或纠错统一规范化并禁止清空。 */
private static String targetContainerNo(AggregateRow before, String requested) {
if (requested == null) return StringUtils.hasText(before.getContainerNo()) ? before.getContainerNo() : null;
String normalized = requested.replaceAll("[\\p{javaWhitespace}\\p{Z}]", "")
.toUpperCase(java.util.Locale.ROOT);
if (StringUtils.hasText(normalized) && normalized.equals(before.getContainerNo())) {
return before.getContainerNo();
}
return Iso6346ContainerNumber.require(requested);
return RealContainerNumber.require(requested);
}
private static String transportHash(long planId, int version, String containerNo,
@@ -23,7 +23,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.InboundInventory;
import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot;
import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryUnit;
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
import com.baiyue.firstleg.domain.RealContainerNumber;
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
@@ -140,7 +140,7 @@ public class LoadingPickService {
smartLoadingRepository.items(plan.getId()));
}
/** 确认预装前验证作业资格和真实柜号,格式问题必须在实物作业前纠正。 */
/** 确认预装前验证作业资格和真实柜号必填。 */
public List<Link> requirePreloadConfirmationReady(long loadingPlanId) {
inboundHoldGuard.requirePlanNotHeld(loadingPlanId);
List<Link> links = loadingModelBridge.requireAllocatedLinksForConfirmation(loadingPlanId);
@@ -614,12 +614,12 @@ public class LoadingPickService {
/** 本进程事务提交后的唤醒信号,权威命令和恢复事实仍保存在数据库。 */
public record CompletionAccepted(long commandId, long acceptedAtNanos) {}
/** 在仍可纠错的预装阶段提示格式错误,避免领取或实物作业后卡在完成门禁。 */
/** 确认预装和创建 PDA 任务前要求柜号非空,不以 ISO 格式阻断作业。 */
private static void requireContainerNumberBeforeStarting(String containerNo) {
try {
Iso6346ContainerNumber.require(containerNo);
RealContainerNumber.require(containerNo);
} catch (BizException exception) {
throw conflict("请先在预装运输信息中纠正真实集装箱号后再确认预装或启动PDA,需符合ISO 6346");
throw conflict("请先在预装运输信息中录入真实集装箱号后再确认预装或启动PDA");
}
}
@@ -630,9 +630,9 @@ public class LoadingPickService {
throw conflict("PDA确认装柜完成前必须录入真实集装箱号");
}
try {
Iso6346ContainerNumber.require(booking.getContainerNo());
RealContainerNumber.require(booking.getContainerNo());
} catch (BizException exception) {
throw conflict("PDA确认装柜完成前必须录入符合ISO 6346的真实集装箱号");
throw conflict("PDA确认装柜完成前必须录入真实集装箱号");
}
}