From 3f7ed3718defef2c4ab37e1deb2534d2329747e4 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Fri, 28 Aug 2026 11:55:43 +0800 Subject: [PATCH] fix: improve shadow diagnostic matching --- doc/algorithm-shadow/README.md | 2 + .../shadow/ShadowInboundReference.java | 9 ++ .../shadow/ShadowPlanningEngine.java | 84 +++++++++++++++---- .../shadow/ShadowStockReference.java | 27 ++++++ .../mapper/algorithm/ShadowInputMapper.xml | 38 ++++++++- .../shadow/ShadowPlanningEngineTest.java | 71 +++++++++++++++- .../OutStockoutServiceImplShadowTest.java | 1 + scripts/shadow-export-mcp.js | 2 +- scripts/shadow-followup-mcp.js | 4 +- 9 files changed, 218 insertions(+), 20 deletions(-) diff --git a/doc/algorithm-shadow/README.md b/doc/algorithm-shadow/README.md index 471d754..918093a 100644 --- a/doc/algorithm-shadow/README.md +++ b/doc/algorithm-shadow/README.md @@ -237,6 +237,8 @@ WHERE created_at >= '<测试开始时间>' ORDER BY created_at, decision_id; ``` +出库对比记录的 `reason` 会带出 `detail`、`project`、`product`、`tray`、`stock`、`snapshotSync`、`legacyMatches` 和 `shadowMatches`。其中 `legacyMatches` 是按 Legacy 有效库存条件计算的实时数量,`shadowMatches` 是影子快照中按项目/产品条件计算的数量;两者不一致时先检查快照同步时间,再判断业务映射。 + 交付分析的数据包至少包括: - 决策日志导出文件,保持原始列名和时间字段; diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowInboundReference.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowInboundReference.java index 018c668..15b892c 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowInboundReference.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowInboundReference.java @@ -2,6 +2,7 @@ package com.ruoyi.algorithm.shadow; public class ShadowInboundReference { private String requestId; + private String warehouseId; private String trayNo; private String projectNo; private String productId; @@ -15,6 +16,14 @@ public class ShadowInboundReference { this.requestId = requestId; } + public String getWarehouseId() { + return warehouseId; + } + + public void setWarehouseId(String warehouseId) { + this.warehouseId = warehouseId; + } + public String getTrayNo() { return trayNo; } 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 8389dd9..4f272ca 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 @@ -41,28 +41,30 @@ public class ShadowPlanningEngine { public OutboundShadowObservation planProjectOutbound(String stockoutDetailId) { return transactions.execute(status -> { ShadowStockReference reference = inputMapper.selectProjectOutboundReference(stockoutDetailId); - String warehouseId = warehouseId(); + 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, schedulingFacade.planOutbound(warehouseId, request)); + return toOutboundObservation(warehouseId, reference.getRequestId(), reference, + schedulingFacade.planOutbound(warehouseId, request)); }); } public OutboundShadowObservation planExactStockOutbound(String stockoutDetailId, String stockId) { return transactions.execute(status -> { ShadowStockReference reference = inputMapper.selectStockReference(stockId); - String warehouseId = warehouseId(); + String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId()); if (reference == null || isBlank(reference.getTrayNo())) { return OutboundShadowObservation.unavailable(warehouseId, stockoutDetailId, "SKIPPED_MISSING_STOCK", "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, reference, schedulingFacade.planOutbound(warehouseId, request)); + return toOutboundObservation(warehouseId, stockoutDetailId, reference, + schedulingFacade.planOutbound(warehouseId, request)); }); } @@ -73,6 +75,7 @@ public class ShadowPlanningEngine { comparisonCode(observation.proposedSpaceId(), legacySpaceId), "request=" + observation.requestId() + "; shadow=" + observation.resultCode() + "; proposedSpace=" + nullSafe(observation.proposedSpaceId()) + + "; shadowReason=" + nullSafe(observation.reason()) + "; legacy=" + nullSafe(legacyResultCode) + "; legacySpace=" + nullSafe(legacySpaceId))); } @@ -84,12 +87,13 @@ public class ShadowPlanningEngine { "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) { - String warehouseId = warehouseId(); ShadowInboundReference reference = inputMapper.selectInboundInput(taskNo, trayNo); + String warehouseId = warehouseId(reference == null ? null : reference.getWarehouseId()); 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"); @@ -102,38 +106,90 @@ public class ShadowPlanningEngine { if (plan.assignment().isPresent()) { var assignment = plan.assignment().orElseThrow(); return new InboundShadowObservation(true, warehouseId, reference.getRequestId(), - "READY", assignment.spaceId(), assignment.reason()); + "READY", assignment.spaceId(), inboundDiagnostic(taskNo, trayNo, reference, assignment.reason())); } var rejection = plan.rejection().orElseThrow(); return InboundShadowObservation.unavailable(warehouseId, reference.getRequestId(), - "REJECTED_" + rejection.code().name(), rejection.reason()); + "REJECTED_" + rejection.code().name(), + inboundDiagnostic(taskNo, trayNo, reference, rejection.reason())); } - private OutboundShadowObservation toOutboundObservation(String warehouseId, + private OutboundShadowObservation toOutboundObservation(String warehouseId, String requestId, ShadowStockReference reference, OutboundPlan plan) { - String requestId = reference.getRequestId(); 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(), assignment.reason()); + reference.getStockId(), assignment.sourceSpaceId(), + outboundDiagnostic(requestId, reference, assignment.reason())); } if (plan.waiting().isPresent()) { var waiting = plan.waiting().orElseThrow(); return OutboundShadowObservation.unavailable(warehouseId, requestId, - "WAITING_" + waiting.code().name(), waiting.reason()); + "WAITING_" + waiting.code().name(), outboundDiagnostic(requestId, reference, waiting.reason())); } var error = plan.error().orElseThrow(); return OutboundShadowObservation.unavailable(warehouseId, requestId, - "ERROR_" + error.code().name(), error.message()); + "ERROR_" + error.code().name(), outboundDiagnostic(requestId, reference, error.message())); } - private String warehouseId() { - return isBlank(properties.getWarehouseId()) + private String warehouseId(String preferredWarehouseId) { + String configuredWarehouseId = isBlank(properties.getWarehouseId()) ? schedulingFacade.defaultWarehouseId() : properties.getWarehouseId(); + if (!isBlank(preferredWarehouseId)) { + String normalizedPreferred = preferredWarehouseId.trim(); + if (sameNumericWarehouse(normalizedPreferred, configuredWarehouseId)) { + return configuredWarehouseId; + } + return normalizedPreferred; + } + return configuredWarehouseId; + } + + 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 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()) + + "; reason=" + abbreviate(nullSafe(resultReason), 600); + } + + private String inboundDiagnostic(String taskNo, String trayNo, ShadowInboundReference reference, + String resultReason) { + return "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()) + + "; reason=" + abbreviate(nullSafe(resultReason), 600); + } + + private String numberOrUnknown(Integer value) { + return value == null ? "" : value.toString(); + } + + private String abbreviate(String value, int maxLength) { + return value.length() <= maxLength ? value : value.substring(0, maxLength); } private String comparisonCode(String shadowSpaceId, String legacySpaceId) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java index f8d5efe..4ff57a1 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java @@ -8,6 +8,9 @@ public class ShadowStockReference { private String warehouseId; private String projectNo; private String productId; + private String snapshotSynchronizedAt; + private Integer liveMatchCount; + private Integer shadowMatchCount; public String getRequestId() { return requestId; @@ -64,4 +67,28 @@ public class ShadowStockReference { public void setProductId(String productId) { this.productId = productId; } + + public String getSnapshotSynchronizedAt() { + return snapshotSynchronizedAt; + } + + public void setSnapshotSynchronizedAt(String snapshotSynchronizedAt) { + this.snapshotSynchronizedAt = snapshotSynchronizedAt; + } + + public Integer getLiveMatchCount() { + return liveMatchCount; + } + + public void setLiveMatchCount(Integer liveMatchCount) { + this.liveMatchCount = liveMatchCount; + } + + public Integer getShadowMatchCount() { + return shadowMatchCount; + } + + public void setShadowMatchCount(Integer shadowMatchCount) { + this.shadowMatchCount = shadowMatchCount; + } } diff --git a/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml b/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml index 8975b4f..7a7df18 100644 --- a/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml +++ b/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml @@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" @@ -37,7 +57,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" stock.cargo_space_id as spaceId, stock.warehouse_id as warehouseId, nullif(ltrim(rtrim(stock.spare4)), '') as projectNo, - stock.product_id as productId + 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, + (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' 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 d64cc39..68fef5a 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 @@ -5,6 +5,9 @@ import com.ruoyi.algorithm.inbound.InboundBorrowType; import com.ruoyi.algorithm.inbound.InboundLadderRung; import com.ruoyi.algorithm.inbound.InboundPlan; import com.ruoyi.algorithm.integration.SchedulingFacade; +import com.ruoyi.algorithm.outbound.OutboundAssignment; +import com.ruoyi.algorithm.outbound.OutboundPlan; +import com.ruoyi.algorithm.outbound.FifoDecision; import com.ruoyi.algorithm.repository.VersionToken; import com.ruoyi.algorithm.service.InboundPlanningResult; import com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper; @@ -16,8 +19,10 @@ import org.springframework.transaction.support.SimpleTransactionStatus; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ShadowPlanningEngineTest { @@ -25,13 +30,14 @@ class ShadowPlanningEngineTest { @Test void inboundPlanningUsesRequiresNewAndConfiguredTimeout() { ShadowObservationProperties properties = new ShadowObservationProperties(); - properties.setWarehouseId("W01"); + properties.setWarehouseId("FALLBACK"); properties.setTimeoutSeconds(7); CapturingTransactionManager transactions = new CapturingTransactionManager(); ShadowInputMapper mapper = mock(ShadowInputMapper.class); SchedulingFacade facade = mock(SchedulingFacade.class); ShadowInboundReference input = new ShadowInboundReference(); input.setRequestId("JOB-1"); + input.setWarehouseId("W01"); input.setTrayNo("TRAY-1"); input.setProjectNo("PROJECT-1"); input.setProductId("PRODUCT-1"); @@ -56,6 +62,69 @@ class ShadowPlanningEngineTest { assertThat(observation.proposedSpaceId()).isEqualTo("SPACE-9"); } + @Test + void outboundPlanningUsesReferenceWarehouseAndKeepsMatchDiagnostics() { + ShadowObservationProperties properties = new ShadowObservationProperties(); + properties.setWarehouseId("FALLBACK"); + CapturingTransactionManager transactions = new CapturingTransactionManager(); + ShadowInputMapper mapper = mock(ShadowInputMapper.class); + SchedulingFacade facade = mock(SchedulingFacade.class); + ShadowStockReference reference = new ShadowStockReference(); + reference.setRequestId("DETAIL-1"); + reference.setWarehouseId("W02"); + reference.setProjectNo("PROJECT-1"); + reference.setProductId("PRODUCT-1"); + reference.setSnapshotSynchronizedAt("2026-08-28T02:53:45.133Z"); + reference.setLiveMatchCount(2); + reference.setShadowMatchCount(1); + when(mapper.selectProjectOutboundReference("DETAIL-1")).thenReturn(reference); + OutboundAssignment assignment = new OutboundAssignment( + "DETAIL-1", "TRAY-1", "PROJECT-1", "PRODUCT-1", "SPACE-9", 1, + true, java.util.List.of(), java.util.List.of(), false, + 1.0, 2.0, 3.0, FifoDecision.off(), java.util.Optional.empty(), + "shadow proposal"); + when(facade.planOutbound(eq("W02"), any())) + .thenReturn(OutboundPlan.ready(assignment, com.ruoyi.algorithm.outbound.TaskCountEstimate.zero())); + ShadowPlanningEngine engine = new ShadowPlanningEngine(facade, mapper, properties, transactions); + + OutboundShadowObservation observation = engine.planProjectOutbound("DETAIL-1"); + engine.compareOutbound(observation, "10"); + + assertThat(observation.warehouseId()).isEqualTo("W02"); + assertThat(observation.requestId()).isEqualTo("DETAIL-1"); + assertThat(observation.proposedSpaceId()).isEqualTo("SPACE-9"); + verify(mapper).insertComparison(any(), eq("W02"), eq("OUTBOUND_COMPARE"), eq("LEGACY_10"), + argThat(reason -> reason.contains("detail=DETAIL-1") + && reason.contains("project=PROJECT-1") + && reason.contains("product=PRODUCT-1") + && reason.contains("snapshotSync=2026-08-28T02:53:45.133Z") + && reason.contains("legacyMatches=2") + && reason.contains("shadowMatches=1"))); + } + + @Test + void outboundPlanningTreatsPaddedAndUnpaddedWarehouseIdsAsEquivalent() { + ShadowObservationProperties properties = new ShadowObservationProperties(); + properties.setWarehouseId("01"); + CapturingTransactionManager transactions = new CapturingTransactionManager(); + ShadowInputMapper mapper = mock(ShadowInputMapper.class); + SchedulingFacade facade = mock(SchedulingFacade.class); + ShadowStockReference reference = new ShadowStockReference(); + reference.setRequestId("DETAIL-2"); + reference.setWarehouseId("1"); + reference.setProjectNo("PROJECT-2"); + when(mapper.selectProjectOutboundReference("DETAIL-2")).thenReturn(reference); + when(facade.planOutbound(eq("01"), any())).thenReturn( + OutboundPlan.error(new com.ruoyi.algorithm.outbound.OutboundError( + com.ruoyi.algorithm.outbound.OutboundErrorCode.STOCK_NOT_FOUND, + "no stock", java.util.Map.of()))); + ShadowPlanningEngine engine = new ShadowPlanningEngine(facade, mapper, properties, transactions); + + OutboundShadowObservation observation = engine.planProjectOutbound("DETAIL-2"); + + assertThat(observation.warehouseId()).isEqualTo("01"); + } + private static final class CapturingTransactionManager implements PlatformTransactionManager { private TransactionDefinition definition; diff --git a/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java b/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java index fc864d8..c7935fc 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java @@ -123,6 +123,7 @@ class OutStockoutServiceImplShadowTest { private void prepareAutoAllot() { OutStockout bill = new OutStockout(); bill.setStatus("2"); + bill.setWarehouseId("1"); when(outStockoutMapper.selectOutStockoutByStockoutId("OUT-1")).thenReturn(bill); when(outStockoutDetailService.selectOutStockoutDetailList(any())) .thenReturn(List.of(pendingDetail)); diff --git a/scripts/shadow-export-mcp.js b/scripts/shadow-export-mcp.js index 677a0cc..f393292 100644 --- a/scripts/shadow-export-mcp.js +++ b/scripts/shadow-export-mcp.js @@ -114,7 +114,7 @@ async function main() { ["package_manifest", "SELECT package_id, package_version, installed_at FROM alg.shadow_package_manifest;"], ["decision_summary", `SELECT decision_type, result_code, COUNT(*) AS row_count, MIN(created_at) AS first_created_at, MAX(created_at) AS last_created_at FROM alg.decision_log WHERE created_at >= CAST('${detailStart}' AS datetime2) AND created_at < CAST('${detailEnd}' AS datetime2) GROUP BY decision_type, result_code ORDER BY decision_type, result_code`], ["decision_log", `SELECT decision_id, warehouse_id, decision_type, input_version, result_code, reason, created_at FROM alg.decision_log WHERE created_at >= CAST('${detailStart}' AS datetime2) AND created_at < CAST('${detailEnd}' AS datetime2) ORDER BY created_at, decision_id`], - ["outbound_jobs", `SELECT TOP (300) d.created_at AS decision_created_at, d.result_code AS shadow_compare_result, d.reason, j.job_id, j.task_no, j.tray_code AS job_tray_code, j.cargo_space_id AS job_source_space_id, j.target_cargo_space_id, j.warehouse_id, j.status AS job_status, j.in_or_out, j.create_time, j.finish_time FROM alg.decision_log d LEFT JOIN dbo.mydo_job j ON d.reason LIKE '%request=' + CONVERT(varchar(255), j.task_no) + '%' AND j.warehouse_id = '01' WHERE d.decision_type = 'OUTBOUND_COMPARE' AND d.created_at >= CAST('${detailStart}' AS datetime2) AND d.created_at < CAST('${detailEnd}' AS datetime2) ORDER BY d.created_at, j.create_time, j.task_no`], + ["outbound_jobs", `SELECT TOP (300) d.created_at AS decision_created_at, d.result_code AS shadow_compare_result, d.reason, detail.stockout_detail_id, detail.stockout_id, detail.warehouse_id, detail.spare4 AS project_no, detail.product_id, detail.quantity, detail.allot_quantity, detail.assign_num, detail.create_time AS detail_create_time FROM alg.decision_log d OUTER APPLY (SELECT LTRIM(RTRIM(SUBSTRING(d.reason, CHARINDEX('request=', d.reason) + 8, CHARINDEX(CHAR(59), d.reason + CHAR(59), CHARINDEX('request=', d.reason)) - (CHARINDEX('request=', d.reason) + 8)))) AS request_id) r LEFT JOIN dbo.out_stockout_detail detail ON detail.stockout_detail_id = r.request_id AND TRY_CONVERT(int, detail.warehouse_id) = TRY_CONVERT(int, '01') WHERE d.decision_type = 'OUTBOUND_COMPARE' AND d.created_at >= CAST('${detailStart}' AS datetime2) AND d.created_at < CAST('${detailEnd}' AS datetime2) ORDER BY d.created_at, detail.create_time, detail.stockout_detail_id`], ["inventory_mapping", "SELECT TOP (1000) c.warehouse_id, s.stock_id, s.cargo_space_id, s.tray_code, s.spare4 AS legacy_project_no, s.product_id AS legacy_product_no, s.lot_no, s.in_date, s.quantity, s.out_quantity, i.space_id AS shadow_space_id, i.tray_code AS shadow_tray_code, i.project_no AS shadow_project_no, i.product_no AS shadow_product_no, i.inbound_time_raw, i.inbound_at, i.quantity AS shadow_quantity, i.out_quantity AS shadow_out_quantity FROM dbo.bas_stock s INNER JOIN dbo.bas_cargo_space c ON c.cargo_space_id = s.cargo_space_id AND c.warehouse_id = '01' LEFT JOIN alg.v_inventory_item i ON i.stock_id = s.stock_id ORDER BY s.stock_id;"], ["jobs", "SELECT TOP (1000) job_id, task_no, tray_code, cargo_space_id, target_cargo_space_id, warehouse_id, status, in_or_out, create_time, finish_time FROM dbo.mydo_job WHERE warehouse_id = '01' ORDER BY create_time, job_id;"], ["lane_state", "SELECT warehouse_id, COUNT(*) AS lane_state_count, SUM(CASE WHEN pending_outbound = 1 THEN 1 ELSE 0 END) AS pending_lane_count, SUM(CASE WHEN has_gap = 1 THEN 1 ELSE 0 END) AS gap_lane_count, MAX(rebuilt_at) AS latest_rebuild_at FROM alg.lane_state WHERE warehouse_id = '01' GROUP BY warehouse_id;"], diff --git a/scripts/shadow-followup-mcp.js b/scripts/shadow-followup-mcp.js index 1f60523..df3224b 100644 --- a/scripts/shadow-followup-mcp.js +++ b/scripts/shadow-followup-mcp.js @@ -89,8 +89,8 @@ async function main() { ["shadow_missing_from_legacy", `SELECT TOP (1000) i.warehouse_id, i.stock_id, i.space_id, i.tray_code, i.project_no, i.product_no, i.lot_no, i.inbound_time_raw, i.quantity, i.out_quantity FROM alg.inventory_item_snapshot i LEFT JOIN dbo.bas_stock s ON s.stock_id = i.stock_id WHERE i.warehouse_id = '${warehouse}' AND s.stock_id IS NULL ORDER BY i.stock_id`], ["matched_field_difference_counts", `SELECT COUNT_BIG(*) AS matched_stock_count, SUM(CASE WHEN ISNULL(LTRIM(RTRIM(s.cargo_space_id)), '') <> ISNULL(LTRIM(RTRIM(i.space_id)), '') THEN 1 ELSE 0 END) AS space_mismatch_count, SUM(CASE WHEN ISNULL(LTRIM(RTRIM(s.tray_code)), '') <> ISNULL(LTRIM(RTRIM(i.tray_code)), '') THEN 1 ELSE 0 END) AS tray_mismatch_count, SUM(CASE WHEN ISNULL(LTRIM(RTRIM(s.spare4)), '') <> ISNULL(LTRIM(RTRIM(i.project_no)), '') THEN 1 ELSE 0 END) AS project_mismatch_count, SUM(CASE WHEN ISNULL(LTRIM(RTRIM(s.product_id)), '') <> ISNULL(LTRIM(RTRIM(i.product_no)), '') THEN 1 ELSE 0 END) AS product_mismatch_count, SUM(CASE WHEN ISNULL(LTRIM(RTRIM(s.lot_no)), '') <> ISNULL(LTRIM(RTRIM(i.lot_no)), '') THEN 1 ELSE 0 END) AS lot_mismatch_count, SUM(CASE WHEN ISNULL(s.quantity, 0) <> ISNULL(i.quantity, 0) THEN 1 ELSE 0 END) AS quantity_mismatch_count, SUM(CASE WHEN ISNULL(s.out_quantity, 0) <> ISNULL(i.out_quantity, 0) THEN 1 ELSE 0 END) AS out_quantity_mismatch_count FROM dbo.bas_stock s INNER JOIN dbo.bas_cargo_space c ON c.cargo_space_id = s.cargo_space_id AND c.warehouse_id = '${warehouse}' INNER JOIN alg.inventory_item_snapshot i ON i.warehouse_id = c.warehouse_id AND i.stock_id = s.stock_id`], ["field_difference_rows", `SELECT TOP (1000) s.stock_id, s.cargo_space_id AS legacy_space_id, i.space_id AS shadow_space_id, s.tray_code AS legacy_tray_code, i.tray_code AS shadow_tray_code, s.spare4 AS legacy_project_no, i.project_no AS shadow_project_no, s.product_id AS legacy_product_no, i.product_no AS shadow_product_no, s.lot_no AS legacy_lot_no, i.lot_no AS shadow_lot_no, s.quantity AS legacy_quantity, i.quantity AS shadow_quantity, s.out_quantity AS legacy_out_quantity, i.out_quantity AS shadow_out_quantity FROM dbo.bas_stock s INNER JOIN dbo.bas_cargo_space c ON c.cargo_space_id = s.cargo_space_id AND c.warehouse_id = '${warehouse}' INNER JOIN alg.inventory_item_snapshot i ON i.warehouse_id = c.warehouse_id AND i.stock_id = s.stock_id WHERE ISNULL(LTRIM(RTRIM(s.cargo_space_id)), '') <> ISNULL(LTRIM(RTRIM(i.space_id)), '') OR ISNULL(LTRIM(RTRIM(s.tray_code)), '') <> ISNULL(LTRIM(RTRIM(i.tray_code)), '') OR ISNULL(LTRIM(RTRIM(s.spare4)), '') <> ISNULL(LTRIM(RTRIM(i.project_no)), '') OR ISNULL(LTRIM(RTRIM(s.product_id)), '') <> ISNULL(LTRIM(RTRIM(i.product_no)), '') OR ISNULL(LTRIM(RTRIM(s.lot_no)), '') <> ISNULL(LTRIM(RTRIM(i.lot_no)), '') OR ISNULL(s.quantity, 0) <> ISNULL(i.quantity, 0) OR ISNULL(s.out_quantity, 0) <> ISNULL(i.out_quantity, 0) ORDER BY s.stock_id`], - ["request_match_counts", `SELECT COUNT_BIG(*) AS compare_count, SUM(CASE WHEN j_by_id.job_id IS NOT NULL THEN 1 ELSE 0 END) AS matched_by_job_id, SUM(CASE WHEN j_by_id.job_id IS NULL AND j_by_task.job_id IS NOT NULL THEN 1 ELSE 0 END) AS matched_by_task_no, SUM(CASE WHEN j_by_id.job_id IS NULL AND j_by_task.job_id IS NULL THEN 1 ELSE 0 END) AS unmatched_count FROM alg.decision_log d OUTER APPLY (SELECT LTRIM(RTRIM(SUBSTRING(d.reason, CHARINDEX('request=', d.reason) + 8, CHARINDEX(CHAR(59), d.reason + CHAR(59), CHARINDEX('request=', d.reason)) - (CHARINDEX('request=', d.reason) + 8)))) AS request_id) r LEFT JOIN dbo.mydo_job j_by_id ON j_by_id.warehouse_id = '${warehouse}' AND CONVERT(varchar(255), j_by_id.job_id) = r.request_id LEFT JOIN dbo.mydo_job j_by_task ON j_by_task.warehouse_id = '${warehouse}' AND CONVERT(varchar(255), j_by_task.task_no) = r.request_id WHERE d.decision_type = 'OUTBOUND_COMPARE' AND d.created_at >= '${start}' AND d.created_at < '${end}'`], - ["request_match_details", `SELECT TOP (300) d.created_at, d.result_code, r.request_id, j_by_id.job_id AS job_id_match, j_by_id.task_no AS job_id_match_task_no, j_by_id.tray_code AS job_id_match_tray_code, j_by_id.status AS job_id_match_status, j_by_id.in_or_out AS job_id_match_in_or_out, j_by_task.job_id AS task_no_match_job_id, j_by_task.task_no AS task_no_match_task_no, j_by_task.tray_code AS task_no_match_tray_code, j_by_task.status AS task_no_match_status, d.reason FROM alg.decision_log d OUTER APPLY (SELECT LTRIM(RTRIM(SUBSTRING(d.reason, CHARINDEX('request=', d.reason) + 8, CHARINDEX(CHAR(59), d.reason + CHAR(59), CHARINDEX('request=', d.reason)) - (CHARINDEX('request=', d.reason) + 8)))) AS request_id) r LEFT JOIN dbo.mydo_job j_by_id ON j_by_id.warehouse_id = '${warehouse}' AND CONVERT(varchar(255), j_by_id.job_id) = r.request_id LEFT JOIN dbo.mydo_job j_by_task ON j_by_task.warehouse_id = '${warehouse}' AND CONVERT(varchar(255), j_by_task.task_no) = r.request_id WHERE d.decision_type = 'OUTBOUND_COMPARE' AND d.created_at >= '${start}' AND d.created_at < '${end}' ORDER BY d.created_at`] + ["request_match_counts", `SELECT COUNT_BIG(*) AS compare_count, SUM(CASE WHEN detail.stockout_detail_id IS NOT NULL THEN 1 ELSE 0 END) AS matched_by_detail_id, SUM(CASE WHEN detail.stockout_detail_id IS NULL THEN 1 ELSE 0 END) AS unmatched_count FROM alg.decision_log d OUTER APPLY (SELECT LTRIM(RTRIM(SUBSTRING(d.reason, CHARINDEX('request=', d.reason) + 8, CHARINDEX(CHAR(59), d.reason + CHAR(59), CHARINDEX('request=', d.reason)) - (CHARINDEX('request=', d.reason) + 8)))) AS request_id) r LEFT JOIN dbo.out_stockout_detail detail ON detail.stockout_detail_id = r.request_id AND TRY_CONVERT(int, detail.warehouse_id) = TRY_CONVERT(int, '${warehouse}') WHERE d.decision_type = 'OUTBOUND_COMPARE' AND d.created_at >= '${start}' AND d.created_at < '${end}'`], + ["request_match_details", `SELECT TOP (300) d.created_at, d.result_code, r.request_id, detail.stockout_detail_id AS detail_id_match, detail.stockout_id, detail.warehouse_id, detail.spare4 AS detail_project_no, detail.product_id AS detail_product_id, detail.quantity AS detail_quantity, detail.allot_quantity AS detail_allot_quantity, detail.assign_num AS detail_assign_num, d.reason FROM alg.decision_log d OUTER APPLY (SELECT LTRIM(RTRIM(SUBSTRING(d.reason, CHARINDEX('request=', d.reason) + 8, CHARINDEX(CHAR(59), d.reason + CHAR(59), CHARINDEX('request=', d.reason)) - (CHARINDEX('request=', d.reason) + 8)))) AS request_id) r LEFT JOIN dbo.out_stockout_detail detail ON detail.stockout_detail_id = r.request_id AND TRY_CONVERT(int, detail.warehouse_id) = TRY_CONVERT(int, '${warehouse}') WHERE d.decision_type = 'OUTBOUND_COMPARE' AND d.created_at >= '${start}' AND d.created_at < '${end}' ORDER BY d.created_at`] ]; const results = [];