docs: add shadow v3 field operations package

This commit is contained in:
zgw
2026-09-03 17:08:13 +08:00
parent 169b64850d
commit 47190451ff
8 changed files with 748 additions and 249 deletions

View File

@@ -0,0 +1,86 @@
@echo off
setlocal EnableExtensions DisableDelayedExpansion
if "%~1"=="" goto :usage
if "%~2"=="" goto :usage
if "%~3"=="" goto :usage
if "%~4"=="" goto :usage
if "%~5"=="" goto :usage
if "%~6"=="" goto :usage
set "REPO_ROOT=%~f1"
set "SQL_SERVER=%~2"
set "DATABASE=%~3"
set "START_UTC=%~4"
set "OUTPUT_DIR=%~f5"
set "LOG_DIR=%~f6"
set "SQL_FILE=%~dp0shadow-v3-export.sql"
set "WAREHOUSE_ID=%FORTUCKY_ALGORITHM_WAREHOUSE_ID%"
if not defined WAREHOUSE_ID set "WAREHOUSE_ID=01"
if "%WAREHOUSE_ID%"=="1" set "WAREHOUSE_ID=01"
rem FORTUCKY_SHADOW_ variables are collected by the FORTUCKY_ prefix scan below.
if not "%WAREHOUSE_ID%"=="01" (call :fail "FORTUCKY_ALGORITHM_WAREHOUSE_ID must be 01" & exit /b 1)
if not exist "%REPO_ROOT%\.git" (call :fail "REPO_ROOT is not a Git repository" & exit /b 1)
if not exist "%LOG_DIR%" (call :fail "LOG_DIR does not exist" & exit /b 1)
if not exist "%SQL_FILE%" (call :fail "export SQL file is missing" & exit /b 1)
where git >nul 2>nul
if errorlevel 1 (call :fail "git is not available on PATH" & exit /b 1)
where jps >nul 2>nul
if errorlevel 1 (call :fail "jps is not available on PATH" & exit /b 1)
where sqlcmd >nul 2>nul
if errorlevel 1 (call :fail "sqlcmd is not available on PATH" & exit /b 1)
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
if errorlevel 1 (call :fail "cannot create OUTPUT_DIR" & exit /b 1)
pushd "%REPO_ROOT%" >nul
if errorlevel 1 (call :fail "cannot enter REPO_ROOT" & exit /b 1)
(
echo export_started_utc=%DATE% %TIME%
echo expected_branch=test/ft546wms-shadow-v3-20260903
git branch --show-current
git rev-parse HEAD
git status --porcelain
) > "%OUTPUT_DIR%\deployment.txt" 2>&1
if errorlevel 1 (call :fail "cannot collect Git deployment evidence" & exit /b 1)
popd >nul
jps -lv > "%OUTPUT_DIR%\runtime-processes.txt" 2>&1
if errorlevel 1 (call :fail "cannot collect JVM process evidence" & exit /b 1)
(
for /f "tokens=1 delims==" %%E in ('set FORTUCKY_ 2^>nul') do echo %%E
) > "%OUTPUT_DIR%\environment-variable-names.txt"
sqlcmd -S "%SQL_SERVER%" -d "%DATABASE%" -E -b -r1 -w 65535 -y 0 -Y 0 -v WarehouseId="%WAREHOUSE_ID%" StartUtc="%START_UTC%" OutputDir="%OUTPUT_DIR%" -i "%SQL_FILE%"
if errorlevel 1 (call :fail "read-only SQL export failed" & exit /b 1)
findstr /S /I /N /C:"shadow_failure" "%LOG_DIR%\*.log" "%LOG_DIR%\*.txt" > "%OUTPUT_DIR%\shadow-warnings.txt" 2>nul
call :require_output "sql-diagnostics.txt"
if errorlevel 1 exit /b 1
call :require_output "comparison-detail.txt"
if errorlevel 1 exit /b 1
call :require_output "runtime-metrics.txt"
if errorlevel 1 exit /b 1
call :require_output "inventory-parity.txt"
if errorlevel 1 exit /b 1
call :require_output "shadow-warnings.txt"
if errorlevel 1 exit /b 1
echo EXPORT_OK output=%OUTPUT_DIR%
exit /b 0
:require_output
if not exist "%OUTPUT_DIR%\%~1" (call :fail "expected export file is missing" & exit /b 1)
exit /b 0
:usage
echo Usage: %~nx0 REPO_ROOT SQL_SERVER DATABASE START_UTC OUTPUT_DIR LOG_DIR 1>&2
exit /b 2
:fail
echo EXPORT_FAILED %~1 1>&2
popd >nul 2>nul
exit /b 1

