fix: hold blocked outbound before WCS dispatch
This commit is contained in:
@@ -51,6 +51,11 @@ public class ToWcsServiceImpl implements IToWcsService {
|
|||||||
mydoJobParam.setInOut("2");
|
mydoJobParam.setInOut("2");
|
||||||
List<MydoJob> outMydoJobs = mydoJobService.selectMydoJobListApi(mydoJobParam);
|
List<MydoJob> outMydoJobs = mydoJobService.selectMydoJobListApi(mydoJobParam);
|
||||||
for (MydoJob outMydoJob : outMydoJobs) {
|
for (MydoJob outMydoJob : outMydoJobs) {
|
||||||
|
// HTTP success only confirms receipt. A rear outbound stays in status 1 until
|
||||||
|
// every deeper position in its own lane is physically clear.
|
||||||
|
if (mydoJobService.isOutboundDispatchBlocked(outMydoJob.getJobId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
RequestLog requestLog = requestLogService.getRequestLog(PUSH_OUT_MYDO_JOB, JSONUtil.toJsonStr(outMydoJob), "推送出库任务","wms","wcs");
|
RequestLog requestLog = requestLogService.getRequestLog(PUSH_OUT_MYDO_JOB, JSONUtil.toJsonStr(outMydoJob), "推送出库任务","wms","wcs");
|
||||||
if ("1".equals(requestLog.getIsSuccessful())){
|
if ("1".equals(requestLog.getIsSuccessful())){
|
||||||
outMydoJob.setStatus("3");
|
outMydoJob.setStatus("3");
|
||||||
|
|||||||
@@ -95,4 +95,6 @@ public interface MydoJobMapper
|
|||||||
void updateLedError(HashMap<String, Object> map);
|
void updateLedError(HashMap<String, Object> map);
|
||||||
|
|
||||||
Integer selectBySpare4OrCargo(InStockinDetail inStockinDetail);
|
Integer selectBySpare4OrCargo(InStockinDetail inStockinDetail);
|
||||||
|
|
||||||
|
int countOutboundDispatchBlockers(@Param("jobId") String jobId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,4 +111,7 @@ public interface IMydoJobService
|
|||||||
List<MydoJob> getInJobBySerial(String serial);
|
List<MydoJob> getInJobBySerial(String serial);
|
||||||
|
|
||||||
void ykCompleted(MydoJob outJob);
|
void ykCompleted(MydoJob outJob);
|
||||||
|
|
||||||
|
/** True while a deeper position in the same lane blocks this outbound. */
|
||||||
|
boolean isOutboundDispatchBlocked(String jobId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,4 +540,9 @@ public class MydoJobServiceImpl implements IMydoJobService {
|
|||||||
log.info("移库任务处理成功 jobId:[{}] trayCode:[{}]", outJob.getJobId(), outJob.getTrayCode());
|
log.info("移库任务处理成功 jobId:[{}] trayCode:[{}]", outJob.getJobId(), outJob.getTrayCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOutboundDispatchBlocked(String jobId) {
|
||||||
|
return mydoJobMapper.countOutboundDispatchBlockers(jobId) > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,6 +271,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
select * from mydo_job where task_no = #{taskNo}
|
select * from mydo_job where task_no = #{taskNo}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countOutboundDispatchBlockers" parameterType="String" resultType="int">
|
||||||
|
select count(1)
|
||||||
|
from mydo_job job
|
||||||
|
join bas_cargo_space source_space on source_space.cargo_space_id = job.cargo_space_id
|
||||||
|
where job.job_id = #{jobId}
|
||||||
|
and try_convert(int, job.in_or_out) >= 20
|
||||||
|
and exists (
|
||||||
|
select 1
|
||||||
|
from bas_cargo_space outer_space
|
||||||
|
where outer_space.warehouse_id = source_space.warehouse_id
|
||||||
|
and outer_space.group_coding_no = source_space.group_coding_no
|
||||||
|
and try_convert(int, outer_space.group_coding) > try_convert(int, source_space.group_coding)
|
||||||
|
and (
|
||||||
|
isnull(outer_space.status, '') <> '1'
|
||||||
|
or isnull(outer_space.locked, '') <> 'N'
|
||||||
|
or nullif(ltrim(rtrim(outer_space.tray_code)), '') is not null
|
||||||
|
or exists (select 1 from bas_stock stock where stock.cargo_space_id = outer_space.cargo_space_id)
|
||||||
|
or exists (
|
||||||
|
select 1 from mydo_job outer_job
|
||||||
|
where outer_job.job_id <> job.job_id
|
||||||
|
and (outer_job.cargo_space_id = outer_space.cargo_space_id
|
||||||
|
or outer_job.target_cargo_space_id = outer_space.cargo_space_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
<select id="allotInJobSpace" parameterType="java.util.Map" statementType="CALLABLE">
|
<select id="allotInJobSpace" parameterType="java.util.Map" statementType="CALLABLE">
|
||||||
{call proc_allot_space(
|
{call proc_allot_space(
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.ruoyi.api.service.impl;
|
||||||
|
|
||||||
|
import com.ruoyi.job.mydojob.domain.MydoJob;
|
||||||
|
import com.ruoyi.job.mydojob.service.IMydoJobService;
|
||||||
|
import com.ruoyi.log.domain.RequestLog;
|
||||||
|
import com.ruoyi.log.service.IRequestLogService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class ToWcsServiceImplTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blockedRearOutboundWaitsWithoutCallingWcsOrAdvancingStatus() {
|
||||||
|
IRequestLogService requests = mock(IRequestLogService.class);
|
||||||
|
IMydoJobService jobs = mock(IMydoJobService.class);
|
||||||
|
MydoJob rear = outbound("OUT-REAR", "120803");
|
||||||
|
when(jobs.selectMydoJobListApi(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(rear));
|
||||||
|
when(jobs.isOutboundDispatchBlocked("OUT-REAR")).thenReturn(true);
|
||||||
|
ToWcsServiceImpl service = service(requests, jobs);
|
||||||
|
|
||||||
|
service.pushOutMydoJob();
|
||||||
|
|
||||||
|
verify(requests, never()).getRequestLog(anyString(), anyString(), anyString(), anyString(), anyString());
|
||||||
|
verify(jobs, never()).updateMydoJob(rear);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unblockedOutboundDispatchesAndAdvancesExactlyOnce() {
|
||||||
|
IRequestLogService requests = mock(IRequestLogService.class);
|
||||||
|
IMydoJobService jobs = mock(IMydoJobService.class);
|
||||||
|
MydoJob front = outbound("OUT-FRONT", "120703");
|
||||||
|
when(jobs.selectMydoJobListApi(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(front));
|
||||||
|
when(jobs.isOutboundDispatchBlocked("OUT-FRONT")).thenReturn(false);
|
||||||
|
RequestLog accepted = new RequestLog();
|
||||||
|
accepted.setIsSuccessful("1");
|
||||||
|
when(requests.getRequestLog(eq(ToWcsServiceImpl.PUSH_OUT_MYDO_JOB), anyString(),
|
||||||
|
eq("推送出库任务"), eq("wms"), eq("wcs"))).thenReturn(accepted);
|
||||||
|
ToWcsServiceImpl service = service(requests, jobs);
|
||||||
|
|
||||||
|
service.pushOutMydoJob();
|
||||||
|
|
||||||
|
verify(requests).getRequestLog(eq(ToWcsServiceImpl.PUSH_OUT_MYDO_JOB), anyString(),
|
||||||
|
eq("推送出库任务"), eq("wms"), eq("wcs"));
|
||||||
|
verify(jobs).updateMydoJob(front);
|
||||||
|
org.assertj.core.api.Assertions.assertThat(front.getStatus()).isEqualTo("3");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToWcsServiceImpl service(IRequestLogService requests, IMydoJobService jobs) {
|
||||||
|
ToWcsServiceImpl service = new ToWcsServiceImpl();
|
||||||
|
ReflectionTestUtils.setField(service, "requestLogService", requests);
|
||||||
|
ReflectionTestUtils.setField(service, "mydoJobService", jobs);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MydoJob outbound(String jobId, String spaceId) {
|
||||||
|
MydoJob job = new MydoJob();
|
||||||
|
job.setJobId(jobId);
|
||||||
|
job.setCargoSpaceId(spaceId);
|
||||||
|
job.setInOrOut("21");
|
||||||
|
job.setStatus("1");
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
scripts/dispatch-guard/test-dispatch-guard.ps1
Normal file
44
scripts/dispatch-guard/test-dispatch-guard.ps1
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
param(
|
||||||
|
[string]$Server = 'lpc:.',
|
||||||
|
[string]$MapperFile = "$PSScriptRoot/../../ruoyi-admin/src/main/resources/mapper/job/MydoJobMapper.xml"
|
||||||
|
)
|
||||||
|
$ErrorActionPreference='Stop'
|
||||||
|
[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)
|
||||||
|
$testDb='FT546WMS_DispatchGuard_Test_'+[Guid]::NewGuid().ToString('N')
|
||||||
|
$cn=[Data.SqlClient.SqlConnection]::new("Server=$Server;Database=master;Integrated Security=True;Connect Timeout=8;Application Name=FT546DispatchGuardTests")
|
||||||
|
function Exec([string]$sql){$c=$cn.CreateCommand();$c.CommandTimeout=15;$c.CommandText=$sql;try{[void]$c.ExecuteNonQuery()}finally{$c.Dispose()}}
|
||||||
|
function Scalar([string]$sql,[string]$jobId=$null){$c=$cn.CreateCommand();$c.CommandTimeout=15;$c.CommandText=$sql;if($null-ne$jobId){[void]$c.Parameters.Add('@jobId',[Data.SqlDbType]::VarChar,255);$c.Parameters['@jobId'].Value=$jobId};try{$c.ExecuteScalar()}finally{$c.Dispose()}}
|
||||||
|
function Assert([bool]$ok,[string]$name){if(-not$ok){throw "FAIL: $name"};Write-Output "PASS: $name"}
|
||||||
|
$created=$false
|
||||||
|
$cn.Open()
|
||||||
|
try{
|
||||||
|
$data=[string](Scalar "SELECT CONVERT(nvarchar(4000),SERVERPROPERTY('InstanceDefaultDataPath'))")
|
||||||
|
$log=[string](Scalar "SELECT CONVERT(nvarchar(4000),SERVERPROPERTY('InstanceDefaultLogPath'))")
|
||||||
|
Exec "CREATE DATABASE [$testDb] ON PRIMARY(NAME=N'$testDb',FILENAME=N'$data$testDb.mdf',SIZE=8MB,MAXSIZE=64MB) LOG ON(NAME=N'${testDb}_log',FILENAME=N'$log${testDb}_log.ldf',SIZE=8MB,MAXSIZE=64MB)"
|
||||||
|
$created=$true;$cn.ChangeDatabase($testDb);Exec 'SET LOCK_TIMEOUT 1500;SET DEADLOCK_PRIORITY LOW;'
|
||||||
|
Exec @'
|
||||||
|
CREATE TABLE bas_cargo_space(cargo_space_id varchar(80),warehouse_id varchar(80),group_coding_no varchar(80),group_coding varchar(20),status varchar(10),locked varchar(10),tray_code varchar(80));
|
||||||
|
CREATE TABLE bas_stock(stock_id varchar(80),cargo_space_id varchar(80));
|
||||||
|
CREATE TABLE mydo_job(job_id varchar(255),cargo_space_id varchar(80),target_cargo_space_id varchar(80),in_or_out varchar(10));
|
||||||
|
INSERT bas_cargo_space VALUES
|
||||||
|
('REAR','01','LANE-A','1','4','N','REAR-TRAY'),('FRONT','01','LANE-A','2','2','N','FRONT-TRAY'),
|
||||||
|
('OTHER-REAR','01','LANE-B','1','4','N','OTHER'),('OTHER-FRONT','01','LANE-B','2','1','N',NULL);
|
||||||
|
INSERT bas_stock VALUES('FRONT-STOCK','FRONT');
|
||||||
|
INSERT mydo_job VALUES('OUT-REAR','REAR',NULL,'21'),('OUT-FRONT','FRONT',NULL,'21'),('OUT-OTHER','OTHER-REAR',NULL,'21');
|
||||||
|
'@
|
||||||
|
$settings=[Xml.XmlReaderSettings]::new();$settings.DtdProcessing=[Xml.DtdProcessing]::Ignore
|
||||||
|
$reader=[Xml.XmlReader]::Create($MapperFile,$settings);try{$xml=[Xml.XmlDocument]::new();$xml.Load($reader)}finally{$reader.Dispose()}
|
||||||
|
$node=$xml.SelectSingleNode("//*[local-name()='select' and @id='countOutboundDispatchBlockers']")
|
||||||
|
if($null-eq$node){throw 'Mapper query missing'}
|
||||||
|
$sql=$node.InnerText.Replace('#{jobId}','@jobId')
|
||||||
|
Assert ((Scalar $sql 'OUT-REAR')-gt 0) 'occupied outer position blocks rear outbound'
|
||||||
|
Assert ((Scalar $sql 'OUT-FRONT')-eq 0) 'front outbound is not blocked by its own lane'
|
||||||
|
Assert ((Scalar $sql 'OUT-OTHER')-eq 0) 'unrelated lane remains dispatchable'
|
||||||
|
Exec "DELETE bas_stock;DELETE mydo_job WHERE job_id='OUT-FRONT';UPDATE bas_cargo_space SET status='1',tray_code=NULL WHERE cargo_space_id='FRONT';INSERT mydo_job VALUES('MOVE-FRONT','FRONT','OTHER-FRONT','30')"
|
||||||
|
Assert ((Scalar $sql 'OUT-REAR')-gt 0) 'unfinished relocation keeps rear outbound waiting'
|
||||||
|
Exec "DELETE mydo_job WHERE job_id='MOVE-FRONT'"
|
||||||
|
Assert ((Scalar $sql 'OUT-REAR')-eq 0) 'confirmed relocation completion releases rear outbound'
|
||||||
|
}finally{
|
||||||
|
if($created){$cn.ChangeDatabase('master');[Data.SqlClient.SqlConnection]::ClearAllPools();if($testDb-notmatch'^FT546WMS_DispatchGuard_Test_[a-f0-9]{32}$'){throw'Invalid scratch database'};Exec "DROP DATABASE [$testDb]";Write-Output "Scratch database removed: $testDb"}
|
||||||
|
$cn.Dispose()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user