feat: prepare dense blockers during outbound allocation

This commit is contained in:
zgw
2026-07-16 10:27:59 +08:00
parent c8b8748efc
commit 94d14d538e
4 changed files with 114 additions and 13 deletions

View File

@@ -28,9 +28,14 @@ import com.ruoyi.out.location.domain.LocationOutboundGenerateRequest;
import com.ruoyi.out.location.domain.LocationOutboundStock; import com.ruoyi.out.location.domain.LocationOutboundStock;
import com.ruoyi.out.location.mapper.LocationOutboundMapper; import com.ruoyi.out.location.mapper.LocationOutboundMapper;
import com.ruoyi.out.location.service.ILocationOutboundService; import com.ruoyi.out.location.service.ILocationOutboundService;
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
import com.ruoyi.out.relocation.service.IRelocationJobService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/** /**
* 指定货位出库服务实现。 * 指定货位出库服务实现。
@@ -62,6 +67,12 @@ public class LocationOutboundServiceImpl implements ILocationOutboundService
@Autowired @Autowired
private TaskNoService taskNoService; private TaskNoService taskNoService;
@Autowired
private IRelocationJobService relocationJobService;
@Autowired
private IOutboundDispatchService dispatchService;
@Override @Override
@WarehouseScope(warehouseField = "bcs.warehouse_id") @WarehouseScope(warehouseField = "bcs.warehouse_id")
public List<LocationOutboundStock> selectLocationOutboundStockList(LocationOutboundStock query) public List<LocationOutboundStock> selectLocationOutboundStockList(LocationOutboundStock query)
@@ -150,11 +161,12 @@ public class LocationOutboundServiceImpl implements ILocationOutboundService
basCargoSpaceMapper.markOutboundAssignedByStockId(stock.getStockId()); basCargoSpaceMapper.markOutboundAssignedByStockId(stock.getStockId());
} }
AjaxResult pushResult = mydoJobService.pushOutJobToWcs(jobId); List<String> relocationIds = relocationJobService.createBlockerRelocations(jobId);
if (pushResult == null || pushResult.isError()) { AjaxResult pushResult = dispatchAfterCommit(jobId);
throw new RuntimeException(pushResult == null ? "WCS出库作业下发失败" : Convert.toStr(pushResult.get(AjaxResult.MSG_TAG))); String relocationMessage = relocationIds == null || relocationIds.isEmpty()
} ? "" : ";已生成" + relocationIds.size() + "个遮挡移库作业";
return "指定货位出库作业生成成功:" + jobId + ";" + Convert.toStr(pushResult.get(AjaxResult.MSG_TAG)); return "指定货位出库作业生成成功:" + jobId + relocationMessage
+ ";" + Convert.toStr(pushResult.get(AjaxResult.MSG_TAG));
} }
private void checkRequest(LocationOutboundGenerateRequest request) private void checkRequest(LocationOutboundGenerateRequest request)
@@ -308,4 +320,21 @@ public class LocationOutboundServiceImpl implements ILocationOutboundService
// External callbacks and focused unit tests can run without login context. // External callbacks and focused unit tests can run without login context.
} }
} }
private AjaxResult dispatchAfterCommit(String jobId)
{
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
AjaxResult result = dispatchService.dispatch(jobId, DispatchTrigger.INITIAL);
return result == null ? AjaxResult.error("WCS统一下发服务无返回") : result;
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization()
{
@Override
public void afterCommit()
{
dispatchService.dispatch(jobId, DispatchTrigger.INITIAL);
}
});
return AjaxResult.success("作业已生成,事务提交后下发WCS");
}
} }

View File

@@ -14,6 +14,9 @@ import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.in.job.service.IMydoJobService; import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.out.stockout.domain.OutStockoutAssignDTO; import com.ruoyi.out.stockout.domain.OutStockoutAssignDTO;
import com.ruoyi.out.stockout.domain.OutStockoutManualAssignDTO; import com.ruoyi.out.stockout.domain.OutStockoutManualAssignDTO;
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
import com.ruoyi.out.relocation.service.IRelocationJobService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -57,6 +60,12 @@ public class OutStockoutServiceImpl implements IOutStockoutService
@Autowired @Autowired
private BasCargoSpaceMapper basCargoSpaceMapper; private BasCargoSpaceMapper basCargoSpaceMapper;
@Autowired
private IRelocationJobService relocationJobService;
@Autowired
private IOutboundDispatchService dispatchService;
/** /**
* 查询出库单据主信息 * 查询出库单据主信息
*/ */
@@ -334,7 +343,7 @@ public class OutStockoutServiceImpl implements IOutStockoutService
} }
/** /**
* 将新生成的出库作业推送到 WCS。推送成功后把作业状态从未执行改为待执行,避免重复下发。 * 为新生成作业准备密集库遮挡移库,并统一通过 Outbox 下发 WCS。
*/ */
private AjaxResult pushPendingOutJobs(String stockoutDetailId) private AjaxResult pushPendingOutJobs(String stockoutDetailId)
{ {
@@ -344,11 +353,16 @@ public class OutStockoutServiceImpl implements IOutStockoutService
} }
StringBuilder message = new StringBuilder(); StringBuilder message = new StringBuilder();
for (MydoJob job : jobs) { for (MydoJob job : jobs) {
AjaxResult pushResult = mydoJobService.pushOutJobToWcs(job.getJobId()); List<String> relocationIds = relocationJobService.createBlockerRelocations(job.getJobId());
AjaxResult pushResult = dispatchService.dispatch(job.getJobId(), DispatchTrigger.INITIAL);
if (pushResult == null || pushResult.isError()) { if (pushResult == null || pushResult.isError()) {
return AjaxResult.error(pushResult == null ? "WCS出库作业下发失败" : Convert.toStr(pushResult.get(AjaxResult.MSG_TAG))); return AjaxResult.error(pushResult == null ? "WCS出库作业下发失败" : Convert.toStr(pushResult.get(AjaxResult.MSG_TAG)));
} }
message.append(job.getJobId()).append(":").append(Convert.toStr(pushResult.get(AjaxResult.MSG_TAG))).append("|"); message.append(job.getJobId()).append(":");
if (relocationIds != null && !relocationIds.isEmpty()) {
message.append("已生成").append(relocationIds.size()).append("个遮挡移库作业;");
}
message.append(Convert.toStr(pushResult.get(AjaxResult.MSG_TAG))).append("|");
} }
return AjaxResult.success(message.toString()); return AjaxResult.success(message.toString());
} }