View File

@@ -0,0 +1,190 @@
:on error exit
:out "$(OutputDir)\sql-diagnostics.txt"
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT CAST(SERVERPROPERTY('ProductVersion') AS varchar(30)) AS product_version,
DB_NAME() AS database_name,
SYSUTCDATETIME() AS checked_at_utc,
CONVERT(datetime2(3), '$(StartUtc)', 127) AS export_start_utc,
'$(WarehouseId)' AS warehouse_id;
SELECT package_id, package_version, installed_at
FROM alg.shadow_package_manifest;
SELECT warehouse_id, snapshot_generation, snapshot_status, synchronized_at,
DATEDIFF(SECOND, synchronized_at, SYSUTCDATETIME()) AS snapshot_age_seconds,
CONVERT(varchar(64), source_signature, 2) AS source_signature,
source_row_count, instance_id, last_error
FROM alg.shadow_snapshot_state
WHERE warehouse_id = '$(WarehouseId)';
SELECT warehouse_id, instance_id, claimed_at, last_seen_at,
DATEDIFF(SECOND, last_seen_at, SYSUTCDATETIME()) AS heartbeat_age_seconds
FROM alg.shadow_instance_heartbeat
WHERE warehouse_id = '$(WarehouseId)';
SELECT 'cargo_space_snapshot' AS entity_name, state.snapshot_generation,
COUNT_BIG(snapshot.space_id) AS row_count
FROM alg.shadow_snapshot_state state
LEFT JOIN alg.cargo_space_snapshot snapshot
ON snapshot.warehouse_id = state.warehouse_id
AND snapshot.snapshot_generation = state.snapshot_generation
WHERE state.warehouse_id = '$(WarehouseId)'
GROUP BY state.snapshot_generation
UNION ALL
SELECT 'active_job_snapshot', state.snapshot_generation, COUNT_BIG(snapshot.job_id)
FROM alg.shadow_snapshot_state state
LEFT JOIN alg.active_job_snapshot snapshot
ON snapshot.warehouse_id = state.warehouse_id
AND snapshot.snapshot_generation = state.snapshot_generation
WHERE state.warehouse_id = '$(WarehouseId)'
GROUP BY state.snapshot_generation
UNION ALL
SELECT 'shadow_inbound_request_snapshot', state.snapshot_generation, COUNT_BIG(snapshot.task_no)
FROM alg.shadow_snapshot_state state
LEFT JOIN alg.shadow_inbound_request_snapshot snapshot
ON snapshot.warehouse_id = state.warehouse_id
AND snapshot.snapshot_generation = state.snapshot_generation
WHERE state.warehouse_id = '$(WarehouseId)'
GROUP BY state.snapshot_generation
UNION ALL
SELECT 'shadow_outbound_request_snapshot', state.snapshot_generation, COUNT_BIG(snapshot.stockout_detail_id)
FROM alg.shadow_snapshot_state state
LEFT JOIN alg.shadow_outbound_request_snapshot snapshot
ON snapshot.warehouse_id = state.warehouse_id
AND snapshot.snapshot_generation = state.snapshot_generation
WHERE state.warehouse_id = '$(WarehouseId)'
GROUP BY state.snapshot_generation;
SELECT request.session_id, request.blocking_session_id, request.status,
request.wait_type, request.wait_time, request.command,
SUBSTRING(sql_text.text,
(request.statement_start_offset / 2) + 1,
((CASE request.statement_end_offset
WHEN -1 THEN DATALENGTH(sql_text.text)
ELSE request.statement_end_offset
END - request.statement_start_offset) / 2) + 1) AS statement_text
FROM sys.dm_exec_requests request
OUTER APPLY sys.dm_exec_sql_text(request.sql_handle) sql_text
WHERE request.database_id = DB_ID()
AND (request.blocking_session_id <> 0 OR sql_text.text LIKE '%alg.%')
ORDER BY request.session_id;
GO
:out "$(OutputDir)\comparison-detail.txt"
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT decision_type, result_code,
CASE
WHEN result_code = 'MATCH' THEN 'VALID_MATCH'
WHEN result_code = 'DIFFERENT' THEN 'VALID_DIFFERENT'
WHEN result_code LIKE 'SKIP_%' THEN 'SKIP'
WHEN result_code LIKE 'ERROR_%' THEN 'ERROR'
ELSE 'OTHER'
END AS outcome_group,
COUNT_BIG(*) AS row_count,
MIN(created_at) AS first_created_at,
MAX(created_at) AS last_created_at
FROM alg.decision_log
WHERE warehouse_id = '$(WarehouseId)'
AND created_at >= CONVERT(datetime2(3), '$(StartUtc)', 127)
GROUP BY decision_type, result_code,
CASE
WHEN result_code = 'MATCH' THEN 'VALID_MATCH'
WHEN result_code = 'DIFFERENT' THEN 'VALID_DIFFERENT'
WHEN result_code LIKE 'SKIP_%' THEN 'SKIP'
WHEN result_code LIKE 'ERROR_%' THEN 'ERROR'
ELSE 'OTHER'
END
ORDER BY decision_type, result_code;
SELECT decision_id, warehouse_id, decision_type, input_version,
result_code, reason, created_at
FROM alg.decision_log
WHERE warehouse_id = '$(WarehouseId)'
AND created_at >= CONVERT(datetime2(3), '$(StartUtc)', 127)
ORDER BY created_at, decision_id;
SELECT SUM(CASE WHEN decision_type = 'INBOUND' THEN 1 ELSE 0 END) AS inbound_plan_count,
SUM(CASE WHEN decision_type = 'INBOUND_COMPARE' THEN 1 ELSE 0 END) AS inbound_compare_count,
SUM(CASE WHEN decision_type = 'OUTBOUND' THEN 1 ELSE 0 END) AS outbound_plan_count,
SUM(CASE WHEN decision_type = 'OUTBOUND_COMPARE' THEN 1 ELSE 0 END) AS outbound_compare_count
FROM alg.decision_log
WHERE warehouse_id = '$(WarehouseId)'
AND created_at >= CONVERT(datetime2(3), '$(StartUtc)', 127);
GO
:out "$(OutputDir)\runtime-metrics.txt"
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT warehouse_id, metric_name, bucket_upper_micros, metric_count,
metric_total_micros, metric_max_micros, flushed_at
FROM alg.shadow_runtime_metric
WHERE warehouse_id = '$(WarehouseId)'
ORDER BY metric_name, bucket_upper_micros;
SELECT CASE
WHEN metric_name LIKE 'SKIP_%' THEN 'SKIP'
WHEN metric_name LIKE 'ERROR_%' THEN 'ERROR'
WHEN metric_name = 'ADMITTED' THEN 'ADMITTED'
ELSE 'HISTOGRAM'
END AS metric_group,
SUM(metric_count) AS metric_count,
MAX(flushed_at) AS latest_flush_at
FROM alg.shadow_runtime_metric
WHERE warehouse_id = '$(WarehouseId)'
GROUP BY CASE
WHEN metric_name LIKE 'SKIP_%' THEN 'SKIP'
WHEN metric_name LIKE 'ERROR_%' THEN 'ERROR'
WHEN metric_name = 'ADMITTED' THEN 'ADMITTED'
ELSE 'HISTOGRAM'
END
ORDER BY metric_group;
GO
:out "$(OutputDir)\inventory-parity.txt"
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
WITH legacy_stock AS (
SELECT stock.stock_id, stock.cargo_space_id, stock.tray_code,
stock.spare4 AS project_no, stock.product_id AS product_no,
stock.lot_no, stock.quantity, stock.out_quantity
FROM dbo.bas_stock stock WITH (NOLOCK)
INNER JOIN dbo.bas_cargo_space space WITH (NOLOCK)
ON space.cargo_space_id = stock.cargo_space_id
WHERE TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(space.warehouse_id))) =
TRY_CONVERT(decimal(38,0), '$(WarehouseId)')
), shadow_stock AS (
SELECT snapshot.stock_id, snapshot.space_id AS cargo_space_id,
snapshot.tray_code, snapshot.project_no, snapshot.product_no,
snapshot.lot_no, snapshot.quantity, snapshot.out_quantity
FROM alg.inventory_item_snapshot snapshot
INNER JOIN alg.shadow_snapshot_state state
ON state.warehouse_id = snapshot.warehouse_id
AND state.snapshot_generation = snapshot.snapshot_generation
AND state.snapshot_status = 'PUBLISHED'
WHERE snapshot.warehouse_id = '$(WarehouseId)'
)
SELECT SUM(CASE WHEN legacy.stock_id IS NULL THEN 1 ELSE 0 END) AS shadow_only_count,
SUM(CASE WHEN shadow.stock_id IS NULL THEN 1 ELSE 0 END) AS legacy_only_count,
SUM(CASE WHEN legacy.stock_id IS NOT NULL AND shadow.stock_id IS NOT NULL
AND (ISNULL(legacy.cargo_space_id, '') <> ISNULL(shadow.cargo_space_id, '')
OR ISNULL(legacy.tray_code, '') <> ISNULL(shadow.tray_code, '')
OR ISNULL(legacy.project_no, '') <> ISNULL(shadow.project_no, '')
OR ISNULL(legacy.product_no, '') <> ISNULL(shadow.product_no, '')
OR ISNULL(legacy.lot_no, '') <> ISNULL(shadow.lot_no, '')
OR ISNULL(legacy.quantity, 0) <> ISNULL(shadow.quantity, 0)
OR ISNULL(legacy.out_quantity, 0) <> ISNULL(shadow.out_quantity, 0))
THEN 1 ELSE 0 END) AS differing_row_count,
COUNT_BIG(*) AS compared_stock_count
FROM legacy_stock legacy
FULL OUTER JOIN shadow_stock shadow ON shadow.stock_id = legacy.stock_id;
GO
:out stdout
SELECT 'EXPORT_SQL_OK' AS export_status, SYSUTCDATETIME() AS completed_at_utc;

