feat: enforce canonical container identity across regions
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.common.event;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/** Canonical physical container number used by cross-region business facts. */
|
||||
public final class ContainerNumbers {
|
||||
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 ContainerNumbers() { }
|
||||
|
||||
public static String normalizeRequired(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("containerNo is required");
|
||||
}
|
||||
StringBuilder compact = new StringBuilder(value.length());
|
||||
value.codePoints().filter(codePoint -> !Character.isWhitespace(codePoint)
|
||||
&& !Character.isSpaceChar(codePoint)).forEach(compact::appendCodePoint);
|
||||
String normalized = compact.toString().toUpperCase(Locale.ROOT);
|
||||
if (!normalized.matches("[A-Z]{3}[UJZ][0-9]{7}")) {
|
||||
throw new IllegalArgumentException("containerNo must comply with ISO 6346");
|
||||
}
|
||||
int sum = 0;
|
||||
for (int index = 0; index < 10; index++) {
|
||||
char current = normalized.charAt(index);
|
||||
int number = Character.isDigit(current) ? current - '0' : LETTER_VALUES[current - 'A'];
|
||||
sum += number * (1 << index);
|
||||
}
|
||||
if (normalized.charAt(10) - '0' != sum % 11 % 10) {
|
||||
throw new IllegalArgumentException("containerNo must comply with ISO 6346 check digit");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static String requireCanonical(String value) {
|
||||
String normalized = normalizeRequired(value);
|
||||
if (!normalized.equals(value)) {
|
||||
throw new IllegalArgumentException("containerNo must be normalized");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
package com.baiyue.common.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -8,7 +9,7 @@ public record FirstLegContainerShippingNodeRecordedV1(
|
||||
String queryId,
|
||||
String customerId,
|
||||
String oceanBookingId,
|
||||
String containerBookingId,
|
||||
@JsonProperty("sourceContainerBookingId") String containerBookingId,
|
||||
String containerNo,
|
||||
String standardNode,
|
||||
int nodeSequence,
|
||||
@@ -34,6 +35,7 @@ public record FirstLegContainerShippingNodeRecordedV1(
|
||||
|| containerNo == null || containerNo.isBlank()) {
|
||||
throw new IllegalArgumentException("船节点交接业务标识不能为空");
|
||||
}
|
||||
containerNo = ContainerNumbers.requireCanonical(containerNo);
|
||||
if (standardNode == null || !Integer.valueOf(nodeSequence).equals(STANDARD_NODES.get(standardNode))
|
||||
|| observedAt == null || actualAt == null) {
|
||||
throw new IllegalArgumentException("船节点交接只接受带实际时间的标准节点事实");
|
||||
|
||||
+13
-1
@@ -1,14 +1,26 @@
|
||||
package com.baiyue.common.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** 装柜完成交接快照,包含三类逐货物备注及一键代发托盘快照。 */
|
||||
public record FirstLegLoadingPlanCompletedV2(
|
||||
String loadingPlanId, String planNo, String containerBookingId,
|
||||
String loadingPlanId, String planNo,
|
||||
@JsonProperty("sourceContainerBookingId") String containerBookingId,
|
||||
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
||||
String estimatedPortArrivalAt, String actualPortArrivalAt, String estimatedWarehouseArrivalAt,
|
||||
List<Order> orders) {
|
||||
public FirstLegLoadingPlanCompletedV2 {
|
||||
if (loadingPlanId == null || loadingPlanId.isBlank()
|
||||
|| containerBookingId == null || containerBookingId.isBlank()) {
|
||||
throw new IllegalArgumentException("loadingPlanId and sourceContainerBookingId are required");
|
||||
}
|
||||
containerNo = ContainerNumbers.requireCanonical(containerNo);
|
||||
orders = orders == null ? List.of() : List.copyOf(orders);
|
||||
if (orders.isEmpty()) throw new IllegalArgumentException("orders are required");
|
||||
}
|
||||
|
||||
public FirstLegLoadingPlanCompletedV2(String loadingPlanId, String planNo, String containerBookingId,
|
||||
String containerNo, String cabinetNo, String warehouseId,
|
||||
String warehouseName, List<Order> orders) {
|
||||
|
||||
+6
@@ -13,4 +13,10 @@ public record OverseasContainerUnloadedV1(
|
||||
Instant unloadingPhotoUploadedAt,
|
||||
Instant actualEmptyReportedAt
|
||||
) {
|
||||
public OverseasContainerUnloadedV1 {
|
||||
if (sourceContainerBookingId == null || sourceContainerBookingId.isBlank()) {
|
||||
throw new IllegalArgumentException("sourceContainerBookingId is required");
|
||||
}
|
||||
containerNo = ContainerNumbers.requireCanonical(containerNo);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user