View File

@@ -15,6 +15,9 @@ import com.ruoyi.in.jobdetail.service.IMydoJobDetailService;
import com.ruoyi.out.location.domain.LocationOutboundGenerateRequest; import com.ruoyi.out.location.domain.LocationOutboundGenerateRequest;
import com.ruoyi.out.location.domain.LocationOutboundStock; import com.ruoyi.out.location.domain.LocationOutboundStock;
import com.ruoyi.out.location.mapper.LocationOutboundMapper; import com.ruoyi.out.location.mapper.LocationOutboundMapper;
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
import com.ruoyi.out.relocation.service.IRelocationJobService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -59,6 +62,12 @@ class LocationOutboundServiceImplTest {
@Mock @Mock
private TaskNoService taskNoService; private TaskNoService taskNoService;
@Mock
private IRelocationJobService relocationJobService;
@Mock
private IOutboundDispatchService dispatchService;
@InjectMocks @InjectMocks
private LocationOutboundServiceImpl service; private LocationOutboundServiceImpl service;
@@ -98,14 +107,16 @@ class LocationOutboundServiceImplTest {
when(locationOutboundMapper.reserveStockDetailForOutbound(any(HashMap.class))).thenReturn(1); when(locationOutboundMapper.reserveStockDetailForOutbound(any(HashMap.class))).thenReturn(1);
when(basStockMapper.refreshBasStockSummary("KC001")).thenReturn(1); when(basStockMapper.refreshBasStockSummary("KC001")).thenReturn(1);
when(basCargoSpaceMapper.markOutboundAssignedByStockId("KC001")).thenReturn(1); when(basCargoSpaceMapper.markOutboundAssignedByStockId("KC001")).thenReturn(1);
when(mydoJobService.pushOutJobToWcs("CK001")).thenReturn(AjaxResult.success("WCS出库作业下发成功:OK")); when(relocationJobService.createBlockerRelocations("CK001")).thenReturn(List.of("MOVE-1"));
when(dispatchService.dispatch("CK001", DispatchTrigger.INITIAL))
.thenReturn(AjaxResult.success("作业暂未下发WCS:等待移库"));
LocationOutboundGenerateRequest request = new LocationOutboundGenerateRequest(); LocationOutboundGenerateRequest request = new LocationOutboundGenerateRequest();
request.setStockId("KC001"); request.setStockId("KC001");
request.setExitId("OUT01"); request.setExitId("OUT01");
String result = service.generateWholeTrayOutboundJob(request); String result = service.generateWholeTrayOutboundJob(request);
assertThat(result).contains("CK001").contains("WCS出库作业下发成功"); assertThat(result).contains("CK001").contains("等待移库");
ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class); ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class);
verify(mydoJobService).insertMydoJob(jobCaptor.capture()); verify(mydoJobService).insertMydoJob(jobCaptor.capture());
@@ -132,7 +143,9 @@ class LocationOutboundServiceImplTest {
.containsExactly("SD001", "SD002"); .containsExactly("SD001", "SD002");
verify(basStockMapper).refreshBasStockSummary("KC001"); verify(basStockMapper).refreshBasStockSummary("KC001");
verify(basCargoSpaceMapper).markOutboundAssignedByStockId("KC001"); verify(basCargoSpaceMapper).markOutboundAssignedByStockId("KC001");
verify(mydoJobService).pushOutJobToWcs("CK001"); verify(relocationJobService).createBlockerRelocations("CK001");
verify(dispatchService).dispatch("CK001", DispatchTrigger.INITIAL);
verify(mydoJobService, never()).pushOutJobToWcs(any());
} }
@Test @Test
@@ -157,7 +170,9 @@ class LocationOutboundServiceImplTest {
when(taskNoService.getTaskNo("20")).thenReturn("50002"); when(taskNoService.getTaskNo("20")).thenReturn("50002");
when(mydoJobService.insertMydoJob(any(MydoJob.class))).thenReturn(1); when(mydoJobService.insertMydoJob(any(MydoJob.class))).thenReturn(1);
when(basCargoSpaceMapper.markOutboundAssignedByCargoSpaceId("CS-EMPTY")).thenReturn(1); when(basCargoSpaceMapper.markOutboundAssignedByCargoSpaceId("CS-EMPTY")).thenReturn(1);
when(mydoJobService.pushOutJobToWcs("CK002")).thenReturn(AjaxResult.success("WCS出库作业下发成功:OK")); when(relocationJobService.createBlockerRelocations("CK002")).thenReturn(List.of());
when(dispatchService.dispatch("CK002", DispatchTrigger.INITIAL))
.thenReturn(AjaxResult.success("WCS出库作业下发成功:OK"));
LocationOutboundGenerateRequest request = new LocationOutboundGenerateRequest(); LocationOutboundGenerateRequest request = new LocationOutboundGenerateRequest();
request.setCargoSpaceId("CS-EMPTY"); request.setCargoSpaceId("CS-EMPTY");
@@ -181,7 +196,9 @@ class LocationOutboundServiceImplTest {
verify(locationOutboundMapper, never()).reserveStockDetailForOutbound(any(HashMap.class)); verify(locationOutboundMapper, never()).reserveStockDetailForOutbound(any(HashMap.class));
verify(basStockMapper, never()).refreshBasStockSummary(any()); verify(basStockMapper, never()).refreshBasStockSummary(any());
verify(basCargoSpaceMapper).markOutboundAssignedByCargoSpaceId("CS-EMPTY"); verify(basCargoSpaceMapper).markOutboundAssignedByCargoSpaceId("CS-EMPTY");
verify(mydoJobService).pushOutJobToWcs("CK002"); verify(relocationJobService).createBlockerRelocations("CK002");
verify(dispatchService).dispatch("CK002", DispatchTrigger.INITIAL);
verify(mydoJobService, never()).pushOutJobToWcs(any());
} }
private BasStockDetail stockDetail(String detailId, String productId, String productName, String lotNo, String availableQuantity) { private BasStockDetail stockDetail(String detailId, String productId, String productName, String lotNo, String availableQuantity) {

View File

@@ -4,6 +4,10 @@ import com.ruoyi.bas.id.job.service.JobIdService;
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper; import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.in.job.service.IMydoJobService; import com.ruoyi.in.job.service.IMydoJobService;
import com.ruoyi.in.job.domain.MydoJob;
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
import com.ruoyi.out.relocation.service.IRelocationJobService;
import com.ruoyi.out.stockout.mapper.OutStockoutMapper; import com.ruoyi.out.stockout.mapper.OutStockoutMapper;
import com.ruoyi.out.stockoutdetail.domain.OutStockoutDetail; import com.ruoyi.out.stockoutdetail.domain.OutStockoutDetail;
import com.ruoyi.out.stockoutdetail.mapper.OutStockoutDetailMapper; import com.ruoyi.out.stockoutdetail.mapper.OutStockoutDetailMapper;
@@ -46,6 +50,12 @@ class OutStockoutServiceImplTest {
@Mock @Mock
private BasCargoSpaceMapper basCargoSpaceMapper; private BasCargoSpaceMapper basCargoSpaceMapper;
@Mock
private IRelocationJobService relocationJobService;
@Mock
private IOutboundDispatchService dispatchService;
@InjectMocks @InjectMocks
private OutStockoutServiceImpl service; private OutStockoutServiceImpl service;
@@ -102,4 +112,35 @@ class OutStockoutServiceImplTest {
.contains("没有可分配库存明细"); .contains("没有可分配库存明细");
verify(mydoJobService, never()).selectPendingOutMydoJobsByStockoutDetailId(any()); verify(mydoJobService, never()).selectPendingOutMydoJobsByStockoutDetailId(any());
} }
@Test
void successfulAllocationCreatesDenseBlockersAndUsesUnifiedDispatch()
{
OutStockoutDetail detail = new OutStockoutDetail();
detail.setStockoutDetailId("CKD-1");
when(outStockoutDetailMapper.selectOutStockoutDetailListByStockoutId("CKD"))
.thenReturn(List.of(detail));
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
HashMap<String, Object> map = invocation.getArgument(0);
map.put("v_result", 10);
map.put("v_resultString", "自动分配完成");
return null;
}).when(outStockoutMapper).autoAssignOutStockoutByProc(any());
MydoJob job = new MydoJob();
job.setJobId("OUT-1");
when(mydoJobService.selectPendingOutMydoJobsByStockoutDetailId("CKD-1"))
.thenReturn(List.of(job));
when(relocationJobService.createBlockerRelocations("OUT-1"))
.thenReturn(List.of("MOVE-1"));
when(dispatchService.dispatch("OUT-1", DispatchTrigger.INITIAL))
.thenReturn(AjaxResult.success("作业暂未下发WCS:等待移库"));
AjaxResult result = service.autoAssignOutStockout("CKD", "02-AU-01");
assertThat(result.isSuccess()).isTrue();
verify(relocationJobService).createBlockerRelocations("OUT-1");
verify(dispatchService).dispatch("OUT-1", DispatchTrigger.INITIAL);
verify(mydoJobService, never()).pushOutJobToWcs(any());
}
} }