View File

@@ -0,0 +1,34 @@
@echo off
setlocal EnableExtensions DisableDelayedExpansion
if "%~1"=="" goto :usage
if "%~2"=="" goto :usage
if "%~3"=="" goto :usage
set "SQL_SERVER=%~1"
set "DATABASE=%~2"
set "MODE=%~3"
set "SQL_FILE="
if /I "%~3"=="upgrade" set "SQL_FILE=%~dp0..\fortucky-algorithm\src\main\resources\db\sqlserver\upgrade-shadow-v2-to-v3.sql"
if /I "%~3"=="clean" set "SQL_FILE=%~dp0..\fortucky-algorithm\src\main\resources\db\sqlserver\install-shadow.sql"
if not defined SQL_FILE (call :fail "mode must be upgrade or clean" & exit /b 1)
if not exist "%SQL_FILE%" (call :fail "SQL package file is missing" & exit /b 1)
where sqlcmd >nul 2>nul
if errorlevel 1 (call :fail "sqlcmd is not available on PATH" & exit /b 1)
echo Installing FT546WMS shadow v3 in %MODE% mode...
sqlcmd -S "%SQL_SERVER%" -d "%DATABASE%" -E -b -r1 -i "%SQL_FILE%"
if errorlevel 1 exit /b 1
echo INSTALL_OK mode=%MODE%
exit /b 0
:usage
echo Usage: %~nx0 SQL_SERVER DATABASE upgrade^|clean 1>&2
exit /b 2
:fail
echo INSTALL_FAILED %~1 1>&2
exit /b 1

