fix(shadow): align outbound predicates with site data for v3.3

Use the business project and captured site predicates, validate single-stock proposals, and skip unsupported scopes without changing Legacy. Add the guarded V3.3 SQL upgrade, CMD operations, regression tests and deployment runbook.
This commit is contained in:
zgw
2026-09-07 12:43:43 +08:00
parent 79e0db29f9
commit 699310e61d
18 changed files with 1394 additions and 25 deletions

View File

@@ -2,7 +2,7 @@ SET NOCOUNT ON;
SET XACT_ABORT ON;
IF SCHEMA_ID(N'alg') IS NULL THROW 51010, 'FT546 shadow uninstall refused: alg schema does not exist', 1;
IF OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL THROW 51011, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
IF (SELECT COUNT(*) FROM alg.shadow_package_manifest WHERE package_id='FT546WMS_SHADOW_ONLY' AND package_version='2026-09-03-3')<>1 OR (SELECT COUNT(*) FROM alg.shadow_package_manifest)<>1 THROW 51012, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
IF (SELECT COUNT(*) FROM alg.shadow_package_manifest WHERE package_id='FT546WMS_SHADOW_ONLY' AND package_version IN ('2026-09-03-3','2026-09-07-33'))<>1 OR (SELECT COUNT(*) FROM alg.shadow_package_manifest)<>1 THROW 51012, 'FT546 shadow uninstall refused: shadow package manifest does not match', 1;
DECLARE @expected_objects TABLE (object_name sysname NOT NULL PRIMARY KEY, object_type char(2) NOT NULL);
INSERT INTO @expected_objects VALUES
(N'shadow_package_manifest','U'),(N'warehouse_policy','U'),(N'project_policy','U'),(N'space_role','U'),(N'space_topology','U'),(N'inventory_item_snapshot','U'),(N'space_inventory_summary','U'),(N'lane_state','U'),(N'reservation','U'),(N'deep_workflow_lock','U'),(N'decision_log','U'),(N'shadow_snapshot_state','U'),(N'cargo_space_snapshot','U'),(N'active_job_snapshot','U'),(N'shadow_inbound_request_snapshot','U'),(N'shadow_outbound_request_snapshot','U'),(N'shadow_runtime_metric','U'),(N'shadow_instance_heartbeat','U'),
@@ -11,6 +11,7 @@ IF EXISTS (SELECT 1 FROM sys.objects object LEFT JOIN @expected_objects expected
IF EXISTS (SELECT 1 FROM @expected_objects expected LEFT JOIN sys.objects object ON object.schema_id=SCHEMA_ID(N'alg') AND object.name=expected.object_name AND object.type COLLATE DATABASE_DEFAULT=expected.object_type WHERE object.object_id IS NULL) THROW 51014, 'FT546 shadow uninstall refused: expected object is missing or has wrong type', 1;
BEGIN TRY
BEGIN TRANSACTION;
IF EXISTS (SELECT 1 FROM alg.shadow_instance_heartbeat WITH (UPDLOCK, HOLDLOCK) WHERE last_seen_at >= DATEADD(SECOND,-120,SYSUTCDATETIME()) OR claimed_at >= DATEADD(SECOND,-120,SYSUTCDATETIME())) THROW 51015, 'FT546 shadow uninstall refused: recent heartbeat; stop WMS and wait over 120 seconds', 1;
DROP PROCEDURE alg.usp_refresh_shadow_snapshot; DROP PROCEDURE alg.usp_claim_shadow_instance; DROP PROCEDURE alg.usp_refresh_dirty_lanes; DROP PROCEDURE alg.usp_rebuild_lane_state; DROP PROCEDURE alg.usp_compute_lane_semantic_fingerprint; DROP PROCEDURE alg.usp_sync_inventory_snapshot; DROP PROCEDURE alg.usp_rebuild_space_topology;
DROP VIEW alg.v_space_state; DROP VIEW alg.v_active_job; DROP VIEW alg.v_inventory_item;
DROP TABLE alg.shadow_instance_heartbeat; DROP TABLE alg.shadow_runtime_metric; DROP TABLE alg.shadow_outbound_request_snapshot; DROP TABLE alg.shadow_inbound_request_snapshot; DROP TABLE alg.active_job_snapshot; DROP TABLE alg.cargo_space_snapshot; DROP TABLE alg.shadow_snapshot_state; DROP TABLE alg.decision_log; DROP TABLE alg.deep_workflow_lock; DROP TABLE alg.reservation; DROP TABLE alg.lane_state; DROP TABLE alg.space_inventory_summary; DROP TABLE alg.inventory_item_snapshot; DROP TABLE alg.space_topology; DROP TABLE alg.space_role; DROP TABLE alg.project_policy; DROP TABLE alg.warehouse_policy; DROP TABLE alg.shadow_package_manifest;

View File

@@ -0,0 +1,557 @@
/*
FT546WMS shadow V3/V3.1/V3.2 -> V3.3, SQL Server 2016 RTM compatible.
Run only after stopping the WMS shadow-enabled JVM and waiting >120 seconds.
Changes only alg.*. Never executes or alters a Legacy procedure.
One transaction: any failure rolls back columns, procedure, state and manifest.
*/
SET NOCOUNT ON;
SET XACT_ABORT ON;
SET LOCK_TIMEOUT 5000;
SET DEADLOCK_PRIORITY LOW;
IF DB_NAME() <> N'FT546-wms'
THROW 51060, 'V3.3 upgrade refused: expected FT546-wms database', 1;
IF SCHEMA_ID(N'alg') IS NULL OR OBJECT_ID(N'alg.shadow_package_manifest', N'U') IS NULL
THROW 51061, 'V3.3 upgrade refused: shadow package manifest is missing', 1;
DECLARE @expected TABLE (object_name sysname PRIMARY KEY, object_type char(2) NOT NULL);
INSERT INTO @expected VALUES
(N'shadow_package_manifest','U'),(N'warehouse_policy','U'),(N'project_policy','U'),
(N'space_role','U'),(N'space_topology','U'),(N'inventory_item_snapshot','U'),
(N'space_inventory_summary','U'),(N'lane_state','U'),(N'reservation','U'),
(N'deep_workflow_lock','U'),(N'decision_log','U'),(N'shadow_snapshot_state','U'),
(N'cargo_space_snapshot','U'),(N'active_job_snapshot','U'),
(N'shadow_inbound_request_snapshot','U'),(N'shadow_outbound_request_snapshot','U'),
(N'shadow_runtime_metric','U'),(N'shadow_instance_heartbeat','U'),
(N'v_inventory_item','V'),(N'v_active_job','V'),(N'v_space_state','V'),
(N'usp_rebuild_space_topology','P'),(N'usp_sync_inventory_snapshot','P'),
(N'usp_compute_lane_semantic_fingerprint','P'),(N'usp_rebuild_lane_state','P'),
(N'usp_refresh_dirty_lanes','P'),(N'usp_claim_shadow_instance','P'),
(N'usp_refresh_shadow_snapshot','P');
DECLARE @columns TABLE (
table_name sysname, column_name sysname, type_name sysname, max_length smallint,
precision_value tinyint, scale_value tinyint, PRIMARY KEY (table_name, column_name));
INSERT INTO @columns VALUES
(N'inventory_item_snapshot',N'spare1',N'varchar',255,0,0),
(N'inventory_item_snapshot',N'spare2',N'varchar',255,0,0),
(N'inventory_item_snapshot',N'spare3',N'varchar',255,0,0),
(N'inventory_item_snapshot',N'voltage',N'varchar',255,0,0),
(N'inventory_item_snapshot',N'phase',N'varchar',255,0,0),
(N'inventory_item_snapshot',N'tray_job_count',N'int',4,10,0),
(N'shadow_outbound_request_snapshot',N'spare1',N'varchar',255,0,0),
(N'shadow_outbound_request_snapshot',N'spare2',N'varchar',255,0,0),
(N'shadow_outbound_request_snapshot',N'spare3',N'varchar',255,0,0),
(N'shadow_outbound_request_snapshot',N'voltage',N'varchar',255,0,0),
(N'shadow_outbound_request_snapshot',N'phase',N'varchar',255,0,0),
(N'shadow_outbound_request_snapshot',N'allocated_quantity',N'decimal',17,38,6),
(N'shadow_outbound_request_snapshot',N'predicate_version',N'int',4,10,0);
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @version varchar(40);
SELECT @version = package_version
FROM alg.shadow_package_manifest WITH (UPDLOCK, HOLDLOCK)
WHERE package_id = 'FT546WMS_SHADOW_ONLY';
IF @version IS NULL OR @version NOT IN ('2026-09-03-3', '2026-09-07-33')
OR (SELECT COUNT(*) FROM alg.shadow_package_manifest) <> 1
THROW 51062, 'V3.3 upgrade refused: exact shadow manifest does not match', 1;
IF EXISTS (
SELECT 1 FROM sys.objects object
LEFT JOIN @expected expected ON expected.object_name = object.name
AND expected.object_type = object.type COLLATE DATABASE_DEFAULT
WHERE object.schema_id = SCHEMA_ID(N'alg') AND object.is_ms_shipped = 0
AND object.parent_object_id = 0 AND expected.object_name IS NULL)
THROW 51063, 'V3.3 upgrade refused: unknown object exists in alg schema', 1;
IF EXISTS (
SELECT 1 FROM @expected expected
LEFT JOIN sys.objects object ON object.schema_id = SCHEMA_ID(N'alg')
AND object.name = expected.object_name
AND object.type COLLATE DATABASE_DEFAULT = expected.object_type
WHERE object.object_id IS NULL)
THROW 51064, 'V3.3 upgrade refused: expected object is missing or has wrong type', 1;
IF EXISTS (SELECT 1 FROM sys.triggers trigger_object
JOIN sys.objects parent_object ON parent_object.object_id = trigger_object.parent_id
WHERE parent_object.schema_id = SCHEMA_ID(N'alg'))
THROW 51065, 'V3.3 upgrade refused: unexpected trigger on shadow table', 1;
IF EXISTS (SELECT 1 FROM alg.shadow_instance_heartbeat WITH (UPDLOCK, HOLDLOCK)
WHERE last_seen_at >= DATEADD(SECOND, -120, SYSUTCDATETIME())
OR claimed_at >= DATEADD(SECOND, -120, SYSUTCDATETIME()))
THROW 51066, 'V3.3 upgrade refused: recent shadow heartbeat; stop WMS and wait over 120 seconds', 1;
IF @version = '2026-09-07-33'
BEGIN
IF EXISTS (
SELECT 1 FROM @columns expected
LEFT JOIN sys.columns column_info
ON column_info.object_id = OBJECT_ID(N'alg.' + expected.table_name)
AND column_info.name = expected.column_name
WHERE column_info.column_id IS NULL
OR TYPE_NAME(column_info.user_type_id) <> expected.type_name
OR column_info.max_length <> expected.max_length
OR column_info.precision <> expected.precision_value
OR column_info.scale <> expected.scale_value
OR column_info.is_nullable <> 1)
THROW 51067, 'V3.3 upgrade refused: target columns are missing or incompatible', 1;
COMMIT TRANSACTION;
PRINT 'V33_ALREADY_INSTALLED';
RETURN;
END;
IF EXISTS (
SELECT 1 FROM @columns expected JOIN sys.columns column_info
ON column_info.object_id = OBJECT_ID(N'alg.' + expected.table_name)
AND column_info.name = expected.column_name)
THROW 51068, 'V3.3 upgrade refused: partial or foreign predicate columns already exist', 1;
ALTER TABLE alg.inventory_item_snapshot ADD
spare1 varchar(255) NULL, spare2 varchar(255) NULL, spare3 varchar(255) NULL,
voltage varchar(255) NULL, phase varchar(255) NULL, tray_job_count int NULL;
ALTER TABLE alg.shadow_outbound_request_snapshot ADD
spare1 varchar(255) NULL, spare2 varchar(255) NULL, spare3 varchar(255) NULL,
voltage varchar(255) NULL, phase varchar(255) NULL,
allocated_quantity decimal(38,6) NULL, predicate_version int NULL;
DECLARE @refresh_sql nvarchar(max) = N'ALTER PROCEDURE alg.usp_refresh_shadow_snapshot
@warehouse_id varchar(80),
@snapshot_generation bigint,
@instance_id varchar(80)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF NULLIF(LTRIM(RTRIM(@warehouse_id)), '''') IS NULL
THROW 51006, ''warehouse_id is required'', 1;
IF NULLIF(LTRIM(RTRIM(@instance_id)), '''') IS NULL
THROW 51007, ''instance_id is required'', 1;
IF @snapshot_generation <= 0
THROW 51008, ''snapshot_generation must be positive'', 1;
DECLARE @pre_source_count bigint, @post_source_count bigint,
@pre_source_signature varbinary(32), @post_source_signature varbinary(32),
@source_payload nvarchar(max), @synchronized_at datetime2,
@numeric_warehouse_id decimal(38,0),
@current_generation bigint;
IF LTRIM(RTRIM(@warehouse_id)) NOT LIKE ''%[^0-9]%''
SET @numeric_warehouse_id = TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(@warehouse_id)));
BEGIN TRY
BEGIN TRANSACTION;
SELECT @current_generation = state.snapshot_generation
FROM alg.shadow_snapshot_state state WITH (UPDLOCK, HOLDLOCK)
WHERE state.warehouse_id = @warehouse_id;
IF @current_generation IS NOT NULL AND @snapshot_generation <= @current_generation
THROW 51009, ''snapshot_generation must advance the published generation'', 1;
SELECT @pre_source_count =
(SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) WHERE cargo.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK)
JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
WHERE cargo.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) WHERE job.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK)
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
WHERE job.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id)))
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id)));
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id],
cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name],
cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no],
cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw],
cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code]
FROM dbo.bas_cargo_space cargo WITH (NOLOCK)
WHERE cargo.warehouse_id = @warehouse_id
ORDER BY cargo.cargo_space_id
FOR XML PATH(''cargo''), ROOT(''cargo_spaces''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|STOCK|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id],
stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code],
stock.spare4 AS [@project_no], stock.product_id AS [@product_no],
stock.lot_no AS [@lot_no], stock.in_date AS [@in_date],
stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity],
stock.spare1 AS [@spare1], stock.spare2 AS [@spare2], stock.spare3 AS [@spare3],
stock.voltage AS [@voltage], stock.phase AS [@phase],
(SELECT COUNT(*) FROM dbo.mydo_job trayJob WITH (NOLOCK)
WHERE trayJob.tray_code = stock.tray_code) AS [@tray_job_count]
FROM dbo.bas_stock stock WITH (NOLOCK)
JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
WHERE cargo.warehouse_id = @warehouse_id
ORDER BY stock.stock_id
FOR XML PATH(''stock''), ROOT(''stocks''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|JOB|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no],
job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id],
job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out],
job.create_time AS [@create_time], job.finish_time AS [@finish_time],
job.warehouse_id AS [@warehouse_id]
FROM dbo.mydo_job job WITH (NOLOCK)
WHERE job.warehouse_id = @warehouse_id
ORDER BY job.job_id
FOR XML PATH(''job''), ROOT(''jobs''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|JOB_DETAIL|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id],
detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no],
detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id],
detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id],
detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity],
detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity],
detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id]
FROM dbo.mydo_job_detail detail WITH (NOLOCK)
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
WHERE job.warehouse_id = @warehouse_id
ORDER BY detail.job_detail_id
FOR XML PATH(''job_detail''), ROOT(''job_details''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|INBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid],
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
job.warehouse_id AS [@warehouse_id], stockin.warehouse_id AS [@source_warehouse_id],
job.task_no AS [@task_no],
job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
detail.in_or_out_id AS [@request_id]
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id))
ORDER BY inbound.stockin_detail_id
FOR XML PATH(''inbound''), ROOT(''inbound_requests''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|OUTBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
stockout.warehouse_id AS [@source_warehouse_id],
outbound.spare4 AS [@project_no],
outbound.spare1 AS [@spare1], outbound.spare2 AS [@spare2], outbound.spare3 AS [@spare3],
outbound.fid AS [@voltage], outbound.unit AS [@phase],
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
outbound.allot_quantity AS [@allot_quantity]
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id))
ORDER BY outbound.stockout_detail_id
FOR XML PATH(''outbound''), ROOT(''outbound_requests''), TYPE
)), '''');
SET @pre_source_signature = HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), @source_payload));
DELETE FROM alg.cargo_space_snapshot WHERE warehouse_id = @warehouse_id;
DELETE FROM alg.inventory_item_snapshot WHERE warehouse_id = @warehouse_id;
DELETE FROM alg.active_job_snapshot WHERE warehouse_id = @warehouse_id;
DELETE FROM alg.shadow_inbound_request_snapshot WHERE warehouse_id = @warehouse_id;
DELETE FROM alg.shadow_outbound_request_snapshot WHERE warehouse_id = @warehouse_id;
DELETE FROM alg.space_topology WHERE warehouse_id = @warehouse_id;
DELETE FROM alg.space_inventory_summary WHERE warehouse_id = @warehouse_id;
DELETE FROM alg.lane_state WHERE warehouse_id = @warehouse_id;
SET @synchronized_at = SYSUTCDATETIME();
INSERT INTO alg.cargo_space_snapshot (
warehouse_id, space_id, lane_code, cargo_area_id, roadway_id, display_name,
platoon, column_no, floor_no, depth_raw, space_status, locked, tray_code,
snapshot_generation, synchronized_at)
SELECT cargo.warehouse_id, cargo.cargo_space_id, cargo.group_coding_no,
cargo.cargo_area_id, cargo.roadway_id, cargo.cargo_space_name,
cargo.the_platoon, cargo.the_column, cargo.the_floor, cargo.group_coding,
cargo.status, cargo.locked, cargo.tray_code,
@snapshot_generation, @synchronized_at
FROM dbo.bas_cargo_space cargo WITH (NOLOCK)
WHERE cargo.warehouse_id = @warehouse_id;
INSERT INTO alg.inventory_item_snapshot (
warehouse_id, stock_id, space_id, tray_code, project_no, product_no, lot_no,
inbound_time_raw, inbound_at, quantity, out_quantity, snapshot_generation, synchronized_at,
spare1, spare2, spare3, voltage, phase, tray_job_count)
SELECT cargo.warehouse_id, stock.stock_id, stock.cargo_space_id, stock.tray_code,
stock.spare4, stock.product_id, stock.lot_no, stock.in_date,
TRY_CONVERT(datetime2, stock.in_date), stock.quantity, stock.out_quantity,
@snapshot_generation, @synchronized_at,
stock.spare1, stock.spare2, stock.spare3, stock.voltage, stock.phase,
(SELECT COUNT(*) FROM dbo.mydo_job trayJob WITH (NOLOCK)
WHERE trayJob.tray_code = stock.tray_code)
FROM dbo.bas_stock stock WITH (NOLOCK)
JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
WHERE cargo.warehouse_id = @warehouse_id;
INSERT INTO alg.active_job_snapshot (
warehouse_id, job_id, task_no, tray_code, source_space_id, target_space_id,
status, in_or_out, create_time_raw, snapshot_generation, synchronized_at)
SELECT job.warehouse_id, job.job_id, job.task_no, job.tray_code,
NULLIF(LTRIM(RTRIM(job.cargo_space_id)), ''''),
NULLIF(LTRIM(RTRIM(job.target_cargo_space_id)), ''''), job.status,
job.in_or_out, job.create_time, @snapshot_generation, @synchronized_at
FROM dbo.mydo_job job WITH (NOLOCK)
WHERE job.warehouse_id = @warehouse_id
AND (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''');
;WITH inbound_source AS (
SELECT @warehouse_id AS warehouse_id, job.task_no, job.tray_code,
detail.in_or_out_id AS request_id, job.warehouse_id AS raw_warehouse_id,
inbound.spare4 AS project_no, inbound.product_id, inbound.lot_no,
ROW_NUMBER() OVER (
PARTITION BY job.task_no, job.tray_code
ORDER BY inbound.stockin_detail_id, detail.job_detail_id) AS source_rank
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
JOIN dbo.mydo_job_detail detail WITH (NOLOCK)
ON detail.stockin_detail_id = inbound.stockin_detail_id
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id))
)
INSERT INTO alg.shadow_inbound_request_snapshot (
warehouse_id, task_no, tray_no, request_id, raw_warehouse_id,
project_no, product_id, lot_no, snapshot_generation, synchronized_at)
SELECT @warehouse_id, ISNULL(task_no, ''''), ISNULL(tray_code, ''''), request_id,
raw_warehouse_id, project_no, product_id, lot_no,
@snapshot_generation, @synchronized_at
FROM inbound_source
WHERE source_rank = 1;
INSERT INTO alg.shadow_outbound_request_snapshot (
warehouse_id, stockout_detail_id, raw_warehouse_id, project_no, product_id,
requested_quantity, snapshot_generation, synchronized_at,
allocated_quantity, spare1, spare2, spare3, voltage, phase, predicate_version)
SELECT @warehouse_id, outbound.stockout_detail_id, outbound.warehouse_id,
outbound.spare4, outbound.product_id,
CONVERT(decimal(38,6), outbound.quantity),
@snapshot_generation, @synchronized_at,
CONVERT(decimal(38,6), outbound.allot_quantity),
outbound.spare1, outbound.spare2, outbound.spare3, outbound.fid, outbound.unit,
CASE WHEN ISNULL(DATALENGTH(outbound.product_id), 0) <= 80
AND ISNULL(DATALENGTH(outbound.spare1), 0) <= 80
AND ISNULL(DATALENGTH(outbound.spare2), 0) <= 80
AND ISNULL(DATALENGTH(outbound.spare3), 0) <= 80
AND ISNULL(DATALENGTH(outbound.spare4), 0) <= 80
AND ISNULL(DATALENGTH(outbound.fid), 0) <= 80
AND ISNULL(DATALENGTH(outbound.unit), 0) <= 80
THEN 1 ELSE 0 END
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id));
EXEC alg.usp_rebuild_space_topology @warehouse_id = @warehouse_id,
@snapshot_generation = @snapshot_generation;
EXEC alg.usp_sync_inventory_snapshot @warehouse_id = @warehouse_id,
@snapshot_generation = @snapshot_generation;
EXEC alg.usp_rebuild_lane_state @warehouse_id = @warehouse_id,
@snapshot_generation = @snapshot_generation;
SELECT @post_source_count =
(SELECT COUNT_BIG(*) FROM dbo.bas_cargo_space cargo WITH (NOLOCK) WHERE cargo.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.bas_stock stock WITH (NOLOCK)
JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
WHERE cargo.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.mydo_job job WITH (NOLOCK) WHERE job.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.mydo_job_detail detail WITH (NOLOCK)
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
WHERE job.warehouse_id = @warehouse_id)
+ (SELECT COUNT_BIG(*) FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id)))
+ (SELECT COUNT_BIG(*) FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id)));
SELECT @source_payload = ISNULL(CONVERT(nvarchar(max), (
SELECT cargo.cargo_space_id AS [@space_id], cargo.warehouse_id AS [@warehouse_id],
cargo.group_coding_no AS [@lane_code], cargo.cargo_area_id AS [@cargo_area_id],
cargo.roadway_id AS [@roadway_id], cargo.cargo_space_name AS [@display_name],
cargo.the_platoon AS [@platoon], cargo.the_column AS [@column_no],
cargo.the_floor AS [@floor_no], cargo.group_coding AS [@depth_raw],
cargo.status AS [@space_status], cargo.locked AS [@locked], cargo.tray_code AS [@tray_code]
FROM dbo.bas_cargo_space cargo WITH (NOLOCK)
WHERE cargo.warehouse_id = @warehouse_id
ORDER BY cargo.cargo_space_id
FOR XML PATH(''cargo''), ROOT(''cargo_spaces''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|STOCK|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT stock.stock_id AS [@stock_id], stock.cargo_space_id AS [@space_id],
stock.warehouse_id AS [@warehouse_id], stock.tray_code AS [@tray_code],
stock.spare4 AS [@project_no], stock.product_id AS [@product_no],
stock.lot_no AS [@lot_no], stock.in_date AS [@in_date],
stock.quantity AS [@quantity], stock.out_quantity AS [@out_quantity],
stock.spare1 AS [@spare1], stock.spare2 AS [@spare2], stock.spare3 AS [@spare3],
stock.voltage AS [@voltage], stock.phase AS [@phase],
(SELECT COUNT(*) FROM dbo.mydo_job trayJob WITH (NOLOCK)
WHERE trayJob.tray_code = stock.tray_code) AS [@tray_job_count]
FROM dbo.bas_stock stock WITH (NOLOCK)
JOIN dbo.bas_cargo_space cargo WITH (NOLOCK) ON cargo.cargo_space_id = stock.cargo_space_id
WHERE cargo.warehouse_id = @warehouse_id
ORDER BY stock.stock_id
FOR XML PATH(''stock''), ROOT(''stocks''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|JOB|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT job.job_id AS [@job_id], job.status AS [@status], job.task_no AS [@task_no],
job.tray_code AS [@tray_code], job.cargo_space_id AS [@source_space_id],
job.target_cargo_space_id AS [@target_space_id], job.in_or_out AS [@in_or_out],
job.create_time AS [@create_time], job.finish_time AS [@finish_time],
job.warehouse_id AS [@warehouse_id]
FROM dbo.mydo_job job WITH (NOLOCK)
WHERE job.warehouse_id = @warehouse_id
ORDER BY job.job_id
FOR XML PATH(''job''), ROOT(''jobs''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|JOB_DETAIL|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT detail.job_detail_id AS [@job_detail_id], detail.job_id AS [@job_id],
detail.stock_id AS [@stock_id], detail.lot_no AS [@lot_no],
detail.in_date AS [@in_date], detail.in_or_out_id AS [@request_id],
detail.in_or_out_d_id AS [@request_detail_id], detail.product_id AS [@product_id],
detail.stock_status AS [@stock_status], detail.in_quantity AS [@in_quantity],
detail.stock_quantity AS [@stock_quantity], detail.out_quantity AS [@out_quantity],
detail.pick_quantity AS [@pick_quantity], detail.stockin_detail_id AS [@stockin_detail_id]
FROM dbo.mydo_job_detail detail WITH (NOLOCK)
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
WHERE job.warehouse_id = @warehouse_id
ORDER BY detail.job_detail_id
FOR XML PATH(''job_detail''), ROOT(''job_details''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|INBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT inbound.stockin_detail_id AS [@stockin_detail_id], inbound.stockin_guid AS [@stockin_guid],
inbound.stockin_id AS [@stockin_id], inbound.lot_no AS [@lot_no],
inbound.product_id AS [@product_id], inbound.quantity AS [@quantity],
inbound.assign_num AS [@assign_num], inbound.spare4 AS [@project_no],
job.warehouse_id AS [@warehouse_id], stockin.warehouse_id AS [@source_warehouse_id],
job.task_no AS [@task_no],
job.tray_code AS [@tray_code], detail.job_id AS [@job_id],
detail.in_or_out_id AS [@request_id]
FROM dbo.in_stockin_detail inbound WITH (NOLOCK)
JOIN dbo.mydo_job_detail detail WITH (NOLOCK) ON detail.stockin_detail_id = inbound.stockin_detail_id
JOIN dbo.mydo_job job WITH (NOLOCK) ON job.job_id = detail.job_id
JOIN dbo.in_stockin stockin WITH (NOLOCK) ON stockin.stockin_id = inbound.stockin_id
WHERE (job.finish_time IS NULL OR LTRIM(RTRIM(job.finish_time)) = '''')
AND (LTRIM(RTRIM(stockin.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockin.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockin.warehouse_id))) = @numeric_warehouse_id))
ORDER BY inbound.stockin_detail_id
FOR XML PATH(''inbound''), ROOT(''inbound_requests''), TYPE
)), '''');
SET @source_payload = @source_payload + ''|OUTBOUND|'' + ISNULL(CONVERT(nvarchar(max), (
SELECT outbound.stockout_detail_id AS [@stockout_detail_id], outbound.warehouse_id AS [@warehouse_id],
stockout.warehouse_id AS [@source_warehouse_id],
outbound.spare4 AS [@project_no],
outbound.spare1 AS [@spare1], outbound.spare2 AS [@spare2], outbound.spare3 AS [@spare3],
outbound.fid AS [@voltage], outbound.unit AS [@phase],
outbound.product_id AS [@product_id], outbound.lot_no AS [@lot_no],
outbound.quantity AS [@quantity], outbound.assign_num AS [@assign_num],
outbound.allot_quantity AS [@allot_quantity]
FROM dbo.out_stockout_detail outbound WITH (NOLOCK)
JOIN dbo.out_stockout stockout WITH (NOLOCK) ON stockout.stockout_id = outbound.stockout_id
WHERE ISNULL(outbound.quantity, 0) > ISNULL(outbound.allot_quantity, 0)
AND (LTRIM(RTRIM(stockout.warehouse_id)) = LTRIM(RTRIM(@warehouse_id))
OR (@numeric_warehouse_id IS NOT NULL
AND LTRIM(RTRIM(stockout.warehouse_id)) NOT LIKE ''%[^0-9]%''
AND TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id))) = @numeric_warehouse_id))
ORDER BY outbound.stockout_detail_id
FOR XML PATH(''outbound''), ROOT(''outbound_requests''), TYPE
)), '''');
SET @post_source_signature = HASHBYTES(''SHA2_256'', CONVERT(varbinary(max), @source_payload));
IF @pre_source_count <> @post_source_count
OR @pre_source_signature <> @post_source_signature
THROW 51025, ''FT546 shadow refresh refused: source changed during refresh'', 1;
MERGE alg.shadow_snapshot_state WITH (HOLDLOCK) AS target
USING (SELECT @warehouse_id AS warehouse_id) AS source
ON source.warehouse_id = target.warehouse_id
WHEN MATCHED THEN UPDATE SET
snapshot_generation = @snapshot_generation,
snapshot_status = ''PUBLISHED'',
synchronized_at = SYSUTCDATETIME(),
source_signature = @post_source_signature,
source_row_count = @post_source_count,
instance_id = @instance_id,
last_error = NULL
WHEN NOT MATCHED THEN INSERT (
warehouse_id, snapshot_generation, snapshot_status, synchronized_at,
source_signature, source_row_count, instance_id, last_error)
VALUES (
@warehouse_id, @snapshot_generation, ''PUBLISHED'', SYSUTCDATETIME(),
@post_source_signature, @post_source_count, @instance_id, NULL);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH;
END;';
EXEC sys.sp_executesql @refresh_sql;
-- Keep the generation monotonic; old rows cannot become predicate-version-1 evidence.
UPDATE alg.shadow_snapshot_state WITH (UPDLOCK, HOLDLOCK)
SET snapshot_status = 'INITIALIZING', source_signature = NULL, synchronized_at = NULL,
last_error = NULL;
UPDATE alg.shadow_package_manifest
SET package_version = '2026-09-07-33'
WHERE package_id = 'FT546WMS_SHADOW_ONLY' AND package_version = '2026-09-03-3';
IF @@ROWCOUNT <> 1
THROW 51069, 'V3.3 upgrade refused: manifest changed during upgrade', 1;
COMMIT TRANSACTION;
PRINT 'V33_UPGRADE_OK';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
GO

View File

@@ -0,0 +1,56 @@
package com.ruoyi.algorithm.repository;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import static org.assertj.core.api.Assertions.assertThat;
class ShadowV33MigrationTest {
private String migration() throws Exception {
try (var input = getClass().getResourceAsStream("/db/sqlserver/upgrade-shadow-v3-to-v33.sql")) {
assertThat(input).as("V3.3 migration resource").isNotNull();
return new String(input.readAllBytes(), StandardCharsets.UTF_8).toLowerCase(Locale.ROOT)
.replace("''", "'");
}
}
@Test
void guardsExactOwnershipAndStoppedInstanceBeforeTransactionalChanges() throws Exception {
String sql = migration();
assertThat(sql).contains("2026-09-03-3", "2026-09-07-33", "ft546wms_shadow_only",
"unknown object", "expected object", "updlock, holdlock", "last_seen_at", "claimed_at",
"dateadd(second, -120, sysutcdatetime())", "set lock_timeout 5000", "rollback transaction");
assertThat(sql.indexOf("unknown object")).isLessThan(sql.indexOf("alter table"));
assertThat(sql.indexOf("begin transaction")).isLessThan(sql.indexOf("alter table"));
assertThat(sql.indexOf("recent shadow heartbeat")).isLessThan(sql.indexOf("alter table"));
assertThat(sql).doesNotContain("alter table dbo.", "update dbo.", "delete from dbo.",
"truncate table", "drop table", "delete from alg.decision_log", "update alg.decision_log");
}
@Test
void bothSourceSignaturesCaptureAllNewPredicateInputs() throws Exception {
String sql = migration();
for (String field : new String[]{"spare1", "spare2", "spare3", "voltage", "phase"}) {
assertThat(sql.split("stock\\." + field + " as", -1)).hasSize(3);
}
for (String field : new String[]{"spare1", "spare2", "spare3", "spare4", "fid", "unit"}) {
assertThat(sql.split("outbound\\." + field + " as", -1)).hasSize(3);
}
assertThat(sql).contains("@pre_source_signature <> @post_source_signature", "source changed during refresh");
assertThat(sql.split(java.util.regex.Pattern.quote("as [@tray_job_count]"), -1)).hasSize(3);
}
@Test
void newSnapshotsUseBusinessProjectRemainingInputsAndVersionMarker() throws Exception {
String sql = migration();
assertThat(sql).contains("outbound.spare4, outbound.product_id", "outbound.allot_quantity",
"outbound.spare1, outbound.spare2, outbound.spare3, outbound.fid, outbound.unit",
"datalength(outbound.spare4)", "then 1 else 0 end",
"predicate_version int null", "allocated_quantity decimal(38,6) null", "tray_job_count int null");
assertThat(sql).doesNotContain("warehouse.project_no", "join dbo.bas_warehouse");
assertThat(sql).contains("snapshot_status = 'initializing'", "source_signature = null");
assertThat(sql).doesNotContain("set snapshot_generation = 0");
}
}