From 699310e61dd5bc9974b0bfdbfbdf44c887c6a00c Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Mon, 7 Sep 2026 12:43:43 +0800 Subject: [PATCH] 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. --- doc/algorithm-shadow/README.md | 2 + doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md | 114 ++++ .../db/sqlserver/uninstall-shadow.sql | 3 +- .../db/sqlserver/upgrade-shadow-v3-to-v33.sql | 557 ++++++++++++++++++ .../repository/ShadowV33MigrationTest.java | 56 ++ ruoyi-admin/pom.xml | 6 + .../shadow/ShadowPlanningEngine.java | 91 ++- .../shadow/ShadowStockReference.java | 29 + .../shadow/mapper/ShadowInputMapper.java | 4 + .../mapper/algorithm/ShadowInputMapper.xml | 58 +- .../shadow/ShadowOutboundPredicateTest.java | 283 +++++++++ .../shadow/ShadowOutboundValidationTest.java | 158 +++++ .../shadow/ShadowPlanningEngineTest.java | 7 + .../shadow/ShadowV3OperationsPackageTest.java | 24 +- .../resources/shadow/Sqlcmd13OutputFixture.cs | 4 + scripts/shadow-v3-install.cmd | 14 +- scripts/shadow-v3-preflight.cmd | 7 +- scripts/shadow-v3-uninstall.cmd | 2 +- 18 files changed, 1394 insertions(+), 25 deletions(-) create mode 100644 doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md create mode 100644 fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql create mode 100644 fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowOutboundPredicateTest.java create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowOutboundValidationTest.java diff --git a/doc/algorithm-shadow/README.md b/doc/algorithm-shadow/README.md index 7c192a2..3f1b518 100644 --- a/doc/algorithm-shadow/README.md +++ b/doc/algorithm-shadow/README.md @@ -1,5 +1,7 @@ # FT546WMS 影子 v3 现场操作手册 +已部署 V3/V3.1/V3.2 的现场升级到 V3.3,请先使用 [V3.3 升级和回滚步骤](SHADOW_V33_DEPLOYMENT.md)。下面保留最初 v2 到 v3 的历史操作说明,不要把“当前现场已有 v2”套用于已升级现场。 + ## 1. 这次部署做什么 目标分支:`test/ft546wms-shadow-v3-20260903`。 diff --git a/doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md b/doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md new file mode 100644 index 0000000..cd305fc --- /dev/null +++ b/doc/algorithm-shadow/SHADOW_V33_DEPLOYMENT.md @@ -0,0 +1,114 @@ +# FT546WMS 影子 V3.3:升级和回滚 + +适用现场:WMS 已运行影子 V3/V3.1/V3.2,数据库包版本为 `2026-09-03-3`。 +分支仍为 `test/ft546wms-shadow-v3-20260903`。这不是正式算法切换。 + +## 这次改什么 + +自动出库影子请求的项目号改取出库明细 `spare4`,不再取仓库配置中的 `FT546`。 +补齐产品、spare1/2/3、项目、fid 对电压、unit 对相别的可选筛选,以及库位锁定、状态、托盘一致性检查。 +按计划数量减已分配数量观察。新旧输入通过 `predicate_version` 区分,不把历史样本重新认定为新版本通过。 + +共享规划器和 Legacy 存储过程、调用入口没有修改。升级只新增 `alg` 表的可空字段、替换影子刷新过程、使旧快照失效并更新影子包版本。保留决策日志和累计指标。 + +注意:影子后台仍会增加数据库读取和计算开销,不能承诺对性能绝对零影响。升级过程需要一次计划内短暂停机。 + +## 第一步:先准备,不停程序 + +让服务器 AI 核实 IDEA 当前运行的 **WMS** 项目目录、分支、提交号、环境变量和日志目录,备份本地配置,并导出现有影子日志。 +不要操作 WCS 或 WCS 影子程序,也不要因进程里有 `wcs` 字样就停止它。 + +在已经确认的 WMS 项目根目录打开 CMD,执行: + +```cmd +git status --short +git branch --show-current +git rev-parse HEAD +git fetch origin +``` + +保存当前提交号作为回退点。工作区有改动时先停止同步,让服务器 AI 保存和核对改动,不要强制覆盖。 +获得本次已推送的交付提交号后,再执行 `git pull --ff-only` 并核对 `git rev-parse HEAD`。 +本地未推送的代码不能靠服务器 `git pull` 获得,不要猜提交号。 + +IDEA 重新加载 Maven,使用当前可用的 JDK 17 和 Maven 3.9.11 编译测试。 +首次可能需要下载 H2 测试依赖;H2 不进入运行包,依赖问题应在停机前解决。 +程序运行期间不要点 Build Project、热替换或重启;可在独立准备目录做构建,确认完成后再安排维护窗口。 + +## 第二步:先在数据库备份副本演练 + +本地 Java 测试和 SQL 2016 语法解析不能代替 SQL Server 实际升级验证。 +让服务器 AI/DBA 在隔离 SQL Server 上恢复备份(数据库名保持 `FT546-wms`),先验证升级成功、重复执行无破坏、升级失败可回滚。 +不要在生产库创建“测试作业”,不要触发业务回放。演练失败就不进入下一步。 + +## 第三步:低峰时停止 WMS,升级影子表 + +1. 在 IDEA 停止当前那个 WMS 实例,确认其 PID 已退出。不要同时启动稳定 WMS 和影子 WMS。 +2. 等待至少 130 秒,确认没有其他 WMS 影子实例仍在运行。 +3. 在项目根目录的 CMD 执行下面命令(使用现有 Windows 集成认证): + +```cmd +call scripts\shadow-v3-install.cmd "10.6.22.6,1433" "FT546-wms" v33 +``` + +必须看到 `V33_UPGRADE_OK`(重复执行为 `V33_ALREADY_INSTALLED`)和 `INSTALL_OK mode=v33`。 +现场已有 V3,**不要用 `clean`,不要卸载后重装,不要执行旧版 v2 升级来绕过错误**。 + +遇到心跳、清单、字段或锁超时错误,保存完整输出并停止。脚本对本次变更执行事务回滚;不要修改 manifest 绕过检查。 + +## 第四步:只启动一次,保持影子配置 + +沿用现场已有 Run Configuration,只核对下面两个值,其他连接、日志和限流设置不要随意覆盖: + +```text +FORTUCKY_SHADOW_ENABLED=true +FORTUCKY_ALGORITHM_MODE=SHADOW +``` + +在 IDEA 启动一次 WMS。记录启动时间和时区。不要设置 `ACTIVE`。 +旧快照已失效,后台会重新生成;刚启动还没有新快照或样本,不代表需要再次重启。 + +先检查登录、查询,再由现场人员按正常流程验证入库、自动出库、手动出库。 +让服务器 AI 仅做只读核对: + +```sql +SELECT package_id, package_version FROM alg.shadow_package_manifest; +SELECT warehouse_id, snapshot_generation, snapshot_status, synchronized_at, last_error +FROM alg.shadow_snapshot_state WHERE warehouse_id = '01'; +SELECT TOP (20) stockout_detail_id, project_no, product_id, requested_quantity, + allocated_quantity, predicate_version, synchronized_at +FROM alg.shadow_outbound_request_snapshot +WHERE warehouse_id = '01' ORDER BY synchronized_at DESC; +SELECT TOP (30) * FROM alg.decision_log ORDER BY created_at DESC; +``` + +包版本应为 `2026-09-07-33`,新快照应为 `PUBLISHED`,正常长度的请求输入应有 `predicate_version=1`。 +实际自动出库产生后,新日志应包含 `predicateVersion=1`;可用建议还应有真实的 `proposedStock` 和 `selectedProject`。 +快照刷新由事件触发,没业务时同步时间不连续推进,不要仅凭这个重启。 + +## 出问题怎么回滚 + +最短路径:停止 WMS,在原 IDEA 配置里只把 `FORTUCKY_SHADOW_ENABLED` 改为 `false`,完整重启一次,再检查正常业务。 +如仍异常,停止这个实例,使用第一步保留的原稳定源码和 Run Configuration 启动,仍保持影子关闭。 +不要两个 WMS 同时运行;不要执行 `git reset --hard` 来覆盖现场配置。 + +紧急回滚不卸载数据库。新增的 `alg` 字段不参与 Legacy;保留它们和历史数据供排查。 +若回到旧影子代码,也先关闭影子,避免用旧逻辑继续采集混杂样本。 + +确实需要完整卸载时,必须先备份影子数据、恢复稳定业务并停用影子,等待至少 130 秒,再由 DBA 执行: + +```cmd +call scripts\shadow-v3-uninstall.cmd "10.6.22.6,1433" "FT546-wms" CONFIRM_UNINSTALL +``` + +卸载会删除全部本包 `alg` 对象及历史数据,只能从事先导出的证据/数据库备份恢复。它不是升级的必需步骤。 + +## 后续怎样判断 + +重新记录 V3.3 开始时间,只分析此次重启后的自然业务样本,建议覆盖至少 7 天及高峰。 +继续使用 `shadow-v3-export.cmd`,让服务器 AI 同时提供准确分支、提交、IDEA 配置核对结果、完整日志和本次升级输出。 +累计性能指标包含旧版本,必须用升级前后导出差值,不能当成 V3.3 单独指标。 + +本版保守跳过:多项目/多产品候选、候选超过 200 行、建议不在合格候选集、一托多库存行、单条库存不足以覆盖剩余数量,以及托盘已有作业。 +现场动态 SQL 参数限长 80 字节;超长原始条件保留在快照,但标记版本 0,不作为有效输入。不会截断条件来凑匹配。 +这些跳过不算通过。`READY` 只表示受支持范围内的库存建议通过核验,不证明完整出库流程、FIFO 效果、性能或正式切换已验收。 diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql index 8194ebf..d5aaf6c 100644 --- a/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql +++ b/fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql @@ -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; diff --git a/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql b/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql new file mode 100644 index 0000000..a8bdc32 --- /dev/null +++ b/fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v3-to-v33.sql @@ -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 diff --git a/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java b/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java new file mode 100644 index 0000000..6a0fb07 --- /dev/null +++ b/fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowV33MigrationTest.java @@ -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"); + } +} diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 61f0a19..f443b1b 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -131,6 +131,12 @@ test + + com.h2database + h2 + test + + com.microsoft.sqlserver 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 4761709..b5d2ab9 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 @@ -13,6 +13,7 @@ import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionTemplate; import java.time.Instant; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -29,6 +30,7 @@ public class ShadowPlanningEngine { private static final int RESULT_REASON_MAX_LENGTH = 600; private static final int NUMBER_MAX_LENGTH = 64; private static final String PUBLISHED = "PUBLISHED"; + private static final int MAX_OUTBOUND_CANDIDATES = 200; private final SchedulingFacade schedulingFacade; private final ShadowInputMapper inputMapper; @@ -87,19 +89,83 @@ public class ShadowPlanningEngine { String warehouseId = snapshot.warehouseId(); ShadowStockReference reference = inputMapper.selectProjectOutboundReference( warehouseId, snapshot.generation(), stockoutDetailId); - if (reference == null || isBlank(reference.getProjectNo())) { - return OutboundShadowObservation.unavailable(snapshot, stockoutDetailId, - "SKIPPED_MISSING_PROJECT", - outboundDiagnostic(snapshot, stockoutDetailId, reference, - "No project number was available for shadow planning")); + if (reference == null || !Integer.valueOf(1).equals(reference.getPredicateVersion())) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_MISSING_PREDICATE_INPUT", "Request is missing or predates predicate version 1"); } + if (reference.getRequestedQuantity() == null || reference.getRequestedQuantity().signum() <= 0) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_INVALID_REMAINING_QUANTITY", "No positive unallocated quantity was captured"); + } + List candidates = inputMapper.selectProjectOutboundCandidates( + warehouseId, snapshot.generation(), stockoutDetailId); + if (candidates.size() > MAX_OUTBOUND_CANDIDATES) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_CANDIDATE_LIMIT", "Eligible candidate set exceeds 200 rows"); + } + if (candidates.isEmpty()) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "ERROR_STOCK_NOT_FOUND", "No inventory satisfies the captured outbound predicates"); + } + var projects = candidates.stream().map(ShadowStockReference::getProjectNo).distinct().toList(); + if (projects.size() != 1 || isBlank(projects.get(0))) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_UNSUPPORTED_PROJECT_SCOPE", "Planner requires one nonblank candidate project"); + } + var products = candidates.stream().map(ShadowStockReference::getProductId).distinct().toList(); + if (products.size() != 1 || isBlank(products.get(0))) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_UNSUPPORTED_PRODUCT_SCOPE", "Planner requires one nonblank candidate product"); + } + // The shared planner is single-source and does not apply the site's optional predicates. + // Keep it unchanged; validate its proposal against the generation-qualified candidate set. OutboundRequest request = new OutboundRequest(stockoutDetailId, null, - reference.getProjectNo(), reference.getProductId(), null, null, null, true); - return toOutboundObservation(snapshot, reference.getRequestId(), reference, - schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request)); + projects.get(0), products.get(0), null, null, null, false); + OutboundPlan plan = schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request); + if (plan.assignment().isEmpty()) { + return toOutboundObservation(snapshot, stockoutDetailId, reference, plan); + } + var assignment = plan.assignment().orElseThrow(); + var selected = candidates.stream().filter(candidate -> + Objects.equals(candidate.getSpaceId(), assignment.sourceSpaceId()) + && Objects.equals(candidate.getTrayNo(), assignment.trayNo()) + && Objects.equals(candidate.getProjectNo(), assignment.projectNo()) + && Objects.equals(candidate.getProductId(), assignment.productId())).toList(); + if (selected.size() != 1) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_INELIGIBLE_PROPOSAL", "Proposed source is not one unambiguous eligible stock row"); + } + ShadowStockReference stock = selected.get(0); + if (!Integer.valueOf(0).equals(stock.getTrayJobCount())) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_EXISTING_TRAY_JOB", "Existing tray jobs require unsupported delivery compatibility checks"); + } + if (!Integer.valueOf(1).equals(stock.getTrayItemCount())) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_MULTI_STOCK_TRAY", "Selected tray does not contain exactly one inventory row"); + } + if (stock.getAvailableQuantity() == null + || stock.getAvailableQuantity().compareTo(reference.getRequestedQuantity()) < 0) { + return unavailableProject(snapshot, stockoutDetailId, reference, + "SKIPPED_MULTI_STOCK_REQUEST", "Selected single stock cannot cover the remaining request quantity"); + } + StringBuilder reason = new StringBuilder(outboundDiagnostic(snapshot, stockoutDetailId, reference, + assignment.reason())); + appendField(reason, "selectedProject", stock.getProjectNo(), WIDE_IDENTIFIER_MAX_LENGTH); + appendField(reason, "selectedTray", stock.getTrayNo(), WIDE_IDENTIFIER_MAX_LENGTH); + appendField(reason, "availableQuantity", objectOrEmpty(stock.getAvailableQuantity()), NUMBER_MAX_LENGTH); + return new OutboundShadowObservation(true, snapshot.generation(), warehouseId, + snapshot.synchronizedAt(), snapshot.sourceSignature(), stockoutDetailId, + "READY", stock.getStockId(), stock.getSpaceId(), reason.toString()); }); } + private OutboundShadowObservation unavailableProject(PublishedShadowSnapshot snapshot, String requestId, + ShadowStockReference reference, String code, String reason) { + return OutboundShadowObservation.unavailable(snapshot, requestId, code, + outboundDiagnostic(snapshot, requestId, reference, reason)); + } + public OutboundShadowObservation planExactStockOutbound(PublishedShadowSnapshot snapshot, String stockoutDetailId, String stockId) { return planWithStableSnapshot(snapshot, () -> { @@ -313,6 +379,15 @@ public class ShadowPlanningEngine { reference == null ? null : reference.getSnapshotSynchronizedAt(), SNAPSHOT_SYNC_MAX_LENGTH); appendField(diagnostic, "shadowMatches", numberOrUnknown( reference == null ? null : reference.getShadowMatchCount()), NUMBER_MAX_LENGTH); + appendField(diagnostic, "predicateVersion", numberOrUnknown( + reference == null ? null : reference.getPredicateVersion()), NUMBER_MAX_LENGTH); + if (reference != null && reference.getPredicateVersion() != null) { + appendField(diagnostic, "spare1", reference.getSpare1(), 120); + appendField(diagnostic, "spare2", reference.getSpare2(), 120); + appendField(diagnostic, "spare3", reference.getSpare3(), 120); + appendField(diagnostic, "voltage", reference.getVoltage(), 120); + appendField(diagnostic, "phase", reference.getPhase(), 120); + } appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH); return diagnostic.toString(); } 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 fad13ab..ca57d26 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 @@ -13,6 +13,35 @@ public class ShadowStockReference { private String snapshotSynchronizedAt; private BigDecimal requestedQuantity; private Integer shadowMatchCount; + private Integer predicateVersion; + private String spare1; + private String spare2; + private String spare3; + private String voltage; + private String phase; + private BigDecimal availableQuantity; + private Integer trayItemCount; + private Integer trayJobCount; + + public Integer getTrayJobCount() { return trayJobCount; } + public void setTrayJobCount(Integer value) { trayJobCount = value; } + + public Integer getPredicateVersion() { return predicateVersion; } + public void setPredicateVersion(Integer value) { predicateVersion = value; } + public String getSpare1() { return spare1; } + public void setSpare1(String value) { spare1 = value; } + public String getSpare2() { return spare2; } + public void setSpare2(String value) { spare2 = value; } + public String getSpare3() { return spare3; } + public void setSpare3(String value) { spare3 = value; } + public String getVoltage() { return voltage; } + public void setVoltage(String value) { voltage = value; } + public String getPhase() { return phase; } + public void setPhase(String value) { phase = value; } + public BigDecimal getAvailableQuantity() { return availableQuantity; } + public void setAvailableQuantity(BigDecimal value) { availableQuantity = value; } + public Integer getTrayItemCount() { return trayItemCount; } + public void setTrayItemCount(Integer value) { trayItemCount = value; } public String getRequestId() { return requestId; diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java index 1b7bb2b..142ccef 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java @@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.time.Instant; +import java.util.List; @Mapper public interface ShadowInputMapper { @@ -21,6 +22,9 @@ public interface ShadowInputMapper { ShadowStockReference selectStockReference(@Param("warehouseId") String warehouseId, @Param("generation") long generation, @Param("stockId") String stockId); + List selectProjectOutboundCandidates(@Param("warehouseId") String warehouseId, + @Param("generation") long generation, @Param("stockoutDetailId") String stockoutDetailId); + boolean claimInstance(@Param("warehouseId") String warehouseId, @Param("instanceId") String instanceId, @Param("staleSeconds") int staleSeconds); diff --git a/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml b/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml index 5e44a6d..465f443 100644 --- a/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml +++ b/ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml @@ -25,6 +25,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and input.tray_no = #{trayNo} + + item.warehouse_id = #{warehouseId} + and item.snapshot_generation = #{generation} + and cargo.warehouse_id = item.warehouse_id + and cargo.snapshot_generation = item.snapshot_generation + and cargo.space_id = item.space_id and cargo.tray_code = item.tray_code + and cargo.locked = 'N' + and ((cargo.space_status = '2' and item.out_quantity = 0) + or (cargo.space_status = '4' and item.quantity > item.out_quantity)) + and (nullif(input.project_no, '') is null or item.project_no = input.project_no) + and (nullif(input.product_id, '') is null or item.product_no = input.product_id) + and (nullif(input.spare1, '') is null or item.spare1 = input.spare1) + and (nullif(input.spare2, '') is null or item.spare2 = input.spare2) + and (nullif(input.spare3, '') is null or item.spare3 = input.spare3) + and (nullif(input.voltage, '') is null or item.voltage = input.voltage) + and (nullif(input.phase, '') is null or item.phase = input.phase) + and not exists ( + select 1 from alg.active_job_snapshot job + where job.warehouse_id = item.warehouse_id + and job.snapshot_generation = item.snapshot_generation + and job.tray_code = item.tray_code and job.in_or_out = '26') + + + +