View File

@@ -0,0 +1,64 @@
@echo off
setlocal EnableExtensions DisableDelayedExpansion
if "%~1"=="" goto :usage
if "%~2"=="" goto :usage
if "%~3"=="" goto :usage
if "%~4"=="" goto :usage
set "REPO_ROOT=%~f1"
set "EXPECTED_COMMIT=%~2"
set "SQL_SERVER=%~3"
set "DATABASE=%~4"
set "EXPECTED_BRANCH=test/ft546wms-shadow-v3-20260903"
set "MAIN_COMMIT=916ac6bda32a5b102a3181cc8c5dbd5319c5656d"
where git >nul 2>nul
if errorlevel 1 (call :fail "git is not available on PATH" & exit /b 1)
where jps >nul 2>nul
if errorlevel 1 (call :fail "jps is not available on PATH" & exit /b 1)
where sqlcmd >nul 2>nul
if errorlevel 1 (call :fail "sqlcmd is not available on PATH" & exit /b 1)
if not exist "%REPO_ROOT%\.git" (call :fail "REPO_ROOT is not a Git repository" & exit /b 1)
pushd "%REPO_ROOT%" >nul
if errorlevel 1 (call :fail "cannot enter REPO_ROOT" & exit /b 1)
git rev-parse --is-inside-work-tree >nul 2>nul
if errorlevel 1 (call :fail "REPO_ROOT is not a Git worktree" & exit /b 1)
set "CURRENT_BRANCH="
for /f "usebackq delims=" %%I in (`git branch --show-current`) do set "CURRENT_BRANCH=%%I"
if /I not "%CURRENT_BRANCH%"=="%EXPECTED_BRANCH%" (call :fail "unexpected branch" & exit /b 1)
set "CURRENT_COMMIT="
for /f "usebackq delims=" %%I in (`git rev-parse HEAD`) do set "CURRENT_COMMIT=%%I"
if /I not "%CURRENT_COMMIT%"=="%EXPECTED_COMMIT%" (call :fail "HEAD does not match EXPECTED_COMMIT" & exit /b 1)
set "DIRTY="
for /f "usebackq delims=" %%I in (`git status --porcelain`) do set "DIRTY=1"
if defined DIRTY (call :fail "Git worktree is not clean" & exit /b 1)
git merge-base --is-ancestor %MAIN_COMMIT% HEAD
if errorlevel 1 (call :fail "required main commit is not an ancestor" & exit /b 1)
popd >nul
set "WMS_PROCESS_COUNT=0"
for /f %%I in ('jps -lv 2^>nul ^| findstr /I /C:"com.ruoyi.RuoYiApplication" /C:"FT546WMS" ^| find /C /V ""') do set "WMS_PROCESS_COUNT=%%I"
if %WMS_PROCESS_COUNT% GTR 1 (call :fail "more than one FT546WMS JVM is running" & exit /b 1)
set "PREFLIGHT_SQL=SET NOCOUNT ON; IF SCHEMA_ID(N'alg') IS NULL SELECT N'CLEAN_INSTALL_ELIGIBLE' AS package_state; ELSE BEGIN IF OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL BEGIN; THROW 51050, 'shadow manifest is missing', 1; END; IF NOT EXISTS (SELECT 1 FROM alg.shadow_package_manifest WHERE package_id='FT546WMS_SHADOW_ONLY' AND package_version IN ('2026-08-12-1','2026-09-03-3')) BEGIN; THROW 51051, 'shadow manifest is not a supported version', 1; END; SELECT package_id, package_version, installed_at FROM alg.shadow_package_manifest; IF OBJECT_ID(N'alg.shadow_snapshot_state', N'U') IS NOT NULL SELECT warehouse_id, snapshot_generation, snapshot_status, synchronized_at, CONVERT(varchar(64), source_signature, 2) AS source_signature FROM alg.shadow_snapshot_state; END"
sqlcmd -S "%SQL_SERVER%" -d "%DATABASE%" -E -b -r1 -Q "%PREFLIGHT_SQL%"
if errorlevel 1 (call :fail "SQL connectivity or shadow package validation failed" & exit /b 1)
echo PREFLIGHT_OK branch=%CURRENT_BRANCH% commit=%CURRENT_COMMIT% wmsProcesses=%WMS_PROCESS_COUNT%
exit /b 0
:usage
echo Usage: %~nx0 REPO_ROOT EXPECTED_COMMIT SQL_SERVER DATABASE 1>&2
exit /b 2
:fail
echo PREFLIGHT_FAILED %~1 1>&2
popd >nul 2>nul
exit /b 1

