fix(firstleg): compare stable carrier facts separately from response audit hashes

This commit is contained in:
2026-10-03 00:39:57 +08:00
parent 9803dcfd2c
commit 4cd51a4955
9 changed files with 266 additions and 23 deletions
@@ -575,8 +575,8 @@ public class CarrierScheduleQueryService {
fact.setSourceReference(trim(sourceReference)); fact.setRawPayloadHash(rawPayloadHash);
if (repository.insertIfAbsent(fact)) return fact;
CarrierScheduleNodeFactEntity existing = repository.nodeByContainer(containerBookingId, fact.getEventKey());
if (existing != null && !Objects.equals(existing.getRawPayloadHash(), rawPayloadHash)) {
throw conflict("相同providerEventId对应不同rawPayloadHash,拒绝覆盖既有节点事实");
if (existing != null && !nodeFactHash(existing).equals(nodeFactHash(fact))) {
throw conflict("相同providerEventId对应不同事件事实摘要,拒绝覆盖既有节点事实");
}
return null;
}
@@ -595,12 +595,32 @@ public class CarrierScheduleQueryService {
fact.setSourceReference(trim(sourceReference)); fact.setRawPayloadHash(rawPayloadHash);
if (repository.insertIfAbsent(fact)) return true;
CarrierScheduleExceptionFactEntity existing = repository.exception(queryId, fact.getEventKey());
if (existing != null && !Objects.equals(existing.getRawPayloadHash(), rawPayloadHash)) {
throw conflict("相同providerEventId对应不同rawPayloadHash,拒绝覆盖既有异常事实");
if (existing != null && !exceptionFactHash(existing).equals(exceptionFactHash(fact))) {
throw conflict("相同providerEventId对应不同事件事实摘要,拒绝覆盖既有异常事实");
}
return false;
}
// rawPayloadHash audits the whole provider response. Sibling events, ETA and query time
// may change between polls; compare the immutable event projection instead of that audit hash.
private String nodeFactHash(CarrierScheduleNodeFactEntity fact) {
return hash("PROVIDER_NODE_FACT_V1", fact.getContainerBookingId(), java.util.Arrays.asList(
fact.getProviderCode(), fact.getProviderEventId(), fact.getProviderNodeCode(), fact.getStandardNode(),
factTimestamp(fact.getEstimatedAt()), factTimestamp(fact.getActualAt()),
fact.getLocationCode(), fact.getLocationName(), fact.getSourceReference()));
}
private String exceptionFactHash(CarrierScheduleExceptionFactEntity fact) {
return hash("PROVIDER_EXCEPTION_FACT_V1", fact.getQueryId(), java.util.Arrays.asList(
fact.getProviderCode(), fact.getProviderEventId(), fact.getProviderStatus(), fact.getExceptionCode(),
fact.getActive(), fact.getLocationCode(), fact.getLocationName(), fact.getSourceReference()));
}
private static LocalDateTime factTimestamp(LocalDateTime value) {
// Match DATETIME(3) storage rounding, including a carry into the next second.
return value == null ? null : value.plusNanos(500_000).truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
}
private String requireContainerNo(long containerBookingId) {
ContainerBookingEntity container = portSchedules.containerBooking(containerBookingId);
if (container == null) {