From ae7f8c08f453ec179dedd6f7744ca13f407feddb Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Thu, 10 Sep 2026 16:03:20 +0800 Subject: [PATCH] feat(wms): add controlled three-channel mixed-storage pilot --- scripts/mixed-pilot/README.md | 92 +++++++ scripts/mixed-pilot/capture-test-fixture.ps1 | 20 ++ scripts/mixed-pilot/control-mixed-pilot.ps1 | 101 +++++++ scripts/mixed-pilot/install-mixed-pilot.ps1 | 92 +++++++ scripts/mixed-pilot/mixed-pilot.cmd | 18 ++ scripts/mixed-pilot/objects.sql | 256 ++++++++++++++++++ scripts/mixed-pilot/test-mixed-pilot.ps1 | 121 +++++++++ scripts/mixed-pilot/tests/behavior.sql | 138 ++++++++++ scripts/mixed-pilot/tests/legacy-outbound.sql | 29 ++ scripts/mixed-pilot/tests/seed.sql | 22 ++ scripts/mixed-pilot/wrappers.sql | 112 ++++++++ 11 files changed, 1001 insertions(+) create mode 100644 scripts/mixed-pilot/README.md create mode 100644 scripts/mixed-pilot/capture-test-fixture.ps1 create mode 100644 scripts/mixed-pilot/control-mixed-pilot.ps1 create mode 100644 scripts/mixed-pilot/install-mixed-pilot.ps1 create mode 100644 scripts/mixed-pilot/mixed-pilot.cmd create mode 100644 scripts/mixed-pilot/objects.sql create mode 100644 scripts/mixed-pilot/test-mixed-pilot.ps1 create mode 100644 scripts/mixed-pilot/tests/behavior.sql create mode 100644 scripts/mixed-pilot/tests/legacy-outbound.sql create mode 100644 scripts/mixed-pilot/tests/seed.sql create mode 100644 scripts/mixed-pilot/wrappers.sql diff --git a/scripts/mixed-pilot/README.md b/scripts/mixed-pilot/README.md new file mode 100644 index 0000000..fdd2d88 --- /dev/null +++ b/scripts/mixed-pilot/README.md @@ -0,0 +1,92 @@ +# FT546WMS 指定通道混放试点 + +这是旧版分配规则的应急补丁,与影子算法是否接管无关。无需更换 WMS/WCS JAR;安装后默认关闭,启用后才允许新增混放。 + +## 试点范围 + +| 通道 | 内侧原项目位置 | 外侧可混放位置 | 当前新增容量 | +| --- | --- | --- | --- | +| C1203 | 120803 | 120703 | 1 托盘 | +| C0204 | 020804 | 020704 | 1 托盘 | +| C0205 | 020805 | 020705 | 1 托盘 | + +白名单按**整条通道**设置,入库和出库保护覆盖两端。三条双深通道共六个位置,内侧已有货,因此本次最多新增三个同时在库的客居托盘。不会把全库零散空位一起开放。 + +现场于 2026-09-10 确认所有项目允许分托盘共用通道,并确认这三个外侧位置适配当前托盘。一个托盘仍只能有一个明确项目;999/无项目托盘保持原 B/D 区规则。项目号和库存归属不改写。 + +原规则找到货位时优先采用原分配。原规则返回无货位时,才尝试白名单通道的外侧空位。每通道最多两个项目,内侧不能越过外侧托盘补货;有在途任务或锁定时拒绝。新增混放时,同层必须至少有两个可通行的 E 区空位。该检查不是长期预留,后续移库仍会重新检查和占用目标。 + +## CMD 操作 + +在服务器 CMD 中进入本文件所在目录。命令使用 Windows 集成认证,CMD 会自动调用系统 PowerShell,无需在命令里填写数据库密码。 + +1. 安装(开关仍关闭): + +```bat +call mixed-pilot.cmd install "lpc:." "FT546-wms" +``` + +出现 `INSTALL_OK` 才表示成功。安装核对三个原存储过程的 SHA-256,版本不符、锁等待超过两秒或执行失败时,事务回滚,不覆盖现场改动。脚本创建触发器和替换过程时需要短暂架构锁;业务繁忙导致安装失败时稍后重试,不能去杀业务会话。 + +2. 查询配置和现场状态: + +```bat +call mixed-pilot.cmd status "lpc:." "FT546-wms" +``` + +3. 已确认所有项目可混放、托盘适配后开启: + +```bat +call mixed-pilot.cmd enable "lpc:." "FT546-wms" CONFIRM-FIT-ALL-PROJECTS +``` + +出现 `ENABLED` 才表示三条通道均已启用。启用会重新核对内侧库存、外侧空位和在途任务,条件变化则整次拒绝。 + +4. 像原来一样创建入库任务。出库优先使用**自动分配**,它会按旧程序先为外侧托盘建立移库,再建立内侧出库。手工选择被挡住的内侧库存、却没有外侧腾挪任务时,会明确拒绝,不能手动把等待任务改为已下发或已完成。 + +## 观察和退出 + +内侧任务等待期间状态为 `8`,含义是“等待前位清空”;现有界面字典未配置该标签时可能直接显示数字 8。`status` 命令的 `waiting` 给出任务清单。只有外侧货位、库存和在途任务均已清空,内侧任务才恢复状态 `1`,由现有 WMS 定时推送交给 WCS。创建移库任务本身不会释放等待。 + +先验证一组完整业务:新项目入库 → 前位正常出库 → 再混放一托 → 后位自动出库并完成前位移库。测试通过后再增加通道数量;本包不开放 A 区深通道,也不切换整套新算法。 + +出现异常时先关闭**新增混放**,无需重启: + +```bat +call mixed-pilot.cmd disable "lpc:." "FT546-wms" +``` + +关闭会保留已混放库存的出库保护,不会自动撤销库存位置或移库任务。不要直接删除触发器或覆盖存储过程。 + +完整卸载必须先关闭开关,三条试点通道清空、相关移库任务和移出到 E 区的试点托盘也全部处理完: + +```bat +call mixed-pilot.cmd uninstall "lpc:." "FT546-wms" +``` + +卸载恢复三个原过程的完整逻辑、移除试点触发器和辅助过程,保留 `mixed_pilot` 中的原定义与审计表。未清空时拒绝卸载。卸载后的再次安装需要重新准备版本,避免覆盖已有审计资料。 + +导出观察记录发回分析: + +```bat +call mixed-pilot.cmd status "lpc:." "FT546-wms" > mixed-pilot-status.json +``` + +同时提供对应时间段 WMS/WCS 日志,以及实际入库、前位出库、后位移库出库的任务号和现场结果。仅有安装成功或一段无报错日志不能证明设备完整流程通过。 + +## 已完成的程序验证 + +在 SQL Server 2016 上创建独立小型测试数据库,只使用合成数据和读取到的现场原过程,不连接设备或调用业务 HTTP。已验证默认关闭、普通分配优先、白名单与项目一致性、拒绝第三项目、重复分配、前后位派发顺序、移库目标校验、重复完成回调、开关关闭仍保护、两个连接争抢同一空位、安装漂移拒绝及卸载恢复。 + +还直接执行了现场 `proc_out_auto_allocation`:确认实际生成的移库任务搬的是外侧客居托盘,内侧出库进入等待,移库完成后才放行。这些是数据库行为验证,设备现场往返仍需通过上述小范围业务试点确认。 + +本补丁替换 `proc_allot_space`、`proc_yk_job_add`、`proc_yk_complete` 三个入口并保留原定义,不修改两个出库分配过程。增加三处数据库触发器,涉及 `mydo_job`、`bas_cargo_space`、`bas_stock`。未登记通道仍使用原分配与派发规则;试点启用期间,入库分配增加短时间串行校验,移库增加目标锁检查,争用时可能需要重试。 + +开发人员复测(应在安装前采集原过程;测试只创建独立、随机命名的测试数据库): + +```bat +powershell.exe -NoProfile -NonInteractive -File capture-test-fixture.ps1 -Server "lpc:." -Database "FT546-wms" > fixture.json +powershell.exe -NoProfile -NonInteractive -File test-mixed-pilot.ps1 -Server "lpc:." -FixtureJson fixture.json +``` + +测试库初始数据文件与日志各 8 MB,上限各 64 MB,正常结束或失败后都会尝试清理。测试不恢复生产库,不调用 WMS/WCS 的业务接口;原序列号过程用测试序列替代,合成表保留字段类型和顺序、放宽非空约束,因此不覆盖全部生产约束及设备流程。 diff --git a/scripts/mixed-pilot/capture-test-fixture.ps1 b/scripts/mixed-pilot/capture-test-fixture.ps1 new file mode 100644 index 0000000..33fc6c4 --- /dev/null +++ b/scripts/mixed-pilot/capture-test-fixture.ps1 @@ -0,0 +1,20 @@ +param([Parameter(Mandatory=$true)][string]$Server,[Parameter(Mandatory=$true)][string]$Database) +$ErrorActionPreference='Stop' +[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false) +$builder=[Data.SqlClient.SqlConnectionStringBuilder]::new() +$builder['Data Source']=$Server;$builder['Initial Catalog']=$Database;$builder['Integrated Security']=$true +$builder['Connect Timeout']=8;$builder['Application Name']='MixedPilotFixtureReadOnly' +$cn=[Data.SqlClient.SqlConnection]::new($builder.ConnectionString);$cn.Open() +function Query([string]$sql){ + $c=$cn.CreateCommand();$c.CommandTimeout=10;$c.CommandText='SET NOCOUNT ON;SET LOCK_TIMEOUT 2000;'+$sql + $t=[Data.DataTable]::new();try{$r=$c.ExecuteReader();try{$t.Load($r)}finally{$r.Dispose()}}finally{$c.Dispose()} + foreach($row in $t.Rows){$o=[ordered]@{};foreach($col in $t.Columns){$v=$row[$col.ColumnName];$o[$col.ColumnName]=if($v -is [DBNull]){$null}else{$v}};[pscustomobject]$o} +} +try { + # Definitions and column metadata only: no production inventory/order/task rows are exported. + $fixture=[ordered]@{ + columns=@(Query "SELECT o.name table_name,c.name column_name,TYPE_NAME(c.user_type_id) type_name,c.max_length,c.precision,c.scale,c.is_nullable,c.is_identity FROM sys.objects o JOIN sys.columns c ON c.object_id=o.object_id WHERE o.schema_id=SCHEMA_ID('dbo') AND o.name IN('bas_cargo_space','bas_stock','mydo_job','mydo_job_detail','mydo_job_finish','bas_led','move_msg','out_stockout','out_stockout_detail') ORDER BY o.name,c.column_id") + wms_objects=@(Query "SELECT o.name,m.definition FROM sys.objects o JOIN sys.sql_modules m ON m.object_id=o.object_id WHERE o.schema_id=SCHEMA_ID('dbo') AND o.name IN('cargo_space_group_count','proc_allot_space','proc_yk_job_add','proc_yk_complete','proc_out_auto_allocation','proc_out_hand_allocation')") + } + $fixture|ConvertTo-Json -Depth 6 -Compress +}finally{$cn.Dispose()} diff --git a/scripts/mixed-pilot/control-mixed-pilot.ps1 b/scripts/mixed-pilot/control-mixed-pilot.ps1 new file mode 100644 index 0000000..5859a27 --- /dev/null +++ b/scripts/mixed-pilot/control-mixed-pilot.ps1 @@ -0,0 +1,101 @@ +param( + [Parameter(Mandatory=$true)][string]$Server, + [Parameter(Mandatory=$true)][string]$Database, + [ValidateSet('status','enable','disable','uninstall')][string]$Mode='status', + [switch]$ConfirmPhysicalFit, + [switch]$AllProjectsMayMix, + [switch]$TestDatabase +) +$ErrorActionPreference='Stop' +[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false) +if($TestDatabase -and $Database -notmatch '^FT546WMS_MixedPilot_Test_[a-f0-9]{32}$'){throw 'Invalid test database name'} +if(-not $TestDatabase -and $Database -ine 'FT546-wms'){throw 'Only FT546-wms is supported'} +$builder=[Data.SqlClient.SqlConnectionStringBuilder]::new() +$builder['Data Source']=$Server;$builder['Initial Catalog']=$Database;$builder['Integrated Security']=$true +$builder['Connect Timeout']=8;$builder['Application Name']='FT546MixedPilotControl' +$cn=[Data.SqlClient.SqlConnection]::new($builder.ConnectionString);$cn.Open();$tx=$null +function Cmd([string]$sql){$c=$cn.CreateCommand();$c.CommandText=$sql;$c.CommandTimeout=15;if($tx){$c.Transaction=$tx};$c} +function Exec([string]$sql){$c=Cmd $sql;try{[void]$c.ExecuteNonQuery()}finally{$c.Dispose()}} +function Scalar([string]$sql){$c=Cmd $sql;try{$c.ExecuteScalar()}finally{$c.Dispose()}} +function Query([string]$sql){ + $c=Cmd $sql;$t=[Data.DataTable]::new() + try{$r=$c.ExecuteReader();try{$t.Load($r)}finally{$r.Dispose()}}finally{$c.Dispose()} + foreach($row in $t.Rows){$o=[ordered]@{};foreach($col in $t.Columns){$v=$row[$col.ColumnName];$o[$col.ColumnName]=if($v -is [DBNull]){$null}else{$v}};[pscustomobject]$o} +} +try { + Exec 'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET DEADLOCK_PRIORITY LOW;' + if((Scalar "SELECT OBJECT_ID('mixed_pilot.control','U')") -is [DBNull]){ + if($Mode -eq 'status'){Write-Output '{"installed":false}';return};throw 'Pilot is not installed' + } + if($Mode -eq 'status'){ + $report=[ordered]@{ + utc=[DateTime]::UtcNow.ToString('o') + control=@(Query 'SELECT * FROM mixed_pilot.control') + slots=@(Query 'SELECT l.*,s.cargo_space_id,s.group_coding,s.the_floor,s.status,s.locked,s.tray_code FROM mixed_pilot.lane l LEFT JOIN dbo.bas_cargo_space s ON s.warehouse_id=l.warehouse_id AND s.group_coding_no=l.lane_code ORDER BY l.lane_code,s.group_coding') + admissions=@(Query 'SELECT TOP(100) * FROM mixed_pilot.admission ORDER BY created_at DESC') + waiting=@(Query "SELECT j.job_id,j.tray_code,j.cargo_space_id,j.status,h.created_at FROM mixed_pilot.dispatch_hold h JOIN dbo.mydo_job j ON j.job_id=h.job_id WHERE j.status='8'") + moves=@(Query 'SELECT * FROM mixed_pilot.move_audit WHERE completed_at IS NULL') + } + $report|ConvertTo-Json -Depth 6;return + } + $tx=$cn.BeginTransaction() + Exec "DECLARE @r int; EXEC @r=sys.sp_getapplock @Resource='FT546.mixed_pilot.inbound',@LockMode='Exclusive',@LockOwner='Transaction',@LockTimeout=1000; IF @r<0 THROW 51142,'An inbound allocation is active; retry later',1;" + if($Mode -eq 'disable'){ + Exec 'UPDATE mixed_pilot.control SET enabled=0 WHERE id=1; UPDATE mixed_pilot.lane SET enabled=0;' + $tx.Commit();$tx.Dispose();$tx=$null;Write-Output 'DISABLED: no new mixed admissions; existing mixed-stock dispatch protection retained';return + } + if((Scalar 'SELECT installed FROM mixed_pilot.control WHERE id=1') -ne 1){throw 'Pilot hooks are already uninstalled'} + Exec "IF EXISTS(SELECT 1 FROM mixed_pilot.module_backup WHERE installed_hash<>HASHBYTES('SHA2_256',OBJECT_DEFINITION(OBJECT_ID(object_name))) OR OBJECT_DEFINITION(OBJECT_ID(object_name)) IS NULL) OR EXISTS(SELECT 1 FROM mixed_pilot.installed_module WHERE definition_hash<>HASHBYTES('SHA2_256',OBJECT_DEFINITION(OBJECT_ID(object_name))) OR OBJECT_DEFINITION(OBJECT_ID(object_name)) IS NULL) THROW 51143,'Module drift detected; no changes made',1;" + if($Mode -eq 'enable'){ + if(-not $ConfirmPhysicalFit -or -not $AllProjectsMayMix){throw 'Enable requires -ConfirmPhysicalFit and -AllProjectsMayMix; do not infer these from empty SQL height fields'} + Exec @' +IF (SELECT COUNT(*) FROM mixed_pilot.lane)<>3 OR EXISTS(SELECT 1 FROM mixed_pilot.lane WHERE warehouse_id<>'01' OR lane_code NOT IN('C1203','C0204','C0205')) + THROW 51144,'Pilot whitelist differs from the reviewed three lanes',1; +DECLARE @locked int; +SELECT @locked=COUNT(*) FROM dbo.bas_cargo_space s WITH(UPDLOCK,HOLDLOCK) +JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no; +IF @locked<>6 OR EXISTS(SELECT 1 FROM mixed_pilot.lane l WHERE + (SELECT COUNT(*) FROM dbo.bas_cargo_space s WHERE s.warehouse_id=l.warehouse_id AND s.group_coding_no=l.lane_code AND TRY_CONVERT(int,s.group_coding)=1 AND s.this_cargo_area='C' AND s.status='2' AND s.locked='N' AND NULLIF(s.tray_code,'') IS NOT NULL)<>1 + OR (SELECT COUNT(*) FROM dbo.bas_cargo_space s WHERE s.warehouse_id=l.warehouse_id AND s.group_coding_no=l.lane_code AND TRY_CONVERT(int,s.group_coding)=2 AND s.this_cargo_area='C' AND s.status='1' AND s.locked='N' AND NULLIF(s.tray_code,'') IS NULL)<>1) + THROW 51145,'A pilot lane no longer has an occupied rear and empty unlocked front; refresh candidate review',1; +IF EXISTS(SELECT 1 FROM dbo.mydo_job j WITH(UPDLOCK,HOLDLOCK) JOIN dbo.bas_cargo_space s ON s.cargo_space_id=j.cargo_space_id OR s.cargo_space_id=j.target_cargo_space_id OR s.tray_code=j.tray_code + JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no) + THROW 51146,'Pilot lane has an active task; no switches changed',1; +IF EXISTS(SELECT 1 FROM dbo.bas_cargo_space s JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no + WHERE (TRY_CONVERT(int,s.group_coding)=2 AND EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=s.cargo_space_id)) + OR (TRY_CONVERT(int,s.group_coding)=1 AND ( + (SELECT COUNT(DISTINCT spare4) FROM dbo.bas_stock x WHERE x.cargo_space_id=s.cargo_space_id)<>1 + OR EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=s.cargo_space_id AND (NULLIF(x.spare4,'') IS NULL OR x.spare4='999' OR x.tray_code<>s.tray_code OR x.tray_code IS NULL OR ISNULL(x.out_quantity,0)<>0))))) + THROW 51147,'Pilot inventory/project consistency check failed',1; +UPDATE mixed_pilot.control SET enabled=1,all_projects_may_mix=1 WHERE id=1; +UPDATE mixed_pilot.lane SET enabled=1,physical_fit_confirmed=1; +'@ + $tx.Commit();$tx.Dispose();$tx=$null;Write-Output 'ENABLED: C1203/120703, C0204/020704, C0205/020705; at most three simultaneous guest pallets';return + } + # Full uninstall requires all trial stock/tasks to be drained, including relocated trial pallets. + Exec @' +IF EXISTS(SELECT 1 FROM mixed_pilot.control WHERE enabled=1) + THROW 51148,'Disable admission before uninstalling',1; +IF EXISTS(SELECT 1 FROM dbo.bas_cargo_space s JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no + WHERE s.status<>'1' OR NULLIF(s.tray_code,'') IS NOT NULL + OR EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=s.cargo_space_id) + OR EXISTS(SELECT 1 FROM dbo.mydo_job j WHERE j.cargo_space_id=s.cargo_space_id OR j.target_cargo_space_id=s.cargo_space_id)) + OR EXISTS(SELECT 1 FROM mixed_pilot.move_audit a JOIN dbo.bas_stock s ON s.tray_code=a.tray_code) + OR EXISTS(SELECT 1 FROM mixed_pilot.move_audit a JOIN dbo.mydo_job j ON j.job_id=a.job_id) + OR EXISTS(SELECT 1 FROM mixed_pilot.dispatch_hold h JOIN dbo.mydo_job j ON j.job_id=h.job_id AND j.status='8') + THROW 51149,'Trial lanes or relocated trial pallets are not drained; keep protection installed',1; +'@ + $backups=@(Query 'SELECT * FROM mixed_pilot.module_backup') + foreach($b in $backups){ + Exec (('SET ANSI_NULLS '+$(if($b.ansi_nulls){'ON'}else{'OFF'}))+'; SET QUOTED_IDENTIFIER '+$(if($b.quoted_identifier){'ON'}else{'OFF'})+';') + $original=[regex]::Replace($b.original_definition,'(?i)\bCREATE\s+PROCEDURE','ALTER PROCEDURE') + Exec $original + } + Exec 'DROP TRIGGER dbo.tr_mixed_pilot_job_guard; DROP TRIGGER dbo.tr_mixed_pilot_slot_release; DROP TRIGGER dbo.tr_mixed_pilot_stock_release;' + foreach($name in @('allot_space_with_fallback','original_allot_space','original_yk_job_add','original_yk_complete','check_inbound','try_fallback','check_move','release_ready_jobs')){Exec "DROP PROCEDURE mixed_pilot.[$name]"} + Exec 'DROP VIEW mixed_pilot.blocked_jobs; UPDATE mixed_pilot.control SET installed=0,enabled=0 WHERE id=1;' + $tx.Commit();$tx.Dispose();$tx=$null + Write-Output 'UNINSTALLED: original procedure behavior restored; audit and original definitions retained in mixed_pilot tables' +} catch { + if($tx){try{$tx.Rollback()}catch{};$tx.Dispose();$tx=$null};throw +} finally {$cn.Dispose()} diff --git a/scripts/mixed-pilot/install-mixed-pilot.ps1 b/scripts/mixed-pilot/install-mixed-pilot.ps1 new file mode 100644 index 0000000..58114af --- /dev/null +++ b/scripts/mixed-pilot/install-mixed-pilot.ps1 @@ -0,0 +1,92 @@ +param( + [Parameter(Mandatory=$true)][string]$Server, + [Parameter(Mandatory=$true)][string]$Database, + [switch]$TestDatabase +) +$ErrorActionPreference='Stop' +[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false) +if($TestDatabase -and $Database -notmatch '^FT546WMS_MixedPilot_Test_[a-f0-9]{32}$'){throw 'Invalid isolated test database name'} +if(-not $TestDatabase -and $Database -ine 'FT546-wms'){throw 'Only FT546-wms is supported'} +$builder=[Data.SqlClient.SqlConnectionStringBuilder]::new() +$builder['Data Source']=$Server; $builder['Initial Catalog']=$Database; $builder['Integrated Security']=$true +$builder['Connect Timeout']=8; $builder['Application Name']='FT546MixedPilotInstaller' +$cn=[Data.SqlClient.SqlConnection]::new($builder.ConnectionString);$cn.Open();$tx=$null +function Cmd([string]$sql) { + $c=$cn.CreateCommand();$c.CommandText=$sql;$c.CommandTimeout=15 + if($tx){$c.Transaction=$tx};return $c +} +function Exec([string]$sql) {$c=Cmd $sql;try{[void]$c.ExecuteNonQuery()}finally{$c.Dispose()}} +function Scalar([string]$sql) {$c=Cmd $sql;try{$c.ExecuteScalar()}finally{$c.Dispose()}} +function Hash([string]$s) { + $sha=[Security.Cryptography.SHA256]::Create() + try{([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::Unicode.GetBytes($s)))).Replace('-','')}finally{$sha.Dispose()} +} +function RenameProc([string]$sql,[string]$name) { + $regex=[regex]::new('(?i)\bCREATE\s+PROCEDURE\s+(?:\[?dbo\]?\.)?\[?proc_[a-z_]+\]?') + if($regex.Matches($sql).Count -ne 1){throw 'Unexpected procedure header'} + $regex.Replace($sql,('CREATE PROCEDURE [mixed_pilot].['+$name+']'),1) +} +try { + Exec 'SET LOCK_TIMEOUT 2000; SET DEADLOCK_PRIORITY LOW;' + $tx=$cn.BeginTransaction() + Exec "DECLARE @r int; EXEC @r=sys.sp_getapplock @Resource='FT546.mixed_pilot.install',@LockMode='Exclusive',@LockOwner='Transaction',@LockTimeout=0; IF @r<0 THROW 51140,'Another pilot installation is in progress',1;" + if((Scalar "SELECT OBJECT_ID('mixed_pilot.control','U')") -isnot [DBNull]){ + if((Scalar 'SELECT installed FROM mixed_pilot.control WHERE id=1') -ne 1){throw 'Previously uninstalled package: audit data retained; prepare a new reviewed installation rather than overwriting it'} + if((Scalar 'SELECT version FROM mixed_pilot.control WHERE id=1') -ne '20260910-1'){throw 'Unsupported existing mixed-pilot version'} + if((Scalar "SELECT COUNT(*) FROM mixed_pilot.module_backup WHERE installed_hash<>HASHBYTES('SHA2_256',OBJECT_DEFINITION(OBJECT_ID(object_name))) OR OBJECT_DEFINITION(OBJECT_ID(object_name)) IS NULL") -ne 0){throw 'Installed procedure drift detected; refusing overwrite'} + if((Scalar "SELECT OBJECT_ID('mixed_pilot.installed_module','U')") -is [DBNull]){throw 'Missing installed-module manifest'} + if((Scalar "SELECT COUNT(*) FROM mixed_pilot.installed_module WHERE definition_hash<>HASHBYTES('SHA2_256',OBJECT_DEFINITION(OBJECT_ID(object_name))) OR OBJECT_DEFINITION(OBJECT_ID(object_name)) IS NULL") -ne 0){throw 'Pilot helper/trigger drift detected; refusing to report success'} + foreach($table in @('lane','admission','move_audit','dispatch_hold','module_backup')){ + if((Scalar "SELECT OBJECT_ID('mixed_pilot.$table','U')") -is [DBNull]){throw "Missing pilot table: $table"} + } + $tx.Commit();$tx.Dispose();$tx=$null;Write-Output 'ALREADY_INSTALLED: switches and data preserved';return + } + $expected=[ordered]@{ + proc_allot_space='DFF37262FD49271E9E6E657F01697731F1FF73DCD6DECA1F340107D560E26850' + proc_yk_job_add='72708181352148DAF9490DD59FCBD0E3AC7A8C849EE8FC4AFCBFA0B1AF79AB5B' + proc_yk_complete='C80CD445262BB483E7A7BDCF7DB97A44AD07A09D491869A60C094C5037621131' + } + $original=@{} + foreach($name in $expected.Keys){ + $definition=Scalar "SELECT OBJECT_DEFINITION(OBJECT_ID('dbo.$name'))" + if($definition -is [DBNull] -or (Hash $definition) -ne $expected[$name]){throw "Site procedure differs from reviewed baseline: $name. Nothing installed."} + $original[$name]=$definition + } + # No stock/tasks/slot metadata are changed by installation. Admission remains OFF. + Exec "IF SCHEMA_ID('mixed_pilot') IS NOT NULL THROW 51141,'Unexpected pre-existing mixed_pilot schema',1;" + Exec 'CREATE SCHEMA mixed_pilot AUTHORIZATION dbo' + foreach($batch in ([IO.File]::ReadAllText("$PSScriptRoot/objects.sql") -split '(?im)^\s*GO\s*\r?$')){if($batch.Trim()){Exec $batch}} + foreach($name in $expected.Keys){ + $c=Cmd "INSERT mixed_pilot.module_backup(object_name,original_definition,original_hash,ansi_nulls,quoted_identifier) SELECT @name,definition,HASHBYTES('SHA2_256',definition),uses_ansi_nulls,uses_quoted_identifier FROM sys.sql_modules WHERE object_id=OBJECT_ID(@name)" + [void]$c.Parameters.AddWithValue('@name',"dbo.$name");try{[void]$c.ExecuteNonQuery()}finally{$c.Dispose()} + $ansi=Scalar "SELECT ansi_nulls FROM mixed_pilot.module_backup WHERE object_name='dbo.$name'" + $quoted=Scalar "SELECT quoted_identifier FROM mixed_pilot.module_backup WHERE object_name='dbo.$name'" + Exec (('SET ANSI_NULLS '+$(if($ansi){'ON'}else{'OFF'}))+'; SET QUOTED_IDENTIFIER '+$(if($quoted){'ON'}else{'OFF'})+';') + Exec (RenameProc $original[$name] ('original_'+$name.Substring(5))) + } + Exec 'SET ANSI_NULLS ON; SET QUOTED_IDENTIFIER ON;' + $inbound=RenameProc $original['proc_allot_space'] 'allot_space_with_fallback' + $noSlot=[regex]::new("(?is)set\s+@v_result\s*=\s*2\s+set\s+@v_resultmsg\s*=\s*'[^']+'\s+RETURN") + if($noSlot.Matches($inbound).Count -ne 2){throw 'Expected two Legacy no-position exits'} + $inbound=$noSlot.Replace($inbound,[Text.RegularExpressions.MatchEvaluator]{param($match) + 'EXEC mixed_pilot.try_fallback @v_job_id,@v_cargo_space_id OUTPUT;'+"`r`n"+ + 'IF @v_cargo_space_id IS NULL BEGIN '+$match.Value+' END;'+"`r`n"+ + 'GOTO mixed_pilot_destination;' + }) + $destination=[regex]::new('(?i)SELECT\s+@v_job_id\s*=\s*job_id\s+from\s+mydo_job\s+where\s+tray_code\s*=\s*@v_tray_code') + if($destination.Matches($inbound).Count -ne 1){throw 'Expected one common Legacy destination block'} + $inbound=$destination.Replace($inbound,@' +mixed_pilot_destination: +EXEC mixed_pilot.check_inbound @v_job_id,@v_cargo_space_id; +SELECT @v_job_id = job_id from mydo_job where tray_code = @v_tray_code +'@) + Exec $inbound + foreach($batch in ([IO.File]::ReadAllText("$PSScriptRoot/wrappers.sql") -split '(?im)^\s*GO\s*\r?$')){if($batch.Trim()){Exec $batch}} + Exec "UPDATE mixed_pilot.module_backup SET installed_hash=HASHBYTES('SHA2_256',OBJECT_DEFINITION(OBJECT_ID(object_name)))" + Exec "INSERT mixed_pilot.installed_module(object_name,definition_hash) SELECT QUOTENAME(OBJECT_SCHEMA_NAME(object_id))+'.'+QUOTENAME(OBJECT_NAME(object_id)),HASHBYTES('SHA2_256',definition) FROM sys.sql_modules WHERE OBJECT_SCHEMA_NAME(object_id)='mixed_pilot' OR OBJECT_NAME(object_id) IN ('tr_mixed_pilot_job_guard','tr_mixed_pilot_slot_release','tr_mixed_pilot_stock_release')" + $tx.Commit();$tx.Dispose();$tx=$null + Write-Output 'INSTALL_OK: pilot OFF; three C lanes configured but not enabled; Legacy definitions backed up in mixed_pilot.module_backup' +} catch { + if($tx){try{$tx.Rollback()}catch{};$tx.Dispose();$tx=$null} + throw +} finally {$cn.Dispose()} diff --git a/scripts/mixed-pilot/mixed-pilot.cmd b/scripts/mixed-pilot/mixed-pilot.cmd new file mode 100644 index 0000000..1c79b95 --- /dev/null +++ b/scripts/mixed-pilot/mixed-pilot.cmd @@ -0,0 +1,18 @@ +@echo off +setlocal +if "%~3"=="" goto usage +if /I "%~1"=="install" ( + powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0install-mixed-pilot.ps1" -Server "%~2" -Database "%~3" + exit /b +) +if /I "%~1"=="enable" ( + if /I not "%~4"=="CONFIRM-FIT-ALL-PROJECTS" goto usage + powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0control-mixed-pilot.ps1" -Server "%~2" -Database "%~3" -Mode enable -ConfirmPhysicalFit -AllProjectsMayMix + exit /b +) +powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0control-mixed-pilot.ps1" -Server "%~2" -Database "%~3" -Mode "%~1" +exit /b +:usage +echo Usage: mixed-pilot.cmd install^|status^|disable^|uninstall "lpc:." "FT546-wms" +echo Enable: mixed-pilot.cmd enable "lpc:." "FT546-wms" CONFIRM-FIT-ALL-PROJECTS +exit /b 2 diff --git a/scripts/mixed-pilot/objects.sql b/scripts/mixed-pilot/objects.sql new file mode 100644 index 0000000..d8bc1fa --- /dev/null +++ b/scripts/mixed-pilot/objects.sql @@ -0,0 +1,256 @@ +CREATE TABLE mixed_pilot.control ( + id int NOT NULL PRIMARY KEY CHECK(id=1), + version varchar(30) NOT NULL, + installed bit NOT NULL DEFAULT 1, + enabled bit NOT NULL DEFAULT 0, + all_projects_may_mix bit NOT NULL DEFAULT 0, + min_free_e_slots int NOT NULL DEFAULT 2 CHECK(min_free_e_slots>=2) +); +INSERT mixed_pilot.control(id,version,enabled,all_projects_may_mix) VALUES(1,'20260910-1',0,0); +CREATE TABLE mixed_pilot.lane ( + warehouse_id varchar(80) NOT NULL, + lane_code varchar(80) NOT NULL, + enabled bit NOT NULL DEFAULT 0, + physical_fit_confirmed bit NOT NULL DEFAULT 0, + protected bit NOT NULL DEFAULT 0, + PRIMARY KEY(warehouse_id,lane_code) +); +INSERT mixed_pilot.lane(warehouse_id,lane_code) VALUES('01','C1203'),('01','C0204'),('01','C0205'); +CREATE TABLE mixed_pilot.module_backup ( + object_name sysname NOT NULL PRIMARY KEY, + original_definition nvarchar(max) NOT NULL, + original_hash varbinary(32) NOT NULL, + installed_hash varbinary(32) NULL, + ansi_nulls bit NOT NULL, + quoted_identifier bit NOT NULL +); +CREATE TABLE mixed_pilot.installed_module ( + object_name nvarchar(260) NOT NULL PRIMARY KEY, + definition_hash varbinary(32) NOT NULL +); +CREATE TABLE mixed_pilot.admission ( + job_id varchar(255) NOT NULL PRIMARY KEY, + lane_code varchar(80) NOT NULL, + cargo_space_id varchar(80) NOT NULL, + tray_code varchar(255) NOT NULL, + resident_project varchar(255) NOT NULL, + guest_project varchar(255) NOT NULL, + created_at datetime2 NOT NULL DEFAULT SYSUTCDATETIME() +); +CREATE TABLE mixed_pilot.move_audit ( + job_id varchar(255) NOT NULL PRIMARY KEY, + source_slot varchar(255) NOT NULL, + target_slot varchar(255) NOT NULL, + tray_code varchar(255) NOT NULL, + completed_at datetime2 NULL +); +CREATE TABLE mixed_pilot.dispatch_hold ( + job_id varchar(255) NOT NULL PRIMARY KEY, + created_at datetime2 NOT NULL DEFAULT SYSUTCDATETIME(), + released_at datetime2 NULL +); +GO +-- A source position is inaccessible while ANY outer position has cargo or a live task. +CREATE VIEW mixed_pilot.blocked_jobs AS +SELECT j.job_id +FROM dbo.mydo_job j +JOIN dbo.bas_cargo_space s ON s.cargo_space_id=j.cargo_space_id +JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no AND l.protected=1 +WHERE j.in_or_out>=20 AND EXISTS ( + SELECT 1 FROM dbo.bas_cargo_space b + WHERE b.warehouse_id=s.warehouse_id AND b.group_coding_no=s.group_coding_no + AND TRY_CONVERT(int,b.group_coding)>TRY_CONVERT(int,s.group_coding) + AND (ISNULL(b.status,'')<>'1' OR ISNULL(b.locked,'')<>'N' OR NULLIF(b.tray_code,'') IS NOT NULL + OR EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=b.cargo_space_id) + OR EXISTS(SELECT 1 FROM dbo.mydo_job x WHERE x.cargo_space_id=b.cargo_space_id OR x.target_cargo_space_id=b.cargo_space_id)) +); +GO +CREATE PROCEDURE mixed_pilot.release_ready_jobs AS +BEGIN + SET NOCOUNT ON; + -- Clearing only a slot flag is insufficient: inventory and front tasks must also be gone. + UPDATE j SET status='1' + FROM dbo.mydo_job j JOIN mixed_pilot.dispatch_hold h ON h.job_id=j.job_id AND h.released_at IS NULL + JOIN dbo.bas_cargo_space s ON s.cargo_space_id=j.cargo_space_id + WHERE j.status='8' AND s.status='4' AND s.tray_code=j.tray_code + AND EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=s.cargo_space_id AND x.tray_code=j.tray_code) + AND NOT EXISTS(SELECT 1 FROM mixed_pilot.blocked_jobs b WHERE b.job_id=j.job_id); + UPDATE h SET released_at=SYSUTCDATETIME() + FROM mixed_pilot.dispatch_hold h + WHERE h.released_at IS NULL AND NOT EXISTS(SELECT 1 FROM dbo.mydo_job j WHERE j.job_id=h.job_id AND j.status='8'); +END; +GO +CREATE TRIGGER dbo.tr_mixed_pilot_job_guard ON dbo.mydo_job AFTER INSERT,UPDATE,DELETE AS +BEGIN + SET NOCOUNT ON; + IF TRIGGER_NESTLEVEL(OBJECT_ID('dbo.tr_mixed_pilot_job_guard'))>1 RETURN; + IF NOT EXISTS(SELECT 1 FROM mixed_pilot.lane WHERE enabled=1 OR protected=1) RETURN; + -- Coordinate with inbound reservation before checking whether a lane has just become mixed. + DECLARE @locked int; + SELECT @locked=COUNT(*) FROM dbo.bas_cargo_space s WITH(UPDLOCK,HOLDLOCK) + JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no + WHERE (l.enabled=1 OR l.protected=1) AND EXISTS( + SELECT 1 FROM inserted i JOIN dbo.bas_cargo_space origin ON origin.cargo_space_id=i.cargo_space_id + WHERE origin.warehouse_id=s.warehouse_id AND origin.group_coding_no=s.group_coding_no AND i.in_or_out>=20); + IF EXISTS(SELECT 1 FROM inserted i JOIN mixed_pilot.blocked_jobs blocked ON blocked.job_id=i.job_id + JOIN dbo.bas_cargo_space s ON s.cargo_space_id=i.cargo_space_id + JOIN dbo.bas_cargo_space front ON front.warehouse_id=s.warehouse_id AND front.group_coding_no=s.group_coding_no + AND TRY_CONVERT(int,front.group_coding)>TRY_CONVERT(int,s.group_coding) + WHERE i.status='1' AND NOT EXISTS(SELECT 1 FROM dbo.mydo_job j WITH(UPDLOCK,HOLDLOCK) + WHERE j.cargo_space_id=front.cargo_space_id AND j.tray_code=front.tray_code AND j.in_or_out>=20 AND j.status IN('1','3'))) + THROW 51126,N'外侧尚未安排腾挪,请使用自动分配出库;外侧正在入库时请稍后重试。',1; + INSERT mixed_pilot.dispatch_hold(job_id) + SELECT DISTINCT i.job_id FROM inserted i JOIN mixed_pilot.blocked_jobs b ON b.job_id=i.job_id + WHERE i.status='1' AND NOT EXISTS(SELECT 1 FROM mixed_pilot.dispatch_hold h WITH(UPDLOCK,HOLDLOCK) WHERE h.job_id=i.job_id); + UPDATE j SET status='8' + FROM dbo.mydo_job j JOIN inserted i ON i.job_id=j.job_id + JOIN mixed_pilot.blocked_jobs b ON b.job_id=j.job_id + WHERE j.status='1'; + EXEC mixed_pilot.release_ready_jobs; +END; +GO +CREATE TRIGGER dbo.tr_mixed_pilot_slot_release ON dbo.bas_cargo_space AFTER UPDATE AS +BEGIN + SET NOCOUNT ON; + IF NOT EXISTS(SELECT 1 FROM inserted i JOIN mixed_pilot.lane l ON l.warehouse_id=i.warehouse_id AND l.lane_code=i.group_coding_no AND l.protected=1) RETURN; + EXEC mixed_pilot.release_ready_jobs; +END; +GO +CREATE TRIGGER dbo.tr_mixed_pilot_stock_release ON dbo.bas_stock AFTER UPDATE,DELETE AS +BEGIN + SET NOCOUNT ON; + IF NOT EXISTS(SELECT 1 FROM deleted d JOIN dbo.bas_cargo_space s ON s.cargo_space_id=d.cargo_space_id + JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no AND l.protected=1) RETURN; + EXEC mixed_pilot.release_ready_jobs; +END; +GO +CREATE PROCEDURE mixed_pilot.check_inbound + @job_id varchar(255),@slot varchar(80) +AS +BEGIN + SET NOCOUNT ON; + IF @@TRANCOUNT=0 OR ISNULL(APPLOCK_MODE('public','FT546.mixed_pilot.inbound','Transaction'),'NoLock')<>'Exclusive' + THROW 51129,N'试点入库校验必须由受保护的分配入口调用。',1; + DECLARE @lane varchar(80),@warehouse varchar(80),@depth int,@project varchar(255),@tray varchar(255),@resident varchar(255),@rear varchar(80),@floor int; + SELECT @lane=s.group_coding_no,@warehouse=s.warehouse_id,@depth=TRY_CONVERT(int,s.group_coding),@floor=s.the_floor + FROM dbo.bas_cargo_space s JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no + WHERE s.cargo_space_id=@slot AND (l.enabled=1 OR l.protected=1); + IF @lane IS NULL RETURN; + SELECT @project=line_id,@tray=tray_code FROM dbo.mydo_job WHERE job_id=@job_id AND warehouse_id=@warehouse AND in_or_out<20; + IF @project IS NULL OR @project IN ('','999') OR @tray IS NULL + THROW 51101,N'试点货位的任务项目或仓库不明确,请核对入库任务。',1; + IF (SELECT COUNT(*) FROM dbo.bas_cargo_space WHERE warehouse_id=@warehouse AND group_coding_no=@lane)<>2 + OR (SELECT COUNT(*) FROM dbo.bas_cargo_space WHERE warehouse_id=@warehouse AND group_coding_no=@lane AND TRY_CONVERT(int,group_coding)=1)<>1 + OR (SELECT COUNT(*) FROM dbo.bas_cargo_space WHERE warehouse_id=@warehouse AND group_coding_no=@lane AND TRY_CONVERT(int,group_coding)=2)<>1 + OR EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE warehouse_id=@warehouse AND group_coding_no=@lane AND (this_cargo_area<>'C' OR TRY_CONVERT(int,group_coding) NOT IN(1,2) OR TRY_CONVERT(int,group_coding) IS NULL)) + THROW 51102,N'试点仅支持已指定的双深C区通道。',1; + IF NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE cargo_space_id=@slot AND status='1' AND locked='N' AND NULLIF(tray_code,'') IS NULL) + OR EXISTS(SELECT 1 FROM dbo.bas_stock WHERE cargo_space_id=@slot) + OR EXISTS(SELECT 1 FROM dbo.mydo_job j JOIN dbo.bas_cargo_space s ON s.cargo_space_id=j.cargo_space_id OR s.cargo_space_id=j.target_cargo_space_id + WHERE s.warehouse_id=@warehouse AND s.group_coding_no=@lane AND j.job_id<>@job_id) + THROW 51103,N'试点通道已有库存或在途任务,请稍后重试。',1; + IF EXISTS(SELECT 1 FROM dbo.bas_cargo_space s WHERE s.warehouse_id=@warehouse AND s.group_coding_no=@lane + AND (s.locked<>'N' OR s.status NOT IN('1','2'))) + THROW 51104,N'试点通道正在作业或被锁定,请稍后重试。',1; + IF @depth=1 AND EXISTS(SELECT 1 FROM dbo.bas_cargo_space s WHERE s.warehouse_id=@warehouse AND s.group_coding_no=@lane AND TRY_CONVERT(int,s.group_coding)=2 + AND (s.status<>'1' OR NULLIF(s.tray_code,'') IS NOT NULL OR EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=s.cargo_space_id))) + THROW 51105,N'外侧尚有托盘,不能向通道内侧补货。',1; + SELECT @rear=cargo_space_id FROM dbo.bas_cargo_space WHERE warehouse_id=@warehouse AND group_coding_no=@lane AND TRY_CONVERT(int,group_coding)=1; + IF @depth<>2 OR NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE cargo_space_id=@rear AND status='2') RETURN; + IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job_detail WHERE job_id=@job_id) + OR EXISTS(SELECT 1 FROM dbo.mydo_job_detail WHERE job_id=@job_id AND (NULLIF(spare4,'') IS NULL OR spare4<>@project)) + OR (SELECT COUNT(DISTINCT spare4) FROM dbo.bas_stock WHERE cargo_space_id=@rear)<>1 + OR EXISTS(SELECT 1 FROM dbo.bas_stock x JOIN dbo.bas_cargo_space s ON s.cargo_space_id=x.cargo_space_id + WHERE x.cargo_space_id=@rear AND (NULLIF(x.spare4,'') IS NULL OR x.tray_code<>s.tray_code OR x.tray_code IS NULL OR ISNULL(x.out_quantity,0)<>0)) + THROW 51106,N'托盘项目明细不一致或库存已分配,不能参与混放。',1; + SELECT TOP(1) @resident=spare4 FROM dbo.bas_stock WHERE cargo_space_id=@rear; + IF @resident=@project RETURN; + IF @resident='999' OR NOT EXISTS(SELECT 1 FROM mixed_pilot.control WHERE id=1 AND enabled=1 AND all_projects_may_mix=1) + OR NOT EXISTS(SELECT 1 FROM mixed_pilot.lane WHERE warehouse_id=@warehouse AND lane_code=@lane AND enabled=1 AND physical_fit_confirmed=1) + THROW 51107,N'该通道未开放混放或尚未确认托盘适配。',1; + IF (SELECT COUNT(*) FROM dbo.bas_cargo_space e WHERE e.warehouse_id=@warehouse AND e.the_floor=@floor AND e.this_cargo_area='E' + AND e.status='1' AND e.locked='N' AND NULLIF(e.tray_code,'') IS NULL + AND NOT EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=e.cargo_space_id) + AND NOT EXISTS(SELECT 1 FROM dbo.mydo_job x WHERE x.cargo_space_id=e.cargo_space_id OR x.target_cargo_space_id=e.cargo_space_id) + AND NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space b WHERE b.warehouse_id=e.warehouse_id AND b.group_coding_no=e.group_coding_no + AND TRY_CONVERT(int,b.group_coding)>TRY_CONVERT(int,e.group_coding) + AND (b.status<>'1' OR b.locked<>'N' OR NULLIF(b.tray_code,'') IS NOT NULL + OR EXISTS(SELECT 1 FROM dbo.bas_stock x WHERE x.cargo_space_id=b.cargo_space_id) + OR EXISTS(SELECT 1 FROM dbo.mydo_job x WHERE x.cargo_space_id=b.cargo_space_id OR x.target_cargo_space_id=b.cargo_space_id)))) + <(SELECT min_free_e_slots FROM mixed_pilot.control WHERE id=1) + THROW 51108,N'同层可用移库空间不足,暂停新增混放。',1; + UPDATE mixed_pilot.lane SET protected=1 WHERE warehouse_id=@warehouse AND lane_code=@lane; + IF EXISTS(SELECT 1 FROM mixed_pilot.admission WHERE job_id=@job_id AND (cargo_space_id<>@slot OR tray_code<>@tray OR resident_project<>@resident OR guest_project<>@project)) + THROW 51127,N'该任务已有不同的混放记录,请核对后重新创建任务。',1; + IF NOT EXISTS(SELECT 1 FROM mixed_pilot.admission WHERE job_id=@job_id) + INSERT mixed_pilot.admission(job_id,lane_code,cargo_space_id,tray_code,resident_project,guest_project) + VALUES(@job_id,@lane,@slot,@tray,@resident,@project); +END; +GO +CREATE PROCEDURE mixed_pilot.try_fallback + @job_id varchar(255),@slot varchar(80) OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SET @slot=NULL; + IF NOT EXISTS(SELECT 1 FROM mixed_pilot.control WHERE id=1 AND enabled=1 AND all_projects_may_mix=1) RETURN; + DECLARE @candidate varchar(80),@last_error nvarchar(2048); + DECLARE candidates CURSOR LOCAL FAST_FORWARD FOR + SELECT s.cargo_space_id FROM dbo.bas_cargo_space s + JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no + JOIN dbo.mydo_job j ON j.job_id=@job_id AND j.warehouse_id=s.warehouse_id + WHERE l.enabled=1 AND l.physical_fit_confirmed=1 AND s.this_cargo_area='C' AND TRY_CONVERT(int,s.group_coding)=2 + AND s.status='1' AND s.locked='N' AND NULLIF(s.tray_code,'') IS NULL + AND EXISTS(SELECT 1 FROM dbo.bas_cargo_space r WHERE r.warehouse_id=s.warehouse_id AND r.group_coding_no=s.group_coding_no AND TRY_CONVERT(int,r.group_coding)=1 AND r.status='2') + AND NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space busy WHERE busy.warehouse_id=s.warehouse_id AND busy.the_floor=s.the_floor AND busy.status='3') + ORDER BY s.the_floor,s.group_coding_no; + OPEN candidates; FETCH NEXT FROM candidates INTO @candidate; + WHILE @@FETCH_STATUS=0 + BEGIN + BEGIN TRY + EXEC mixed_pilot.check_inbound @job_id,@candidate; + SET @slot=@candidate; + END TRY + BEGIN CATCH + IF ERROR_NUMBER() NOT BETWEEN 51101 AND 51108 BEGIN CLOSE candidates; DEALLOCATE candidates; THROW; END; + SET @last_error=ERROR_MESSAGE(); + END CATCH; + IF @slot IS NOT NULL BREAK; + FETCH NEXT FROM candidates INTO @candidate; + END; + CLOSE candidates; DEALLOCATE candidates; + IF @slot IS NULL AND @last_error IS NOT NULL THROW 51128,@last_error,1; +END; +GO +CREATE PROCEDURE mixed_pilot.check_move + @source varchar(255),@target varchar(255),@tray varchar(255) +AS +BEGIN + SET NOCOUNT ON; + DECLARE @floor int,@warehouse varchar(80),@src_lane varchar(80),@src_depth int,@dst_lane varchar(80),@dst_depth int,@dummy int; + -- Lock both positions before the Legacy routine changes source/target or creates a task. + SELECT @dummy=COUNT(*) FROM dbo.bas_cargo_space WITH(UPDLOCK,HOLDLOCK) WHERE cargo_space_id IN(@source,@target); + SELECT @floor=the_floor,@warehouse=warehouse_id,@src_lane=group_coding_no,@src_depth=TRY_CONVERT(int,group_coding) + FROM dbo.bas_cargo_space WHERE cargo_space_id=@source AND status='2' AND locked='N' AND tray_code=@tray; + IF @floor IS NULL OR @source=@target OR NULLIF(@tray,'') IS NULL + THROW 51111,N'移库起点、托盘或货位状态不一致,未创建移库任务。',1; + IF EXISTS(SELECT 1 FROM dbo.mydo_job WITH(UPDLOCK,HOLDLOCK) WHERE tray_code=@tray OR cargo_space_id IN(@source,@target) OR target_cargo_space_id IN(@source,@target)) + THROW 51112,N'移库起终点已有任务,未重复创建移库。',1; + IF NOT EXISTS(SELECT 1 FROM dbo.bas_stock WITH(UPDLOCK,HOLDLOCK) WHERE cargo_space_id=@source AND tray_code=@tray) + OR EXISTS(SELECT 1 FROM dbo.bas_stock WITH(UPDLOCK,HOLDLOCK) WHERE cargo_space_id=@source AND (tray_code<>@tray OR tray_code IS NULL OR ISNULL(out_quantity,0)<>0)) + THROW 51113,N'移库源库存不一致或已分配,未创建移库任务。',1; + SELECT @dst_lane=group_coding_no,@dst_depth=TRY_CONVERT(int,group_coding) + FROM dbo.bas_cargo_space WHERE cargo_space_id=@target AND warehouse_id=@warehouse AND the_floor=@floor + AND this_cargo_area='E' AND status='1' AND locked='N' AND NULLIF(tray_code,'') IS NULL; + IF @dst_lane IS NULL OR @dst_depth IS NULL OR EXISTS(SELECT 1 FROM dbo.bas_stock WITH(UPDLOCK,HOLDLOCK) WHERE cargo_space_id=@target) + THROW 51114,N'同层移库目标不可用,未创建移库任务。',1; + SELECT @dummy=COUNT(*) FROM dbo.bas_cargo_space WITH(UPDLOCK,HOLDLOCK) + WHERE warehouse_id=@warehouse AND group_coding_no IN(@src_lane,@dst_lane); + IF EXISTS(SELECT 1 FROM dbo.bas_cargo_space b WITH(UPDLOCK,HOLDLOCK) WHERE b.warehouse_id=@warehouse + AND ((b.group_coding_no=@src_lane AND TRY_CONVERT(int,b.group_coding)>@src_depth) + OR (b.group_coding_no=@dst_lane AND TRY_CONVERT(int,b.group_coding)>@dst_depth)) + AND (b.status<>'1' OR b.locked<>'N' OR NULLIF(b.tray_code,'') IS NOT NULL + OR EXISTS(SELECT 1 FROM dbo.bas_stock x WITH(UPDLOCK,HOLDLOCK) WHERE x.cargo_space_id=b.cargo_space_id) + OR EXISTS(SELECT 1 FROM dbo.mydo_job x WITH(UPDLOCK,HOLDLOCK) WHERE x.cargo_space_id=b.cargo_space_id OR x.target_cargo_space_id=b.cargo_space_id))) + THROW 51115,N'移库路径外侧被占用,未创建移库任务。',1; +END; diff --git a/scripts/mixed-pilot/test-mixed-pilot.ps1 b/scripts/mixed-pilot/test-mixed-pilot.ps1 new file mode 100644 index 0000000..34be66a --- /dev/null +++ b/scripts/mixed-pilot/test-mixed-pilot.ps1 @@ -0,0 +1,121 @@ +param( + [string]$Server = 'lpc:.', + [Parameter(Mandatory=$true)][string]$FixtureJson, + [switch]$Baseline +) +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) +# Synthetic rows only. Never run fixtures in a production database. +$testDb = 'FT546WMS_MixedPilot_Test_' + [Guid]::NewGuid().ToString('N') +$cn = [Data.SqlClient.SqlConnection]::new("Server=$Server;Database=master;Integrated Security=True;Connect Timeout=8;Application Name=FT546MixedPilotTests") +$cn.Open() +function Exec([string]$sql) { + $c=$cn.CreateCommand(); $c.CommandTimeout=15 + $c.CommandText=$sql + try { [void]$c.ExecuteNonQuery() } finally {$c.Dispose()} +} +function Scalar([string]$sql) { + $c=$cn.CreateCommand(); $c.CommandTimeout=15; $c.CommandText=$sql + try { $c.ExecuteScalar() } finally {$c.Dispose()} +} +function BatchFile([string]$file) { + foreach($batch in ([IO.File]::ReadAllText($file) -split '(?im)^\s*GO\s*\r?$')) { + if($batch.Trim()) { Exec $batch } + } +} +function Assert([bool]$ok,[string]$name) { if(-not $ok){throw "FAIL: $name"}; Write-Output "PASS: $name" } +$created=$false +try { + Exec "CREATE DATABASE [$testDb] ON PRIMARY (NAME=N'$testDb', FILENAME=N'$(Scalar "SELECT CONVERT(nvarchar(4000),SERVERPROPERTY('InstanceDefaultDataPath'))")$testDb.mdf',SIZE=8MB,MAXSIZE=64MB,FILEGROWTH=8MB) LOG ON (NAME=N'${testDb}_log', FILENAME=N'$(Scalar "SELECT CONVERT(nvarchar(4000),SERVERPROPERTY('InstanceDefaultLogPath'))")${testDb}_log.ldf',SIZE=8MB,MAXSIZE=64MB,FILEGROWTH=8MB)" + $created=$true; $cn.ChangeDatabase($testDb) + Exec 'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET DEADLOCK_PRIORITY LOW;' + $fixture=Get-Content -LiteralPath $FixtureJson -Raw -Encoding UTF8 | ConvertFrom-Json + $allowed=@('bas_cargo_space','bas_stock','mydo_job','mydo_job_detail','mydo_job_finish','bas_led','move_msg','out_stockout','out_stockout_detail') + foreach($table in ($fixture.columns|Where-Object table_name -in $allowed|Group-Object table_name)) { + $defs=@(foreach($c in $table.Group){ + $type=$c.type_name + if($type -in @('varchar','char','nvarchar','nchar','varbinary','binary')) { + $size=if($c.max_length -eq -1){'max'}elseif($type.StartsWith('n')){[string]($c.max_length/2)}else{[string]$c.max_length} + $type+="($size)" + } elseif($type -in @('decimal','numeric')) {$type+="($($c.precision),$($c.scale))"} + # Nullability is relaxed for concise synthetic fixtures, types/order match site. + "[$($c.column_name)] $type NULL" + }) + Exec "CREATE TABLE dbo.[$($table.Name)] ($($defs -join ','))" + } + foreach($p in $fixture.wms_objects|Where-Object name -in @('cargo_space_group_count','proc_allot_space','proc_yk_job_add','proc_yk_complete','proc_out_auto_allocation','proc_out_hand_allocation')) {Exec $p.definition} + Exec "CREATE TABLE dbo.bas_roadway(roadway_id varchar(80),out_locked varchar(10),out_locked2 varchar(10))" + Exec "CREATE PROCEDURE dbo.util_serial_job @kind varchar(10),@id varchar(255) OUTPUT AS SET @id=CONVERT(varchar(36),NEWID())" + Exec "CREATE SEQUENCE dbo.test_task_seq AS int START WITH 900001" + Exec "CREATE PROCEDURE dbo.util_serial_task @kind varchar(10),@id int OUTPUT AS SET @id=NEXT VALUE FOR dbo.test_task_seq" + if(-not $Baseline){ & "$PSScriptRoot/install-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase } + BatchFile "$PSScriptRoot/tests/seed.sql" + if($Baseline){ + $result=Scalar "DECLARE @r int,@m varchar(255); EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; SELECT @r" + Assert ($result -eq 10) 'new project gets designated front slot when Legacy has no position' + } else { + & "$PSScriptRoot/control-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase -Mode enable -ConfirmPhysicalFit -AllProjectsMayMix + Assert ((Scalar 'SELECT COUNT(*) FROM mixed_pilot.lane WHERE enabled=1') -eq 3) 'enable command opens exactly three whole channels' + & "$PSScriptRoot/control-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase -Mode disable + $refused=$false + try { & "$PSScriptRoot/control-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase -Mode uninstall } catch {$refused=$_.Exception.Message -like '*not drained*'} + Assert $refused 'uninstall refuses occupied trial channels' + BatchFile "$PSScriptRoot/tests/behavior.sql" + Write-Output 'PASS: mixed-pilot SQL behavior suite' + Exec 'DELETE dbo.mydo_job; DELETE dbo.mydo_job_detail; DELETE dbo.bas_stock; DELETE dbo.bas_cargo_space; DELETE mixed_pilot.dispatch_hold; DELETE mixed_pilot.move_audit; DELETE mixed_pilot.admission;' + BatchFile "$PSScriptRoot/tests/seed.sql" + BatchFile "$PSScriptRoot/tests/legacy-outbound.sql" + Write-Output 'PASS: actual site Legacy outbound procedure generates blocker relocation and holds rear outbound' + Exec 'DELETE dbo.mydo_job; DELETE dbo.mydo_job_detail; DELETE dbo.bas_stock; DELETE dbo.bas_cargo_space; DELETE mixed_pilot.dispatch_hold; DELETE mixed_pilot.move_audit; DELETE mixed_pilot.admission;' + BatchFile "$PSScriptRoot/tests/seed.sql" + Exec "UPDATE mixed_pilot.control SET enabled=1,all_projects_may_mix=1; UPDATE mixed_pilot.lane SET enabled=CASE WHEN lane_code='C1203' THEN 1 ELSE 0 END,physical_fit_confirmed=1; INSERT dbo.mydo_job(job_id,tray_code,warehouse_id,line_id,in_or_out,status,fault_flag) VALUES('IN-RACE','RACE-TRAY','01','RACE',1,'1','0'); INSERT dbo.mydo_job_detail(job_id,spare4,spare1,spare2,spare3,in_quantity) VALUES('IN-RACE','RACE','X','Y','Z',1)" + $raceCn=[Data.SqlClient.SqlConnection]::new("Server=$Server;Database=$testDb;Integrated Security=True;Connect Timeout=8;Application Name=FT546MixedPilotRaceTest") + $raceCn.Open() + try { + Exec 'BEGIN TRANSACTION;' + Exec "DECLARE @r int,@m varchar(255); EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; IF @r<>10 THROW 51950,'First racing allocation failed',1;" + $raceCmd=$raceCn.CreateCommand();$raceCmd.CommandTimeout=10 + $raceCmd.CommandText="DECLARE @r int,@m varchar(255); EXEC dbo.proc_allot_space 'RACE-TRAY','TEST',@r OUTPUT,@m OUTPUT; SELECT @r;" + $pending=$raceCmd.ExecuteScalarAsync() + # Hold the first reservation across the second request's lock timeout. + $second=$pending.GetAwaiter().GetResult() + Assert ($second -ne 10) 'concurrent request cannot allocate a reserved pilot slot' + Exec 'COMMIT TRANSACTION;' + Assert ((Scalar "SELECT COUNT(*) FROM dbo.mydo_job WHERE cargo_space_id='120703'") -eq 1) 'only one inbound task owns the pilot slot' + } finally { + if((Scalar 'SELECT @@TRANCOUNT') -gt 0){Exec 'ROLLBACK TRANSACTION;'} + if($raceCmd){$raceCmd.Dispose()};$raceCn.Dispose() + } + # Re-running install must preserve runtime switches and history. + Exec 'UPDATE mixed_pilot.control SET enabled=1 WHERE id=1' + & "$PSScriptRoot/install-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase + Assert ((Scalar 'SELECT enabled FROM mixed_pilot.control WHERE id=1') -eq 1) 'repeat install preserves switch' + $releaseDefinition=Scalar "SELECT OBJECT_DEFINITION(OBJECT_ID('mixed_pilot.release_ready_jobs'))" + Exec 'ALTER PROCEDURE mixed_pilot.release_ready_jobs AS RETURN;' + $driftRejected=$false + try { & "$PSScriptRoot/install-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase } catch {$driftRejected=$_.Exception.Message -like '*drift*'} + Assert $driftRejected 'repeat install detects altered pilot helpers' + Exec 'DROP PROCEDURE mixed_pilot.release_ready_jobs;' + Exec $releaseDefinition + & "$PSScriptRoot/control-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase -Mode disable + Exec 'DELETE dbo.mydo_job; DELETE dbo.bas_stock; UPDATE dbo.bas_cargo_space SET status=''1'',tray_code=NULL;' + & "$PSScriptRoot/control-mixed-pilot.ps1" -Server $Server -Database $testDb -TestDatabase -Mode uninstall + Assert ((Scalar 'SELECT installed FROM mixed_pilot.control WHERE id=1') -eq 0) 'uninstall removes runtime protection only after draining' + foreach($p in $fixture.wms_objects|Where-Object name -in @('proc_allot_space','proc_yk_job_add','proc_yk_complete')) { + $restored=Scalar "SELECT OBJECT_DEFINITION(OBJECT_ID('dbo.$($p.name)'))" + # SQL Server may persist CREATE even when ALTER was submitted. Compare the full body with only that keyword normalized. + $expected=[regex]::Replace($p.definition,'(?i)\b(?:CREATE|ALTER)\s+PROCEDURE','CREATE PROCEDURE') + $normalized=[regex]::Replace($restored,'(?i)\b(?:CREATE|ALTER)\s+PROCEDURE','CREATE PROCEDURE') + Assert ($normalized -ceq $expected) "uninstall restores $($p.name) body exactly" + } + } +} finally { + if($created){ + [Data.SqlClient.SqlConnection]::ClearAllPools() + $cn.ChangeDatabase('master') + if($testDb -notmatch '^FT546WMS_MixedPilot_Test_[a-f0-9]{32}$'){throw 'Invalid scratch database name'} + Exec "DROP DATABASE [$testDb]" + Write-Output "Scratch database removed: $testDb" + } + $cn.Dispose() +} diff --git a/scripts/mixed-pilot/tests/behavior.sql b/scripts/mixed-pilot/tests/behavior.sql new file mode 100644 index 0000000..9ec4ae0 --- /dev/null +++ b/scripts/mixed-pilot/tests/behavior.sql @@ -0,0 +1,138 @@ +SET NOCOUNT ON; +DECLARE @r int,@m varchar(255),@job varchar(255),@n int; +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r<>2 OR EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='IN-NEW' AND cargo_space_id IS NOT NULL) + THROW 51900,'Disabled pilot must preserve Legacy no-position behavior',1; + +UPDATE mixed_pilot.control SET enabled=1,all_projects_may_mix=1 WHERE id=1; +UPDATE mixed_pilot.lane SET enabled=1,physical_fit_confirmed=1 WHERE lane_code='C1203'; +-- An ordinary same-project position must win before mixed fallback. +INSERT dbo.bas_cargo_space(cargo_space_id,warehouse_id,the_floor,the_platoon,the_column,group_coding_no,group_coding,this_cargo_area,status,locked,tray_code) +VALUES('110801','01',1,11,8,'C1101','1','C','2','N','SAME'),('110701','01',1,11,7,'C1101','2','C','1','N',NULL); +INSERT dbo.bas_stock(stock_id,cargo_space_id,tray_code,spare4,spare1,spare2,spare3,quantity,out_quantity) +VALUES('SAME-S','110801','SAME','NEW','D','E','F',1,0); +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r<>10 OR NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='IN-NEW' AND cargo_space_id='110701') + THROW 51901,'Normal Legacy allocation must have priority',1; +UPDATE dbo.mydo_job SET cargo_space_id=NULL,status='1' WHERE job_id='IN-NEW'; +DELETE dbo.bas_stock WHERE stock_id='SAME-S'; +DELETE dbo.bas_cargo_space WHERE group_coding_no='C1101'; + +-- No physical confirmation, missing E capacity, ambiguity and common-project input fail closed. +UPDATE mixed_pilot.lane SET physical_fit_confirmed=0 WHERE lane_code='C1203'; +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r=10 THROW 51902,'Unconfirmed physical fit must not admit mixed pallet',1; +UPDATE mixed_pilot.lane SET physical_fit_confirmed=1 WHERE lane_code='C1203'; +UPDATE dbo.bas_cargo_space SET locked='Y' WHERE this_cargo_area='E'; +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r=10 THROW 51903,'No same-floor E capacity must not admit mixed pallet',1; +UPDATE dbo.bas_cargo_space SET locked='N' WHERE this_cargo_area='E'; +UPDATE dbo.mydo_job_detail SET spare4='WRONG' WHERE job_id='IN-NEW'; +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r=10 THROW 51904,'Header/detail project mismatch must be rejected',1; +UPDATE dbo.mydo_job_detail SET spare4='NEW' WHERE job_id='IN-NEW'; + +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r<>10 OR NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='IN-NEW' AND cargo_space_id='120703' AND status='3') + THROW 51905,'Fallback must reserve only the designated outer position',1; +IF NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE cargo_space_id='120703' AND status='3') + THROW 51906,'Fallback must reserve slot and job together',1; +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r<>10 OR (SELECT COUNT(*) FROM mixed_pilot.admission)<>1 + THROW 51907,'Repeated allocation must return same reservation without a second admission',1; + +INSERT dbo.mydo_job(job_id,tray_code,warehouse_id,line_id,in_or_out,status,execution_phase,fault_flag) +VALUES('IN-THIRD','THIRD-TRAY','01','THIRD',1,'1','0','0'); +INSERT dbo.mydo_job_detail(job_detail_id,job_id,spare4,spare1,spare2,spare3,in_quantity) +VALUES('IN-THIRD-1','IN-THIRD','THIRD','X','Y','Z',1); +EXEC dbo.proc_allot_space 'THIRD-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r=10 THROW 51908,'A third project or an already reserved front slot must be rejected',1; + +-- Simulate a confirmed physical inbound completion (no WCS or HTTP is contacted). +INSERT dbo.bas_stock(stock_id,cargo_space_id,tray_code,spare4,spare1,spare2,spare3,quantity,out_quantity) +VALUES('GUEST-S','120703','NEW-TRAY','NEW','D','E','F',1,0); +UPDATE dbo.bas_cargo_space SET status='2',tray_code='NEW-TRAY' WHERE cargo_space_id='120703'; +DELETE dbo.mydo_job WHERE job_id='IN-NEW'; + +-- Automatic allocation creates the blocker move before the rear job. +EXEC dbo.proc_yk_job_add '120703','130803','NEW-TRAY'; +SELECT @job=job_id FROM dbo.mydo_job WHERE in_or_out=30 AND tray_code='NEW-TRAY'; +UPDATE dbo.bas_cargo_space SET status='4' WHERE cargo_space_id='120803'; +INSERT dbo.mydo_job(job_id,tray_code,cargo_space_id,class_id,in_or_out,status,execution_phase,task_no) +VALUES('OUT-REAR','RESIDENT','120803','3',21,'1','0',800001); +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-REAR' AND status='8') + THROW 51909,'Blocked rear outbound must be held before the WMS scheduler can see it',1; +IF NOT EXISTS(SELECT 1 FROM mixed_pilot.dispatch_hold WHERE job_id='OUT-REAR' AND released_at IS NULL) + THROW 51910,'Held job must be diagnosable',1; + +-- Occupied or wrong-floor destinations must not create a move or change inventory. +BEGIN TRY + EXEC dbo.proc_yk_job_add '120703','020704','NEW-TRAY'; + THROW 51911,'Wrong-floor move was accepted',1; +END TRY BEGIN CATCH IF ERROR_NUMBER()=51911 THROW; END CATCH; +IF (SELECT COUNT(*) FROM dbo.mydo_job WHERE in_or_out=30)<>1 + THROW 51912,'Rejected relocation must leave no partial task',1; + +EXEC dbo.proc_yk_job_add '120703','130803','NEW-TRAY'; +IF @job IS NULL OR NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id=@job AND status='1') + THROW 51913,'Outer relocation must remain dispatchable',1; +EXEC dbo.proc_yk_job_add '120703','130803','NEW-TRAY'; +IF (SELECT COUNT(*) FROM dbo.mydo_job WHERE in_or_out=30)<>1 + THROW 51914,'Repeated identical relocation must not duplicate tasks',1; +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-REAR' AND status='8') + THROW 51915,'Creating a relocation is not physical completion',1; + +-- Switch OFF while pallets are mixed: no new admissions, protection must remain. +UPDATE mixed_pilot.control SET enabled=0 WHERE id=1; +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-REAR' AND status='8') + THROW 51916,'Disabling admission must not release blocked outbound',1; +EXEC dbo.proc_yk_complete @job,@r OUTPUT; +IF @r<>10 OR NOT EXISTS(SELECT 1 FROM dbo.bas_stock WHERE stock_id='GUEST-S' AND cargo_space_id='130803' AND spare4='NEW') + THROW 51917,'Move completion must preserve guest project ownership',1; +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-REAR' AND status='1') + THROW 51918,'Rear outbound must be released only after confirmed front clearance',1; +SELECT @n=COUNT(*) FROM dbo.move_msg; +EXEC dbo.proc_yk_complete @job,@r OUTPUT; +IF @r<>10 OR (SELECT COUNT(*) FROM dbo.move_msg)<>@n + THROW 51919,'Duplicate move completion must not change stock or emit duplicate move messages',1; + +-- Ordinary front outbound also blocks the rear until BOTH stock and job are cleared. +DELETE dbo.mydo_job WHERE job_id='OUT-REAR'; +UPDATE dbo.bas_stock SET cargo_space_id='120703' WHERE stock_id='GUEST-S'; +UPDATE dbo.bas_cargo_space SET status='2',tray_code='NEW-TRAY' WHERE cargo_space_id='120703'; +INSERT dbo.mydo_job(job_id,tray_code,cargo_space_id,class_id,in_or_out,status,execution_phase,task_no) +VALUES('OUT-FRONT','NEW-TRAY','120703','3',21,'1','0',800002),('OUT-REAR2','RESIDENT','120803','3',21,'1','0',800003); +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-FRONT' AND status='1') OR NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-REAR2' AND status='8') + THROW 51920,'Front may dispatch while blocked rear must wait',1; +UPDATE dbo.bas_cargo_space SET status='1',tray_code=NULL WHERE cargo_space_id='120703'; +DELETE dbo.bas_stock WHERE stock_id='GUEST-S'; +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-REAR2' AND status='8') + THROW 51921,'An active front task must continue to block rear dispatch',1; +DELETE dbo.mydo_job WHERE job_id='OUT-FRONT'; +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OUT-REAR2' AND status='1') + THROW 51922,'Final front task deletion must release waiting rear job',1; + +-- Unlisted lanes must not be held by the pilot triggers. +INSERT dbo.mydo_job(job_id,tray_code,cargo_space_id,class_id,in_or_out,status,execution_phase,task_no) +VALUES('OTHER-OUT','OTHER-RESIDENT','080801','1',21,'1','0',800004); +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='OTHER-OUT' AND status='1') + THROW 51923,'Unlisted lanes must retain Legacy dispatch behavior',1; +-- Manual allocation must not strand a held task when no blocker move exists. +DELETE dbo.mydo_job WHERE job_id='OUT-REAR2'; +UPDATE dbo.bas_cargo_space SET status='2',tray_code='NEW-TRAY' WHERE cargo_space_id='120703'; +INSERT dbo.bas_stock(stock_id,cargo_space_id,tray_code,spare4,quantity,out_quantity) +VALUES('GUEST-AGAIN','120703','NEW-TRAY','NEW',1,0); +BEGIN TRY + INSERT dbo.mydo_job(job_id,tray_code,cargo_space_id,class_id,in_or_out,status,execution_phase,task_no) + VALUES('MANUAL-BLOCKED','RESIDENT','120803','3',21,'1','0',800005); + THROW 51924,'Manual blocked outbound was accepted without a blocker task',1; +END TRY BEGIN CATCH IF ERROR_NUMBER()=51924 THROW; END CATCH; +IF EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id='MANUAL-BLOCKED') + THROW 51925,'Rejected manual allocation must not leave a stranded held job',1; +BEGIN TRY + EXEC dbo.proc_yk_job_add '080801','130803','OTHER-RESIDENT'; + THROW 51926,'Unlisted source overwrote a protected relocation destination',1; +END TRY BEGIN CATCH IF ERROR_NUMBER()=51926 THROW; END CATCH; +IF NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE cargo_space_id='130803' AND status='2' AND tray_code='NEW-TRAY') + THROW 51927,'Rejected stale relocation target must retain existing occupancy',1; +PRINT 'PASS: mixed-pilot sequential behavior assertions'; diff --git a/scripts/mixed-pilot/tests/legacy-outbound.sql b/scripts/mixed-pilot/tests/legacy-outbound.sql new file mode 100644 index 0000000..09d1f5d --- /dev/null +++ b/scripts/mixed-pilot/tests/legacy-outbound.sql @@ -0,0 +1,29 @@ +SET NOCOUNT ON; +UPDATE mixed_pilot.control SET enabled=1,all_projects_may_mix=1; +UPDATE mixed_pilot.lane SET enabled=CASE WHEN lane_code='C1203' THEN 1 ELSE 0 END,protected=1,physical_fit_confirmed=1; +DECLARE @r int,@m varchar(255),@outResult varchar(100),@move varchar(255),@rear varchar(255); +EXEC dbo.proc_allot_space 'NEW-TRAY','TEST',@r OUTPUT,@m OUTPUT; +IF @r<>10 THROW 51930,'Failed to prepare genuine Legacy outbound fixture',1; +INSERT dbo.bas_stock(stock_id,cargo_space_id,tray_code,spare4,spare1,spare2,spare3,product_id,quantity,out_quantity,in_date) +VALUES('GUEST-S','120703','NEW-TRAY','NEW','D','E','F','P-GUEST',1,0,GETDATE()); +UPDATE dbo.bas_cargo_space SET status='2',tray_code='NEW-TRAY' WHERE cargo_space_id='120703'; +DELETE dbo.mydo_job WHERE job_id='IN-NEW'; +UPDATE dbo.bas_stock SET product_id='P-REAR',in_date=GETDATE() WHERE stock_id='S1'; +INSERT dbo.out_stockout(stockout_id,warehouse_id,stockout_source,fid,spare6,spare8,out_type) +VALUES('ORDER1','01','0','1','1','2','21'); +INSERT dbo.out_stockout_detail(stockout_id,stockout_detail_id,product_id,spare1,spare2,spare3,spare4,quantity,allot_quantity) +VALUES('ORDER1','ORDER1-1','P-REAR','A','B','C','OLD',1,0); +EXEC dbo.proc_out_auto_allocation 'ORDER1-1',@outResult OUTPUT,@m OUTPUT; +IF @outResult<>'10' BEGIN DECLARE @failure nvarchar(2048)=N'Legacy auto allocation failed: '+ISNULL(@m,'NULL'); THROW 51931,@failure,1; END; +SELECT @move=job_id FROM dbo.mydo_job WHERE in_or_out=30 AND tray_code='NEW-TRAY'; +SELECT @rear=job_id FROM dbo.mydo_job WHERE in_or_out=21 AND tray_code='RESIDENT'; +IF @move IS NULL OR @rear IS NULL THROW 51932,'Actual Legacy must create both correct tray tasks',1; +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id=@move AND cargo_space_id='120703' AND status='1') + THROW 51933,'Legacy must move the outer guest, not the resident',1; +IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id=@rear AND cargo_space_id='120803' AND status='8') + THROW 51934,'Legacy rear task must be invisible to the status=1 dispatch scheduler',1; +EXEC dbo.proc_yk_complete @move,@r OUTPUT; +IF @r<>10 OR NOT EXISTS(SELECT 1 FROM dbo.mydo_job WHERE job_id=@rear AND status='1') + THROW 51935,'Actual Legacy relocation completion must release the rear task',1; +IF NOT EXISTS(SELECT 1 FROM dbo.bas_stock WHERE stock_id='S1' AND spare4='OLD' AND out_quantity=1) + THROW 51936,'Resident allocation and project must retain Legacy values',1; diff --git a/scripts/mixed-pilot/tests/seed.sql b/scripts/mixed-pilot/tests/seed.sql new file mode 100644 index 0000000..1a30924 --- /dev/null +++ b/scripts/mixed-pilot/tests/seed.sql @@ -0,0 +1,22 @@ +INSERT dbo.bas_cargo_space(cargo_space_id,warehouse_id,the_floor,the_platoon,the_column,group_coding_no,group_coding,this_cargo_area,status,locked,tray_code) +VALUES ('120803','01',3,12,8,'C1203','1','C','2','N','RESIDENT'), + ('120703','01',3,12,7,'C1203','2','C','1','N',NULL), + ('020804','01',4,2,8,'C0204','1','C','2','N','RESIDENT4'), + ('020704','01',4,2,7,'C0204','2','C','1','N',NULL), + ('020805','01',5,2,8,'C0205','1','C','2','N','RESIDENT5'), + ('020705','01',5,2,7,'C0205','2','C','1','N',NULL), + ('130803','01',3,13,8,'C1303','1','E','1','N',NULL), + ('130703','01',3,13,7,'C1303','2','E','1','N',NULL), + ('140803','01',3,14,8,'C1403','1','E','1','N',NULL), + ('140703','01',3,14,7,'C1403','2','E','1','N',NULL), + ('080801','01',1,8,8,'C0801','1','C','2','N','OTHER-RESIDENT'), + ('080701','01',1,8,7,'C0801','2','C','1','N',NULL); +INSERT dbo.bas_stock(stock_id,cargo_space_id,tray_code,spare4,spare1,spare2,spare3,quantity,out_quantity) +VALUES ('S1','120803','RESIDENT','OLD','A','B','C',10,0), + ('S4','020804','RESIDENT4','OLD4','A','B','C',10,0), + ('S5','020805','RESIDENT5','OLD5','A','B','C',10,0), + ('SX','080801','OTHER-RESIDENT','OLD-X','A','B','C',10,0); +INSERT dbo.mydo_job(job_id,tray_code,warehouse_id,line_id,in_or_out,status,execution_phase,fault_flag) +VALUES ('IN-NEW','NEW-TRAY','01','NEW',1,'1','0','0'); +INSERT dbo.mydo_job_detail(job_detail_id,job_id,spare4,spare1,spare2,spare3,product_id,in_quantity) +VALUES ('IN-NEW-1','IN-NEW','NEW','D','E','F','P1',1); diff --git a/scripts/mixed-pilot/wrappers.sql b/scripts/mixed-pilot/wrappers.sql new file mode 100644 index 0000000..3f16248 --- /dev/null +++ b/scripts/mixed-pilot/wrappers.sql @@ -0,0 +1,112 @@ +ALTER PROCEDURE dbo.proc_allot_space @v_tray_code varchar(80),@v_code varchar(80),@v_result int OUTPUT,@v_resultmsg varchar(255) OUTPUT AS +BEGIN + SET NOCOUNT ON; + IF NOT EXISTS(SELECT 1 FROM mixed_pilot.control WHERE enabled=1) AND NOT EXISTS(SELECT 1 FROM mixed_pilot.lane WHERE protected=1) + BEGIN + EXEC mixed_pilot.original_allot_space @v_tray_code,@v_code,@v_result OUTPUT,@v_resultmsg OUTPUT; + RETURN; + END; + SET XACT_ABORT OFF; + DECLARE @own bit=CASE WHEN @@TRANCOUNT=0 THEN 1 ELSE 0 END,@lock int,@dummy int,@job varchar(255),@slot varchar(80); + IF @own=1 BEGIN TRANSACTION; ELSE SAVE TRANSACTION mp_inbound; + BEGIN TRY + EXEC @lock=sys.sp_getapplock @Resource='FT546.mixed_pilot.inbound',@LockMode='Exclusive',@LockOwner='Transaction',@LockTimeout=500; + IF @lock<0 THROW 51120,N'试点分配正在处理,请稍后重试。',1; + SELECT @dummy=COUNT(*) FROM dbo.bas_cargo_space s WITH(UPDLOCK,HOLDLOCK) + JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no WHERE l.enabled=1 OR l.protected=1; + SELECT @dummy=COUNT(*) FROM dbo.mydo_job WITH(UPDLOCK,HOLDLOCK) WHERE tray_code=@v_tray_code; + IF @dummy<>1 THROW 51121,N'托盘任务不存在或不唯一,请核对后重试。',1; + SELECT @job=job_id,@slot=NULLIF(cargo_space_id,'') FROM dbo.mydo_job WHERE tray_code=@v_tray_code AND in_or_out<20; + IF @job IS NULL THROW 51122,N'没有对应的入库任务。',1; + IF @slot IS NOT NULL + BEGIN + IF NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE cargo_space_id=@slot AND status='3') + THROW 51123,N'已有入库分配的货位状态不一致,请核对任务。',1; + SET @v_result=10; SET @v_resultmsg='成功'; + END + ELSE EXEC mixed_pilot.allot_space_with_fallback @v_tray_code,@v_code,@v_result OUTPUT,@v_resultmsg OUTPUT; + IF @own=1 COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @own=1 AND XACT_STATE()<>0 ROLLBACK TRANSACTION; + ELSE IF XACT_STATE()=1 ROLLBACK TRANSACTION mp_inbound; + ELSE IF XACT_STATE()=-1 THROW; + SET @v_result=2; SET @v_resultmsg=CONVERT(varchar(255),ERROR_MESSAGE()); + END CATCH; +END; +GO +ALTER PROCEDURE dbo.proc_yk_job_add @v_cargo_space_id varchar(255),@v_tar_cargo_space_id varchar(255),@v_tray_code varchar(255) AS +BEGIN + SET NOCOUNT ON; + IF NOT EXISTS(SELECT 1 FROM mixed_pilot.lane WHERE enabled=1 OR protected=1) + BEGIN + EXEC mixed_pilot.original_yk_job_add @v_cargo_space_id,@v_tar_cargo_space_id,@v_tray_code; + RETURN; + END; + DECLARE @own bit=CASE WHEN @@TRANCOUNT=0 THEN 1 ELSE 0 END,@job varchar(255),@dummy int; + IF @own=1 BEGIN TRANSACTION; ELSE SAVE TRANSACTION mp_move; + BEGIN TRY + SELECT @dummy=COUNT(*) FROM dbo.bas_cargo_space WITH(UPDLOCK,HOLDLOCK) WHERE cargo_space_id IN(@v_cargo_space_id,@v_tar_cargo_space_id); + IF NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space s JOIN mixed_pilot.lane l ON l.warehouse_id=s.warehouse_id AND l.lane_code=s.group_coding_no + WHERE s.cargo_space_id IN(@v_cargo_space_id,@v_tar_cargo_space_id) AND (l.protected=1 OR l.enabled=1)) + AND NOT EXISTS(SELECT 1 FROM mixed_pilot.move_audit WHERE target_slot=@v_tar_cargo_space_id) + BEGIN + EXEC mixed_pilot.original_yk_job_add @v_cargo_space_id,@v_tar_cargo_space_id,@v_tray_code; + IF @own=1 COMMIT TRANSACTION; + RETURN; + END; + IF EXISTS(SELECT 1 FROM mixed_pilot.move_audit a JOIN dbo.mydo_job j ON j.job_id=a.job_id + WHERE a.source_slot=@v_cargo_space_id AND a.target_slot=@v_tar_cargo_space_id AND a.tray_code=@v_tray_code AND a.completed_at IS NULL + AND j.in_or_out=30 AND j.cargo_space_id=a.source_slot AND j.target_cargo_space_id=a.target_slot AND j.tray_code=a.tray_code) + BEGIN + IF @own=1 COMMIT TRANSACTION; + RETURN; + END; + EXEC mixed_pilot.check_move @v_cargo_space_id,@v_tar_cargo_space_id,@v_tray_code; + EXEC mixed_pilot.original_yk_job_add @v_cargo_space_id,@v_tar_cargo_space_id,@v_tray_code; + SELECT @job=job_id FROM dbo.mydo_job WHERE in_or_out=30 AND cargo_space_id=@v_cargo_space_id AND target_cargo_space_id=@v_tar_cargo_space_id AND tray_code=@v_tray_code; + IF @job IS NULL THROW 51124,N'未能建立可核验的移库任务。',1; + INSERT mixed_pilot.move_audit(job_id,source_slot,target_slot,tray_code) VALUES(@job,@v_cargo_space_id,@v_tar_cargo_space_id,@v_tray_code); + IF @own=1 COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @own=1 AND XACT_STATE()<>0 ROLLBACK TRANSACTION; + ELSE IF XACT_STATE()=1 ROLLBACK TRANSACTION mp_move; + THROW; + END CATCH; +END; +GO +ALTER PROCEDURE dbo.proc_yk_complete @v_job_id varchar(255),@v_result int OUTPUT AS +BEGIN + SET NOCOUNT ON; + IF NOT EXISTS(SELECT 1 FROM mixed_pilot.move_audit WHERE job_id=@v_job_id) + BEGIN + EXEC mixed_pilot.original_yk_complete @v_job_id,@v_result OUTPUT; + RETURN; + END; + DECLARE @own bit=CASE WHEN @@TRANCOUNT=0 THEN 1 ELSE 0 END,@src varchar(255),@dst varchar(255),@tray varchar(255),@done datetime2,@dummy int; + IF @own=1 BEGIN TRANSACTION; ELSE SAVE TRANSACTION mp_complete; + BEGIN TRY + SELECT @src=source_slot,@dst=target_slot,@tray=tray_code,@done=completed_at FROM mixed_pilot.move_audit WITH(UPDLOCK,HOLDLOCK) WHERE job_id=@v_job_id; + IF @done IS NULL + BEGIN + SELECT @dummy=COUNT(*) FROM dbo.bas_cargo_space WITH(UPDLOCK,HOLDLOCK) WHERE cargo_space_id IN(@src,@dst); + IF NOT EXISTS(SELECT 1 FROM dbo.mydo_job WITH(UPDLOCK,HOLDLOCK) WHERE job_id=@v_job_id AND in_or_out=30 AND tray_code=@tray AND cargo_space_id=@src AND target_cargo_space_id=@dst) + OR NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE cargo_space_id=@src AND status='4' AND tray_code=@tray) + OR NOT EXISTS(SELECT 1 FROM dbo.bas_cargo_space WHERE cargo_space_id=@dst AND status='3' AND NULLIF(tray_code,'') IS NULL) + OR EXISTS(SELECT 1 FROM dbo.bas_stock WHERE cargo_space_id=@dst) + OR NOT EXISTS(SELECT 1 FROM dbo.bas_stock WHERE cargo_space_id=@src AND tray_code=@tray) + OR EXISTS(SELECT 1 FROM dbo.bas_stock WHERE cargo_space_id=@src AND (tray_code<>@tray OR tray_code IS NULL)) + THROW 51125,N'移库完成的起终点或库存不一致,未修改库存。',1; + EXEC mixed_pilot.original_yk_complete @v_job_id,@v_result OUTPUT; + UPDATE mixed_pilot.move_audit SET completed_at=SYSUTCDATETIME() WHERE job_id=@v_job_id; + END; + SET @v_result=10; + IF @own=1 COMMIT TRANSACTION; + END TRY + BEGIN CATCH + IF @own=1 AND XACT_STATE()<>0 ROLLBACK TRANSACTION; + ELSE IF XACT_STATE()=1 ROLLBACK TRANSACTION mp_complete; + THROW; + END CATCH; +END;