View File

@@ -0,0 +1,35 @@
@echo off
setlocal EnableExtensions DisableDelayedExpansion
if "%~1"=="" goto :usage
if "%~2"=="" goto :usage
if "%~3"=="" goto :usage
if not "%~3"=="CONFIRM_UNINSTALL" goto :refuse
set "SQL_SERVER=%~1"
set "DATABASE=%~2"
set "SQL_FILE=%~dp0..\fortucky-algorithm\src\main\resources\db\sqlserver\uninstall-shadow.sql"
if not exist "%SQL_FILE%" (call :fail "uninstall SQL file is missing" & exit /b 1)
where sqlcmd >nul 2>nul
if errorlevel 1 (call :fail "sqlcmd is not available on PATH" & exit /b 1)
echo WARNING: export all alg.* evidence before uninstalling.
echo The shadow application must be stopped and the stable service restored first.
sqlcmd -S "%SQL_SERVER%" -d "%DATABASE%" -E -b -r1 -i "%SQL_FILE%"
if errorlevel 1 exit /b 1
echo UNINSTALL_OK
exit /b 0
:usage
echo Usage: %~nx0 SQL_SERVER DATABASE CONFIRM_UNINSTALL 1>&2
exit /b 2
:refuse
echo UNINSTALL_REFUSED third argument must be CONFIRM_UNINSTALL 1>&2
exit /b 3
:fail
echo UNINSTALL_FAILED %~1 1>&2
exit /b 1