feat: plan shadow decisions from leased generation
This commit is contained in:
@@ -2,18 +2,25 @@ package com.ruoyi.algorithm.shadow;
|
||||
|
||||
public record InboundShadowObservation(
|
||||
boolean observed,
|
||||
long snapshotGeneration,
|
||||
String warehouseId,
|
||||
String requestId,
|
||||
String resultCode,
|
||||
String proposedSpaceId,
|
||||
String reason) {
|
||||
|
||||
public static InboundShadowObservation empty() {
|
||||
return new InboundShadowObservation(false, null, null, "NOT_OBSERVED", null, null);
|
||||
public InboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
||||
String resultCode, String proposedSpaceId, String reason) {
|
||||
this(observed, 0L, warehouseId, requestId, resultCode, proposedSpaceId, reason);
|
||||
}
|
||||
|
||||
public static InboundShadowObservation unavailable(String warehouseId, String requestId,
|
||||
String resultCode, String reason) {
|
||||
return new InboundShadowObservation(true, warehouseId, requestId, resultCode, null, reason);
|
||||
public static InboundShadowObservation empty() {
|
||||
return new InboundShadowObservation(false, 0L, null, null, "NOT_OBSERVED", null, null);
|
||||
}
|
||||
|
||||
public static InboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
||||
String requestId, String resultCode, String reason) {
|
||||
return new InboundShadowObservation(true, snapshotGeneration, warehouseId, requestId,
|
||||
resultCode, null, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.algorithm.shadow;
|
||||
|
||||
public record OutboundShadowObservation(
|
||||
boolean observed,
|
||||
long snapshotGeneration,
|
||||
String warehouseId,
|
||||
String requestId,
|
||||
String resultCode,
|
||||
@@ -9,12 +10,18 @@ public record OutboundShadowObservation(
|
||||
String proposedSpaceId,
|
||||
String reason) {
|
||||
|
||||
public static OutboundShadowObservation empty() {
|
||||
return new OutboundShadowObservation(false, null, null, "NOT_OBSERVED", null, null, null);
|
||||
public OutboundShadowObservation(boolean observed, String warehouseId, String requestId,
|
||||
String resultCode, String proposedStockId, String proposedSpaceId, String reason) {
|
||||
this(observed, 0L, warehouseId, requestId, resultCode, proposedStockId, proposedSpaceId, reason);
|
||||
}
|
||||
|
||||
public static OutboundShadowObservation unavailable(String warehouseId, String requestId,
|
||||
String resultCode, String reason) {
|
||||
return new OutboundShadowObservation(true, warehouseId, requestId, resultCode, null, null, reason);
|
||||
public static OutboundShadowObservation empty() {
|
||||
return new OutboundShadowObservation(false, 0L, null, null, "NOT_OBSERVED", null, null, null);
|
||||
}
|
||||
|
||||
public static OutboundShadowObservation unavailable(long snapshotGeneration, String warehouseId,
|
||||
String requestId, String resultCode, String reason) {
|
||||
return new OutboundShadowObservation(true, snapshotGeneration, warehouseId, requestId,
|
||||
resultCode, null, null, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ public class ShadowInboundReference {
|
||||
private String projectNo;
|
||||
private String productId;
|
||||
private String lotNo;
|
||||
private Integer shadowCandidateCount;
|
||||
|
||||
public String getRequestId() {
|
||||
return requestId;
|
||||
@@ -55,4 +56,12 @@ public class ShadowInboundReference {
|
||||
public void setLotNo(String lotNo) {
|
||||
this.lotNo = lotNo;
|
||||
}
|
||||
|
||||
public Integer getShadowCandidateCount() {
|
||||
return shadowCandidateCount;
|
||||
}
|
||||
|
||||
public void setShadowCandidateCount(Integer shadowCandidateCount) {
|
||||
this.shadowCandidateCount = shadowCandidateCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,17 @@ import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Component
|
||||
public class ShadowPlanningEngine {
|
||||
private static final int INSTANCE_STALE_SECONDS = 120;
|
||||
private static final String PUBLISHED = "PUBLISHED";
|
||||
|
||||
private final SchedulingFacade schedulingFacade;
|
||||
private final ShadowInputMapper inputMapper;
|
||||
private final ShadowObservationProperties properties;
|
||||
@@ -35,159 +40,313 @@ public class ShadowPlanningEngine {
|
||||
}
|
||||
|
||||
public InboundShadowObservation planInbound(String taskNo, String trayNo) {
|
||||
return transactions.execute(status -> planInboundInsideTransaction(taskNo, trayNo));
|
||||
throw leasedSnapshotRequired();
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planProjectOutbound(String stockoutDetailId) {
|
||||
return transactions.execute(status -> {
|
||||
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(stockoutDetailId);
|
||||
String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId());
|
||||
if (reference == null || isBlank(reference.getProjectNo())) {
|
||||
return OutboundShadowObservation.unavailable(warehouseId, stockoutDetailId,
|
||||
"SKIPPED_MISSING_PROJECT", "No project number was available for shadow planning");
|
||||
}
|
||||
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
||||
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
||||
return toOutboundObservation(warehouseId, reference.getRequestId(), reference,
|
||||
schedulingFacade.planOutbound(warehouseId, request));
|
||||
});
|
||||
throw leasedSnapshotRequired();
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planExactStockOutbound(String stockoutDetailId, String stockId) {
|
||||
throw leasedSnapshotRequired();
|
||||
}
|
||||
|
||||
public PublishedShadowSnapshot refreshSnapshot(long generation, String instanceId) {
|
||||
if (generation <= 0) {
|
||||
throw new IllegalArgumentException("shadow snapshot generation must be positive");
|
||||
}
|
||||
if (isBlank(instanceId)) {
|
||||
throw new IllegalArgumentException("shadow snapshot instanceId is required");
|
||||
}
|
||||
String warehouseId = configuredWarehouseId();
|
||||
return transactions.execute(status -> {
|
||||
ShadowStockReference reference = inputMapper.selectStockReference(stockId);
|
||||
String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId());
|
||||
if (!inputMapper.claimInstance(warehouseId, instanceId, INSTANCE_STALE_SECONDS)) {
|
||||
throw new IllegalStateException("could not claim shadow snapshot refresh for warehouse " + warehouseId);
|
||||
}
|
||||
inputMapper.refreshSnapshot(warehouseId, generation, instanceId);
|
||||
return toPublishedSnapshot(inputMapper.selectPublishedSnapshot(warehouseId), generation, warehouseId);
|
||||
});
|
||||
}
|
||||
|
||||
public InboundShadowObservation planInbound(PublishedShadowSnapshot snapshot,
|
||||
String taskNo, String trayNo) {
|
||||
return planWithStableSnapshot(snapshot,
|
||||
() -> planInboundInsideTransaction(snapshot, taskNo, trayNo));
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planProjectOutbound(PublishedShadowSnapshot snapshot,
|
||||
String stockoutDetailId) {
|
||||
return planWithStableSnapshot(snapshot, () -> {
|
||||
String warehouseId = snapshot.warehouseId();
|
||||
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(
|
||||
warehouseId, snapshot.generation(), stockoutDetailId);
|
||||
if (reference == null || isBlank(reference.getProjectNo())) {
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
||||
stockoutDetailId, "SKIPPED_MISSING_PROJECT",
|
||||
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||
"No project number was available for shadow planning"));
|
||||
}
|
||||
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
||||
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
||||
return toOutboundObservation(snapshot, reference.getRequestId(), reference,
|
||||
schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request));
|
||||
});
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planExactStockOutbound(PublishedShadowSnapshot snapshot,
|
||||
String stockoutDetailId, String stockId) {
|
||||
return planWithStableSnapshot(snapshot, () -> {
|
||||
String warehouseId = snapshot.warehouseId();
|
||||
ShadowStockReference reference = inputMapper.selectStockReference(
|
||||
warehouseId, snapshot.generation(), stockId);
|
||||
if (reference == null || isBlank(reference.getTrayNo())) {
|
||||
return OutboundShadowObservation.unavailable(warehouseId, stockoutDetailId,
|
||||
"SKIPPED_MISSING_STOCK", "Selected stock was unavailable to shadow planning");
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
||||
stockoutDetailId, "SKIPPED_MISSING_STOCK",
|
||||
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||
"Selected stock was unavailable to shadow planning"));
|
||||
}
|
||||
OutboundRequest request = new OutboundRequest(stockoutDetailId, reference.getTrayNo(),
|
||||
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
||||
return toOutboundObservation(warehouseId, stockoutDetailId, reference,
|
||||
schedulingFacade.planOutbound(warehouseId, request));
|
||||
return toOutboundObservation(snapshot, stockoutDetailId, reference,
|
||||
schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request));
|
||||
});
|
||||
}
|
||||
|
||||
public void compareInbound(InboundShadowObservation observation, String legacyResultCode,
|
||||
String legacySpaceId) {
|
||||
transactions.executeWithoutResult(status -> inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
||||
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
||||
"request=" + observation.requestId() + "; shadow=" + observation.resultCode()
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)
|
||||
+ "; legacySpace=" + nullSafe(legacySpaceId)));
|
||||
Objects.requireNonNull(observation, "observation");
|
||||
transactions.executeWithoutResult(status -> {
|
||||
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
||||
"published snapshot changed before comparison");
|
||||
inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "INBOUND_COMPARE",
|
||||
observation.snapshotGeneration(),
|
||||
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
||||
"snapshotGeneration=" + observation.snapshotGeneration()
|
||||
+ "; request=" + observation.requestId()
|
||||
+ "; shadow=" + observation.resultCode()
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)
|
||||
+ "; legacySpace=" + nullSafe(legacySpaceId));
|
||||
});
|
||||
}
|
||||
|
||||
public void compareOutbound(OutboundShadowObservation observation, String legacyResultCode) {
|
||||
transactions.executeWithoutResult(status -> inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
||||
"LEGACY_" + nullSafe(legacyResultCode),
|
||||
"request=" + observation.requestId() + "; shadow=" + observation.resultCode()
|
||||
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)));
|
||||
Objects.requireNonNull(observation, "observation");
|
||||
transactions.executeWithoutResult(status -> {
|
||||
requirePublishedGeneration(observation.snapshotGeneration(), observation.warehouseId(),
|
||||
"published snapshot changed before comparison");
|
||||
inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
||||
observation.snapshotGeneration(), "LEGACY_" + nullSafe(legacyResultCode),
|
||||
"snapshotGeneration=" + observation.snapshotGeneration()
|
||||
+ "; request=" + observation.requestId()
|
||||
+ "; shadow=" + observation.resultCode()
|
||||
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; shadowReason=" + nullSafe(observation.reason())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode));
|
||||
});
|
||||
}
|
||||
|
||||
private InboundShadowObservation planInboundInsideTransaction(String taskNo, String trayNo) {
|
||||
ShadowInboundReference reference = inputMapper.selectInboundInput(taskNo, trayNo);
|
||||
String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId());
|
||||
private InboundShadowObservation planInboundInsideTransaction(PublishedShadowSnapshot snapshot,
|
||||
String taskNo, String trayNo) {
|
||||
String warehouseId = snapshot.warehouseId();
|
||||
ShadowInboundReference reference = inputMapper.selectInboundInput(
|
||||
warehouseId, snapshot.generation(), taskNo, trayNo);
|
||||
if (reference == null || isBlank(reference.getRequestId()) || isBlank(reference.getTrayNo())) {
|
||||
return InboundShadowObservation.unavailable(warehouseId, taskNo,
|
||||
"SKIPPED_MISSING_INPUT", "Inbound task details were unavailable to shadow planning");
|
||||
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId, taskNo,
|
||||
"SKIPPED_MISSING_INPUT",
|
||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference,
|
||||
"Inbound task details were unavailable to shadow planning"));
|
||||
}
|
||||
InboundRequest request = new InboundRequest(reference.getRequestId(), reference.getTrayNo(),
|
||||
reference.getProjectNo(), reference.getProductId(), reference.getLotNo(),
|
||||
null, null, null, Set.of(), null);
|
||||
InboundPlanningResult result = schedulingFacade.planInbound(warehouseId, request);
|
||||
InboundPlanningResult result = schedulingFacade.planInboundFromPublishedSnapshot(warehouseId, request);
|
||||
InboundPlan plan = result.plan();
|
||||
if (plan.assignment().isPresent()) {
|
||||
var assignment = plan.assignment().orElseThrow();
|
||||
return new InboundShadowObservation(true, warehouseId, reference.getRequestId(),
|
||||
"READY", assignment.spaceId(), inboundDiagnostic(taskNo, trayNo, reference, assignment.reason()));
|
||||
return new InboundShadowObservation(true, snapshot.generation(), warehouseId,
|
||||
reference.getRequestId(), "READY", assignment.spaceId(),
|
||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference, assignment.reason()));
|
||||
}
|
||||
var rejection = plan.rejection().orElseThrow();
|
||||
return InboundShadowObservation.unavailable(warehouseId, reference.getRequestId(),
|
||||
"REJECTED_" + rejection.code().name(),
|
||||
inboundDiagnostic(taskNo, trayNo, reference, rejection.reason()));
|
||||
return InboundShadowObservation.unavailable(snapshot.generation(), warehouseId,
|
||||
reference.getRequestId(), "REJECTED_" + rejection.code().name(),
|
||||
inboundDiagnostic(snapshot, taskNo, trayNo, reference, rejection.reason()));
|
||||
}
|
||||
|
||||
private OutboundShadowObservation toOutboundObservation(String warehouseId, String requestId,
|
||||
ShadowStockReference reference, OutboundPlan plan) {
|
||||
private OutboundShadowObservation toOutboundObservation(PublishedShadowSnapshot snapshot,
|
||||
String requestId, ShadowStockReference reference, OutboundPlan plan) {
|
||||
if (isBlank(requestId)) {
|
||||
requestId = plan.assignment().map(assignment -> assignment.requestId()).orElse(null);
|
||||
}
|
||||
if (plan.assignment().isPresent()) {
|
||||
var assignment = plan.assignment().orElseThrow();
|
||||
return new OutboundShadowObservation(true, warehouseId, requestId, "READY",
|
||||
reference.getStockId(), assignment.sourceSpaceId(),
|
||||
outboundDiagnostic(requestId, reference, assignment.reason()));
|
||||
return new OutboundShadowObservation(true, snapshot.generation(), snapshot.warehouseId(),
|
||||
requestId, "READY", reference.getStockId(), assignment.sourceSpaceId(),
|
||||
outboundDiagnostic(snapshot, requestId, reference, assignment.reason()));
|
||||
}
|
||||
if (plan.waiting().isPresent()) {
|
||||
var waiting = plan.waiting().orElseThrow();
|
||||
return OutboundShadowObservation.unavailable(warehouseId, requestId,
|
||||
"WAITING_" + waiting.code().name(), outboundDiagnostic(requestId, reference, waiting.reason()));
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
||||
requestId, "WAITING_" + waiting.code().name(),
|
||||
outboundDiagnostic(snapshot, requestId, reference, waiting.reason()));
|
||||
}
|
||||
var error = plan.error().orElseThrow();
|
||||
return OutboundShadowObservation.unavailable(warehouseId, requestId,
|
||||
"ERROR_" + error.code().name(), outboundDiagnostic(requestId, reference, error.message()));
|
||||
return OutboundShadowObservation.unavailable(snapshot.generation(), snapshot.warehouseId(),
|
||||
requestId, "ERROR_" + error.code().name(),
|
||||
outboundDiagnostic(snapshot, requestId, reference, error.message()));
|
||||
}
|
||||
|
||||
private String warehouseId(String preferredWarehouseId) {
|
||||
String configuredWarehouseId = isBlank(properties.getWarehouseId())
|
||||
private <T> T planWithStableSnapshot(PublishedShadowSnapshot snapshot, Supplier<T> planner) {
|
||||
validateLeasedSnapshot(snapshot);
|
||||
return transactions.execute(status -> {
|
||||
requirePublishedSnapshot(snapshot, "published snapshot changed during planning");
|
||||
T result = planner.get();
|
||||
requirePublishedSnapshot(snapshot, "published snapshot changed during planning");
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void validateLeasedSnapshot(PublishedShadowSnapshot snapshot) {
|
||||
Objects.requireNonNull(snapshot, "snapshot");
|
||||
if (snapshot.generation() <= 0) {
|
||||
throw new IllegalArgumentException("leased shadow snapshot generation must be positive");
|
||||
}
|
||||
if (isBlank(snapshot.warehouseId())) {
|
||||
throw new IllegalArgumentException("leased shadow snapshot warehouse is required");
|
||||
}
|
||||
if (!WarehouseIdNormalizer.equivalent(snapshot.warehouseId(), configuredWarehouseId())) {
|
||||
throw new IllegalArgumentException("leased shadow snapshot warehouse does not match configured warehouse");
|
||||
}
|
||||
requireUppercaseHexSignature(snapshot.sourceSignature(), "leased shadow snapshot source signature");
|
||||
}
|
||||
|
||||
private PublishedShadowSnapshot toPublishedSnapshot(ShadowSnapshotDescriptor descriptor,
|
||||
long expectedGeneration, String expectedWarehouseId) {
|
||||
if (descriptor == null) {
|
||||
throw new IllegalStateException("published snapshot descriptor is missing");
|
||||
}
|
||||
if (!PUBLISHED.equals(descriptor.getSnapshotStatus())) {
|
||||
throw new IllegalStateException("shadow snapshot descriptor must be PUBLISHED");
|
||||
}
|
||||
if (descriptor.getSnapshotGeneration() != expectedGeneration) {
|
||||
throw new IllegalStateException("published snapshot generation does not match refreshed generation");
|
||||
}
|
||||
if (!WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), expectedWarehouseId)) {
|
||||
throw new IllegalStateException("published snapshot warehouse does not match refreshed warehouse");
|
||||
}
|
||||
if (descriptor.getSynchronizedAt() == null) {
|
||||
throw new IllegalStateException("published snapshot synchronization time is missing");
|
||||
}
|
||||
requireUppercaseHexSignature(descriptor.getSourceSignature(), "published snapshot source signature");
|
||||
return new PublishedShadowSnapshot(descriptor.getSnapshotGeneration(), descriptor.getWarehouseId(),
|
||||
descriptor.getSynchronizedAt(), descriptor.getSourceSignature());
|
||||
}
|
||||
|
||||
private void requirePublishedSnapshot(PublishedShadowSnapshot snapshot, String message) {
|
||||
if (!matches(snapshot, inputMapper.selectPublishedSnapshot(snapshot.warehouseId()))) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void requirePublishedGeneration(long generation, String warehouseId, String message) {
|
||||
if (generation <= 0 || isBlank(warehouseId)) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
ShadowSnapshotDescriptor descriptor = inputMapper.selectPublishedSnapshot(warehouseId);
|
||||
if (descriptor == null
|
||||
|| !PUBLISHED.equals(descriptor.getSnapshotStatus())
|
||||
|| descriptor.getSnapshotGeneration() != generation
|
||||
|| !WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), warehouseId)) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matches(PublishedShadowSnapshot snapshot, ShadowSnapshotDescriptor descriptor) {
|
||||
return descriptor != null
|
||||
&& PUBLISHED.equals(descriptor.getSnapshotStatus())
|
||||
&& descriptor.getSnapshotGeneration() == snapshot.generation()
|
||||
&& WarehouseIdNormalizer.equivalent(descriptor.getWarehouseId(), snapshot.warehouseId())
|
||||
&& Objects.equals(descriptor.getSynchronizedAt(), snapshot.synchronizedAt())
|
||||
&& Objects.equals(descriptor.getSourceSignature(), snapshot.sourceSignature());
|
||||
}
|
||||
|
||||
private String configuredWarehouseId() {
|
||||
String warehouseId = isBlank(properties.getWarehouseId())
|
||||
? schedulingFacade.defaultWarehouseId()
|
||||
: properties.getWarehouseId();
|
||||
if (!isBlank(preferredWarehouseId)) {
|
||||
String normalizedPreferred = preferredWarehouseId.trim();
|
||||
if (sameNumericWarehouse(normalizedPreferred, configuredWarehouseId)) {
|
||||
return configuredWarehouseId;
|
||||
}
|
||||
return normalizedPreferred;
|
||||
if (isBlank(warehouseId)) {
|
||||
throw new IllegalStateException("shadow warehouse is not configured");
|
||||
}
|
||||
return configuredWarehouseId;
|
||||
return warehouseId.trim();
|
||||
}
|
||||
|
||||
private boolean sameNumericWarehouse(String first, String second) {
|
||||
if (isBlank(first) || isBlank(second)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(first) == Integer.parseInt(second);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return false;
|
||||
}
|
||||
private IllegalStateException leasedSnapshotRequired() {
|
||||
return new IllegalStateException("published shadow snapshot lease is required");
|
||||
}
|
||||
|
||||
private String outboundDiagnostic(String requestId, ShadowStockReference reference, String resultReason) {
|
||||
return "detail=" + nullSafe(requestId)
|
||||
+ "; project=" + nullSafe(reference.getProjectNo())
|
||||
+ "; product=" + nullSafe(reference.getProductId())
|
||||
+ "; tray=" + nullSafe(reference.getTrayNo())
|
||||
+ "; stock=" + nullSafe(reference.getStockId())
|
||||
+ "; snapshotSync=" + nullSafe(reference.getSnapshotSynchronizedAt())
|
||||
+ "; legacyMatches=" + numberOrUnknown(reference.getLiveMatchCount())
|
||||
+ "; shadowMatches=" + numberOrUnknown(reference.getShadowMatchCount())
|
||||
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);
|
||||
}
|
||||
|
||||
private String inboundDiagnostic(String taskNo, String trayNo, ShadowInboundReference reference,
|
||||
String resultReason) {
|
||||
return "task=" + nullSafe(taskNo)
|
||||
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.getRequestId())
|
||||
+ "; warehouse=" + nullSafe(reference.getWarehouseId())
|
||||
+ "; project=" + nullSafe(reference.getProjectNo())
|
||||
+ "; product=" + nullSafe(reference.getProductId())
|
||||
+ "; lot=" + nullSafe(reference.getLotNo())
|
||||
+ "; 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);
|
||||
}
|
||||
|
||||
private String snapshotDiagnostic(PublishedShadowSnapshot snapshot) {
|
||||
return "snapshotGeneration=" + snapshot.generation()
|
||||
+ "; snapshotSync=" + instantOrEmpty(snapshot.synchronizedAt())
|
||||
+ "; sourceSignature=" + nullSafe(snapshot.sourceSignature());
|
||||
}
|
||||
|
||||
private void requireUppercaseHexSignature(String value, String description) {
|
||||
if (isBlank(value) || !value.matches("[0-9A-F]+")) {
|
||||
throw new IllegalStateException(description + " must be uppercase hexadecimal");
|
||||
}
|
||||
}
|
||||
|
||||
private String numberOrUnknown(Integer value) {
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
private String objectOrEmpty(Object value) {
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
private String instantOrEmpty(Instant value) {
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
private String abbreviate(String value, int maxLength) {
|
||||
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class ShadowSnapshotDescriptor {
|
||||
private long snapshotGeneration;
|
||||
private String warehouseId;
|
||||
private String snapshotStatus;
|
||||
private Instant synchronizedAt;
|
||||
private String sourceSignature;
|
||||
|
||||
public long getSnapshotGeneration() {
|
||||
return snapshotGeneration;
|
||||
}
|
||||
|
||||
public void setSnapshotGeneration(long snapshotGeneration) {
|
||||
this.snapshotGeneration = snapshotGeneration;
|
||||
}
|
||||
|
||||
public String getWarehouseId() {
|
||||
return warehouseId;
|
||||
}
|
||||
|
||||
public void setWarehouseId(String warehouseId) {
|
||||
this.warehouseId = warehouseId;
|
||||
}
|
||||
|
||||
public String getSnapshotStatus() {
|
||||
return snapshotStatus;
|
||||
}
|
||||
|
||||
public void setSnapshotStatus(String snapshotStatus) {
|
||||
this.snapshotStatus = snapshotStatus;
|
||||
}
|
||||
|
||||
public Instant getSynchronizedAt() {
|
||||
return synchronizedAt;
|
||||
}
|
||||
|
||||
public void setSynchronizedAt(Instant synchronizedAt) {
|
||||
this.synchronizedAt = synchronizedAt;
|
||||
}
|
||||
|
||||
public String getSourceSignature() {
|
||||
return sourceSignature;
|
||||
}
|
||||
|
||||
public void setSourceSignature(String sourceSignature) {
|
||||
this.sourceSignature = sourceSignature;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class ShadowStockReference {
|
||||
private String requestId;
|
||||
private String stockId;
|
||||
@@ -9,7 +11,7 @@ public class ShadowStockReference {
|
||||
private String projectNo;
|
||||
private String productId;
|
||||
private String snapshotSynchronizedAt;
|
||||
private Integer liveMatchCount;
|
||||
private BigDecimal requestedQuantity;
|
||||
private Integer shadowMatchCount;
|
||||
|
||||
public String getRequestId() {
|
||||
@@ -76,12 +78,12 @@ public class ShadowStockReference {
|
||||
this.snapshotSynchronizedAt = snapshotSynchronizedAt;
|
||||
}
|
||||
|
||||
public Integer getLiveMatchCount() {
|
||||
return liveMatchCount;
|
||||
public BigDecimal getRequestedQuantity() {
|
||||
return requestedQuantity;
|
||||
}
|
||||
|
||||
public void setLiveMatchCount(Integer liveMatchCount) {
|
||||
this.liveMatchCount = liveMatchCount;
|
||||
public void setRequestedQuantity(BigDecimal requestedQuantity) {
|
||||
this.requestedQuantity = requestedQuantity;
|
||||
}
|
||||
|
||||
public Integer getShadowMatchCount() {
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
package com.ruoyi.algorithm.shadow.mapper;
|
||||
|
||||
import com.ruoyi.algorithm.shadow.ShadowInboundReference;
|
||||
import com.ruoyi.algorithm.shadow.ShadowSnapshotDescriptor;
|
||||
import com.ruoyi.algorithm.shadow.ShadowStockReference;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface ShadowInputMapper {
|
||||
ShadowInboundReference selectInboundInput(@Param("taskNo") String taskNo,
|
||||
ShadowInboundReference selectInboundInput(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("taskNo") String taskNo,
|
||||
@Param("trayNo") String trayNo);
|
||||
|
||||
ShadowStockReference selectProjectOutboundReference(
|
||||
ShadowStockReference selectProjectOutboundReference(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation,
|
||||
@Param("stockoutDetailId") String stockoutDetailId);
|
||||
|
||||
ShadowStockReference selectStockReference(@Param("stockId") String stockId);
|
||||
ShadowStockReference selectStockReference(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("stockId") String stockId);
|
||||
|
||||
boolean claimInstance(@Param("warehouseId") String warehouseId,
|
||||
@Param("instanceId") String instanceId, @Param("staleSeconds") int staleSeconds);
|
||||
|
||||
void refreshSnapshot(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("instanceId") String instanceId);
|
||||
|
||||
ShadowSnapshotDescriptor selectPublishedSnapshot(@Param("warehouseId") String warehouseId);
|
||||
|
||||
int insertComparison(@Param("decisionId") String decisionId,
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("decisionType") String decisionType,
|
||||
@Param("inputVersion") long inputVersion,
|
||||
@Param("resultCode") String resultCode,
|
||||
@Param("reason") String reason);
|
||||
}
|
||||
|
||||
@@ -6,77 +6,98 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectInboundInput" resultType="com.ruoyi.algorithm.shadow.ShadowInboundReference">
|
||||
select top (1)
|
||||
job.job_id as requestId,
|
||||
job.warehouse_id as warehouseId,
|
||||
job.tray_code as trayNo,
|
||||
coalesce(nullif(ltrim(rtrim(detail.spare4)), ''),
|
||||
nullif(ltrim(rtrim(stock.spare4)), '')) as projectNo,
|
||||
jobDetail.product_id as productId,
|
||||
jobDetail.lot_no as lotNo
|
||||
from mydo_job job
|
||||
left join mydo_job_detail jobDetail on jobDetail.job_id = job.job_id
|
||||
left join in_stockin_detail detail on detail.stockin_detail_id = jobDetail.stockin_detail_id
|
||||
left join bas_stock stock on stock.stock_id = jobDetail.stock_id
|
||||
where job.task_no = #{taskNo}
|
||||
and job.tray_code = #{trayNo}
|
||||
order by jobDetail.job_detail_id
|
||||
input.request_id as requestId,
|
||||
input.raw_warehouse_id as warehouseId,
|
||||
input.tray_no as trayNo,
|
||||
input.project_no as projectNo,
|
||||
input.product_id as productId,
|
||||
input.lot_no as lotNo,
|
||||
convert(int, coalesce((
|
||||
select sum(convert(bigint, lane.available_count))
|
||||
from alg.lane_state lane
|
||||
where lane.warehouse_id = #{warehouseId}
|
||||
and lane.snapshot_generation = #{generation}
|
||||
), 0)) as shadowCandidateCount
|
||||
from alg.shadow_inbound_request_snapshot input
|
||||
where input.warehouse_id = #{warehouseId}
|
||||
and input.snapshot_generation = #{generation}
|
||||
and input.task_no = #{taskNo}
|
||||
and input.tray_no = #{trayNo}
|
||||
</select>
|
||||
|
||||
<select id="selectProjectOutboundReference" resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
select detail.stockout_detail_id as requestId,
|
||||
detail.warehouse_id as warehouseId,
|
||||
nullif(ltrim(rtrim(detail.spare4)), '') as projectNo,
|
||||
detail.product_id as productId,
|
||||
convert(varchar(40), (select max(snapshot.synchronized_at)
|
||||
from alg.inventory_item_snapshot snapshot
|
||||
where snapshot.warehouse_id = detail.warehouse_id), 126)
|
||||
as snapshotSynchronizedAt,
|
||||
<select id="selectProjectOutboundReference"
|
||||
resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
select top (1)
|
||||
input.stockout_detail_id as requestId,
|
||||
input.raw_warehouse_id as warehouseId,
|
||||
input.project_no as projectNo,
|
||||
input.product_id as productId,
|
||||
input.requested_quantity as requestedQuantity,
|
||||
convert(varchar(40), input.synchronized_at, 126) as snapshotSynchronizedAt,
|
||||
(select count(*)
|
||||
from bas_stock live
|
||||
where live.warehouse_id = detail.warehouse_id
|
||||
and live.stock_status = '1'
|
||||
and live.product_status = '1'
|
||||
and live.quantity > live.out_quantity
|
||||
and (NULLIF(LTRIM(RTRIM(detail.product_id)), '') IS NULL
|
||||
OR live.product_id = NULLIF(LTRIM(RTRIM(detail.product_id)), ''))) as liveMatchCount,
|
||||
(select count(*)
|
||||
from alg.inventory_item_snapshot snapshot
|
||||
where snapshot.warehouse_id = detail.warehouse_id
|
||||
and snapshot.project_no = nullif(ltrim(rtrim(detail.spare4)), '')
|
||||
and (NULLIF(LTRIM(RTRIM(detail.product_id)), '') IS NULL
|
||||
OR snapshot.product_no = NULLIF(LTRIM(RTRIM(detail.product_id)), '')))
|
||||
from alg.inventory_item_snapshot item
|
||||
where item.warehouse_id = #{warehouseId}
|
||||
and item.snapshot_generation = #{generation}
|
||||
and item.project_no = nullif(ltrim(rtrim(input.project_no)), '')
|
||||
and (nullif(ltrim(rtrim(input.product_id)), '') is null
|
||||
or item.product_no = nullif(ltrim(rtrim(input.product_id)), ''))
|
||||
and isnull(item.quantity, 0) > isnull(item.out_quantity, 0))
|
||||
as shadowMatchCount
|
||||
from out_stockout_detail detail
|
||||
where detail.stockout_detail_id = #{stockoutDetailId}
|
||||
from alg.shadow_outbound_request_snapshot input
|
||||
where input.warehouse_id = #{warehouseId}
|
||||
and input.snapshot_generation = #{generation}
|
||||
and input.stockout_detail_id = #{stockoutDetailId}
|
||||
</select>
|
||||
|
||||
<select id="selectStockReference" resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
select top (1)
|
||||
stock.stock_id as stockId,
|
||||
stock.tray_code as trayNo,
|
||||
stock.cargo_space_id as spaceId,
|
||||
stock.warehouse_id as warehouseId,
|
||||
nullif(ltrim(rtrim(stock.spare4)), '') as projectNo,
|
||||
stock.product_id as productId,
|
||||
convert(varchar(40), (select max(snapshot.synchronized_at)
|
||||
from alg.inventory_item_snapshot snapshot
|
||||
where snapshot.warehouse_id = stock.warehouse_id), 126)
|
||||
as snapshotSynchronizedAt,
|
||||
item.stock_id as stockId,
|
||||
item.tray_code as trayNo,
|
||||
item.space_id as spaceId,
|
||||
item.warehouse_id as warehouseId,
|
||||
nullif(ltrim(rtrim(item.project_no)), '') as projectNo,
|
||||
item.product_no as productId,
|
||||
convert(varchar(40), item.synchronized_at, 126) as snapshotSynchronizedAt,
|
||||
(select count(*)
|
||||
from bas_stock live
|
||||
where live.stock_id = stock.stock_id
|
||||
and live.stock_status = '1'
|
||||
and live.product_status = '1'
|
||||
and live.quantity > live.out_quantity) as liveMatchCount,
|
||||
(select count(*)
|
||||
from alg.inventory_item_snapshot snapshot
|
||||
where snapshot.stock_id = stock.stock_id
|
||||
and snapshot.warehouse_id = stock.warehouse_id) as shadowMatchCount
|
||||
from bas_stock stock
|
||||
where stock.stock_id = #{stockId}
|
||||
and stock.stock_status = '1'
|
||||
and stock.product_status = '1'
|
||||
and stock.quantity > stock.out_quantity
|
||||
from alg.inventory_item_snapshot candidate
|
||||
where candidate.warehouse_id = #{warehouseId}
|
||||
and candidate.snapshot_generation = #{generation}
|
||||
and candidate.stock_id = #{stockId}
|
||||
and isnull(candidate.quantity, 0) > isnull(candidate.out_quantity, 0))
|
||||
as shadowMatchCount
|
||||
from alg.inventory_item_snapshot item
|
||||
where item.warehouse_id = #{warehouseId}
|
||||
and item.snapshot_generation = #{generation}
|
||||
and item.stock_id = #{stockId}
|
||||
and isnull(item.quantity, 0) > isnull(item.out_quantity, 0)
|
||||
</select>
|
||||
|
||||
<select id="claimInstance" resultType="boolean">
|
||||
if convert(int, #{staleSeconds}) <> 120
|
||||
throw 51026, 'shadow instance stale threshold must be 120 seconds', 1;
|
||||
declare @claimed bit;
|
||||
exec alg.usp_claim_shadow_instance
|
||||
@warehouse_id = #{warehouseId},
|
||||
@instance_id = #{instanceId},
|
||||
@claimed = @claimed output;
|
||||
</select>
|
||||
|
||||
<update id="refreshSnapshot">
|
||||
exec alg.usp_refresh_shadow_snapshot
|
||||
@warehouse_id = #{warehouseId},
|
||||
@snapshot_generation = #{generation},
|
||||
@instance_id = #{instanceId};
|
||||
</update>
|
||||
|
||||
<select id="selectPublishedSnapshot"
|
||||
resultType="com.ruoyi.algorithm.shadow.ShadowSnapshotDescriptor">
|
||||
select state.snapshot_generation as snapshotGeneration,
|
||||
state.warehouse_id as warehouseId,
|
||||
state.snapshot_status as snapshotStatus,
|
||||
state.synchronized_at as synchronizedAt,
|
||||
convert(varchar(64), state.source_signature, 2) as sourceSignature
|
||||
from alg.shadow_snapshot_state state
|
||||
where state.warehouse_id = #{warehouseId}
|
||||
</select>
|
||||
|
||||
<insert id="insertComparison">
|
||||
@@ -84,6 +105,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
decision_id, warehouse_id, decision_type, input_version, result_code, reason)
|
||||
values (
|
||||
try_convert(uniqueidentifier, #{decisionId}), #{warehouseId}, #{decisionType},
|
||||
null, #{resultCode}, #{reason})
|
||||
convert(binary(8), #{inputVersion}), #{resultCode}, #{reason})
|
||||
</insert>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user