From 9617038e4ca050d80a38f2b974aba4a7a9890131 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Thu, 3 Sep 2026 15:52:52 +0800 Subject: [PATCH] fix: preserve required shadow comparison evidence --- .../shadow/ShadowPlanningEngine.java | 148 ++++++++++++------ .../shadow/ShadowPlanningEngineTest.java | 47 ++++-- 2 files changed, 136 insertions(+), 59 deletions(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java index 671afbe..4761709 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java @@ -22,6 +22,12 @@ import java.util.function.Supplier; public class ShadowPlanningEngine { private static final int INSTANCE_STALE_SECONDS = 120; private static final int DECISION_REASON_MAX_LENGTH = 4000; + private static final int IDENTIFIER_MAX_LENGTH = 80; + private static final int WIDE_IDENTIFIER_MAX_LENGTH = 255; + private static final int SNAPSHOT_SYNC_MAX_LENGTH = 40; + private static final int SOURCE_SIGNATURE_MAX_LENGTH = 64; + private static final int RESULT_REASON_MAX_LENGTH = 600; + private static final int NUMBER_MAX_LENGTH = 64; private static final String PUBLISHED = "PUBLISHED"; private final SchedulingFacade schedulingFacade; @@ -124,13 +130,7 @@ public class ShadowPlanningEngine { observation.snapshotGeneration(), observation.snapshotSynchronizedAt(), observation.sourceSignature(), comparisonCode(observation.proposedSpaceId(), legacySpaceId), - abbreviate("snapshotGeneration=" + observation.snapshotGeneration() - + "; request=" + observation.requestId() - + "; shadow=" + observation.resultCode() - + "; proposedSpace=" + nullSafe(observation.proposedSpaceId()) - + "; legacy=" + nullSafe(legacyResultCode) - + "; legacySpace=" + nullSafe(legacySpaceId) - + "; shadowReason=" + nullSafe(observation.reason()), DECISION_REASON_MAX_LENGTH)); + inboundComparisonReason(observation, legacyResultCode, legacySpaceId)); requireComparisonInserted(inserted); }); } @@ -144,13 +144,7 @@ public class ShadowPlanningEngine { UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE", observation.snapshotGeneration(), observation.snapshotSynchronizedAt(), observation.sourceSignature(), "LEGACY_" + nullSafe(legacyResultCode), - abbreviate("snapshotGeneration=" + observation.snapshotGeneration() - + "; request=" + observation.requestId() - + "; shadow=" + observation.resultCode() - + "; proposedStock=" + nullSafe(observation.proposedStockId()) - + "; proposedSpace=" + nullSafe(observation.proposedSpaceId()) - + "; legacy=" + nullSafe(legacyResultCode) - + "; shadowReason=" + nullSafe(observation.reason()), DECISION_REASON_MAX_LENGTH)); + outboundComparisonReason(observation, legacyResultCode)); requireComparisonInserted(inserted); }); } @@ -299,42 +293,98 @@ public class ShadowPlanningEngine { private String outboundDiagnostic(PublishedShadowSnapshot snapshot, String requestId, ShadowStockReference reference, String resultReason) { - return snapshotDiagnostic(snapshot) - + "; detail=" + nullSafe(requestId) - + "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId()) - + "; project=" + nullSafe(reference == null ? null : reference.getProjectNo()) - + "; product=" + nullSafe(reference == null ? null : reference.getProductId()) - + "; tray=" + nullSafe(reference == null ? null : reference.getTrayNo()) - + "; stock=" + nullSafe(reference == null ? null : reference.getStockId()) - + "; space=" + nullSafe(reference == null ? null : reference.getSpaceId()) - + "; requestedQuantity=" + objectOrEmpty( - reference == null ? null : reference.getRequestedQuantity()) - + "; inputSnapshotSync=" + nullSafe( - reference == null ? null : reference.getSnapshotSynchronizedAt()) - + "; shadowMatches=" + numberOrUnknown( - reference == null ? null : reference.getShadowMatchCount()) - + "; reason=" + abbreviate(nullSafe(resultReason), 600); + StringBuilder diagnostic = snapshotDiagnostic(snapshot); + appendField(diagnostic, "detail", requestId, IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(), + IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(), + WIDE_IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "product", reference == null ? null : reference.getProductId(), + WIDE_IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "tray", reference == null ? null : reference.getTrayNo(), + WIDE_IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "stock", reference == null ? null : reference.getStockId(), + IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "space", reference == null ? null : reference.getSpaceId(), + IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "requestedQuantity", objectOrEmpty( + reference == null ? null : reference.getRequestedQuantity()), NUMBER_MAX_LENGTH); + appendField(diagnostic, "inputSnapshotSync", + reference == null ? null : reference.getSnapshotSynchronizedAt(), SNAPSHOT_SYNC_MAX_LENGTH); + appendField(diagnostic, "shadowMatches", numberOrUnknown( + reference == null ? null : reference.getShadowMatchCount()), NUMBER_MAX_LENGTH); + appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH); + return diagnostic.toString(); } private String inboundDiagnostic(PublishedShadowSnapshot snapshot, String taskNo, String trayNo, ShadowInboundReference reference, String resultReason) { - return snapshotDiagnostic(snapshot) - + "; task=" + nullSafe(taskNo) - + "; tray=" + nullSafe(trayNo) - + "; request=" + nullSafe(reference == null ? null : reference.getRequestId()) - + "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId()) - + "; project=" + nullSafe(reference == null ? null : reference.getProjectNo()) - + "; product=" + nullSafe(reference == null ? null : reference.getProductId()) - + "; lot=" + nullSafe(reference == null ? null : reference.getLotNo()) - + "; shadowCandidates=" + numberOrUnknown( - reference == null ? null : reference.getShadowCandidateCount()) - + "; reason=" + abbreviate(nullSafe(resultReason), 600); + StringBuilder diagnostic = snapshotDiagnostic(snapshot); + appendField(diagnostic, "task", taskNo, IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "tray", trayNo, WIDE_IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "request", reference == null ? null : reference.getRequestId(), + IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(), + IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(), + WIDE_IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "product", reference == null ? null : reference.getProductId(), + WIDE_IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "lot", reference == null ? null : reference.getLotNo(), + IDENTIFIER_MAX_LENGTH); + appendField(diagnostic, "shadowCandidates", numberOrUnknown( + reference == null ? null : reference.getShadowCandidateCount()), NUMBER_MAX_LENGTH); + appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH); + return diagnostic.toString(); } - private String snapshotDiagnostic(PublishedShadowSnapshot snapshot) { - return "snapshotGeneration=" + snapshot.generation() - + "; snapshotSync=" + instantOrEmpty(snapshot.synchronizedAt()) - + "; sourceSignature=" + nullSafe(snapshot.sourceSignature()); + private String inboundComparisonReason(InboundShadowObservation observation, + String legacyResultCode, String legacySpaceId) { + StringBuilder reason = new StringBuilder(); + appendField(reason, "snapshotGeneration", Long.toString(observation.snapshotGeneration()), + NUMBER_MAX_LENGTH); + appendField(reason, "request", observation.requestId(), IDENTIFIER_MAX_LENGTH); + appendField(reason, "shadow", observation.resultCode(), IDENTIFIER_MAX_LENGTH); + appendField(reason, "proposedSpace", observation.proposedSpaceId(), IDENTIFIER_MAX_LENGTH); + appendField(reason, "legacy", legacyResultCode, IDENTIFIER_MAX_LENGTH); + appendField(reason, "legacySpace", legacySpaceId, IDENTIFIER_MAX_LENGTH); + appendRemainingField(reason, "shadowReason", observation.reason()); + return reason.toString(); + } + + private String outboundComparisonReason(OutboundShadowObservation observation, String legacyResultCode) { + StringBuilder reason = new StringBuilder(); + appendField(reason, "snapshotGeneration", Long.toString(observation.snapshotGeneration()), + NUMBER_MAX_LENGTH); + appendField(reason, "request", observation.requestId(), IDENTIFIER_MAX_LENGTH); + appendField(reason, "shadow", observation.resultCode(), IDENTIFIER_MAX_LENGTH); + appendField(reason, "proposedStock", observation.proposedStockId(), IDENTIFIER_MAX_LENGTH); + appendField(reason, "proposedSpace", observation.proposedSpaceId(), IDENTIFIER_MAX_LENGTH); + appendField(reason, "legacy", legacyResultCode, IDENTIFIER_MAX_LENGTH); + appendRemainingField(reason, "shadowReason", observation.reason()); + return reason.toString(); + } + + private StringBuilder snapshotDiagnostic(PublishedShadowSnapshot snapshot) { + StringBuilder diagnostic = new StringBuilder(); + appendField(diagnostic, "snapshotGeneration", Long.toString(snapshot.generation()), NUMBER_MAX_LENGTH); + appendField(diagnostic, "snapshotSync", instantOrEmpty(snapshot.synchronizedAt()), + SNAPSHOT_SYNC_MAX_LENGTH); + appendField(diagnostic, "sourceSignature", snapshot.sourceSignature(), SOURCE_SIGNATURE_MAX_LENGTH); + return diagnostic; + } + + private void appendField(StringBuilder target, String name, String value, int maxValueLength) { + if (target.length() > 0) { + target.append("; "); + } + target.append(name).append('=').append(abbreviate(nullSafe(value), maxValueLength)); + } + + private void appendRemainingField(StringBuilder target, String name, String value) { + String prefix = target.length() == 0 ? name + "=" : "; " + name + "="; + int remaining = Math.max(0, DECISION_REASON_MAX_LENGTH - target.length() - prefix.length()); + target.append(prefix).append(abbreviate(nullSafe(value), remaining)); } private void requireUppercaseHexSignature(String value, String description) { @@ -356,7 +406,15 @@ public class ShadowPlanningEngine { } private String abbreviate(String value, int maxLength) { - return value.length() <= maxLength ? value : value.substring(0, maxLength); + if (value.length() <= maxLength) { + return value; + } + int end = maxLength; + if (end > 0 && Character.isHighSurrogate(value.charAt(end - 1)) + && Character.isLowSurrogate(value.charAt(end))) { + end--; + } + return value.substring(0, end); } private String comparisonCode(String shadowSpaceId, String legacySpaceId) { diff --git a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java index 7448128..8bf35c7 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java @@ -212,13 +212,22 @@ class ShadowPlanningEngineTest { ShadowInputMapper mapper = mock(ShadowInputMapper.class); when(mapper.insertComparison(any(), any(), any(), any(Long.class), any(Instant.class), any(), any(), any())).thenReturn(1); - ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper); - String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z" - + "; sourceSignature=ABC123; project=" + "中".repeat(255) - + "; shadowCandidates=17; reason=" + "长".repeat(5000); - InboundShadowObservation observation = new InboundShadowObservation( - true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(), - "JOB-1", "READY", "SPACE-9", shadowDiagnostic); + SchedulingFacade facade = mock(SchedulingFacade.class); + String oversized = "中".repeat(5000); + ShadowInboundReference reference = inboundReference(oversized); + reference.setRequestId(oversized); + reference.setProjectNo(oversized); + reference.setProductId(oversized); + reference.setLotNo(oversized); + reference.setShadowCandidateCount(17); + when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(reference); + when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT)); + when(facade.planInboundFromPublishedSnapshot(eq("01"), any())) + .thenReturn(new InboundPlanningResult(InboundPlan.assigned(inboundAssignment()), + VersionToken.numeric(8))); + ShadowPlanningEngine engine = engine(facade, mapper); + + InboundShadowObservation observation = engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1"); engine.compareInbound(observation, "READY", "LEGACY-SPACE-9"); @@ -236,13 +245,23 @@ class ShadowPlanningEngineTest { ShadowInputMapper mapper = mock(ShadowInputMapper.class); when(mapper.insertComparison(any(), any(), any(), any(Long.class), any(Instant.class), any(), any(), any())).thenReturn(1); - ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper); - String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z" - + "; sourceSignature=ABC123; project=" + "中".repeat(255) - + "; shadowMatches=23; reason=" + "长".repeat(5000); - OutboundShadowObservation observation = new OutboundShadowObservation( - true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(), - "DETAIL-1", "READY", "STOCK-1", "SPACE-9", shadowDiagnostic); + SchedulingFacade facade = mock(SchedulingFacade.class); + String oversized = "中".repeat(5000); + ShadowStockReference reference = projectReference(oversized); + reference.setRequestId(oversized); + reference.setProjectNo(oversized); + reference.setProductId(oversized); + reference.setTrayNo(oversized); + reference.setStockId(oversized); + reference.setSpaceId(oversized); + reference.setShadowMatchCount(23); + when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference); + when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT)); + when(facade.planOutboundFromPublishedSnapshot(eq("01"), any())) + .thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero())); + ShadowPlanningEngine engine = engine(facade, mapper); + + OutboundShadowObservation observation = engine.planProjectOutbound(SNAPSHOT, "DETAIL-1"); engine.compareOutbound(observation, "10");