feat: add outbound WCS retry dispatch
This commit is contained in:
@@ -86,13 +86,27 @@ public class WcsClient extends BaseExternalClient {
|
||||
}
|
||||
try {
|
||||
String body = objectMapper.writeValueAsString(job);
|
||||
// 正式WCS作业对象仍使用统一接收入口;地址按出库作业所属仓库解析。
|
||||
return post(jobUrl("sendPutawayTask", job), body, "WCS-下发出库任务", this::parseWcsAck, MsgTypeEnum.JSON);
|
||||
return post(jobUrl("sendRetrieveTask", job), body, "WCS-下发出库任务", this::parseWcsAck, MsgTypeEnum.JSON);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.fail("SERIALIZE_FAIL", "出库作业JSON报文生成失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 兼容统一出库命名,实际使用普通出库接口。 */
|
||||
public ApiResponse<String> sendOutboundTask(MydoJob job) {
|
||||
return sendRetrieveTask(job);
|
||||
}
|
||||
|
||||
/** 下发指定货位出库任务。 */
|
||||
public ApiResponse<String> sendLocationOutboundTask(MydoJob job) {
|
||||
return sendJsonJob(job, "sendLocationOutboundTask", "WCS-下发指定货位出库任务", "MOCK_WCS_LOCATION_OUT_OK");
|
||||
}
|
||||
|
||||
/** 下发密集库移库任务。 */
|
||||
public ApiResponse<String> sendRelocationTask(MydoJob job) {
|
||||
return sendJsonJob(job, "sendRelocationTask", "WCS-下发移库任务", "MOCK_WCS_RELOCATION_OK");
|
||||
}
|
||||
|
||||
// ── 接口3:查询设备状态 ───────────────────────────────────
|
||||
|
||||
public ApiResponse<List<Map<String, String>>> queryDeviceStatus(String deviceType) {
|
||||
@@ -161,6 +175,21 @@ public class WcsClient extends BaseExternalClient {
|
||||
return config().getApiUrl(apiKey, job == null ? null : job.getWarehouseId());
|
||||
}
|
||||
|
||||
private ApiResponse<String> sendJsonJob(MydoJob job, String apiKey, String description, String mockResult) {
|
||||
if (job == null) {
|
||||
return ApiResponse.fail("PARAM_ERROR", "作业不能为空");
|
||||
}
|
||||
if (mockEnabled()) {
|
||||
return ApiResponse.ok(mockResult);
|
||||
}
|
||||
try {
|
||||
String body = objectMapper.writeValueAsString(job);
|
||||
return post(jobUrl(apiKey, job), body, description, this::parseWcsAck, MsgTypeEnum.JSON);
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.fail("SERIALIZE_FAIL", "作业JSON报文生成失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String parseOkNg(String rawBody) {
|
||||
String[] parts = rawBody.trim().split("\\|", 2);
|
||||
if ("OK".equals(parts[0])) {
|
||||
|
||||
@@ -23,6 +23,8 @@ import com.ruoyi.in.jobdetail.mapper.MydoJobDetailMapper;
|
||||
import com.ruoyi.in.stockin.mapper.InStockinMapper;
|
||||
import com.ruoyi.out.stockout.mapper.OutStockoutMapper;
|
||||
import com.ruoyi.out.stockoutdetail.mapper.OutStockoutDetailMapper;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import com.ruoyi.pd.diff.mapper.PdDifferenceRecordMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
@@ -78,6 +80,9 @@ public class MydoJobServiceImpl implements IMydoJobService
|
||||
@Autowired
|
||||
private PdDifferenceRecordMapper pdDifferenceRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private IOutboundDispatchService outboundDispatchService;
|
||||
|
||||
/**
|
||||
* 查询作业信息
|
||||
*
|
||||
@@ -309,38 +314,7 @@ public class MydoJobServiceImpl implements IMydoJobService
|
||||
@Override
|
||||
public AjaxResult pushOutJobToWcs(String jobId)
|
||||
{
|
||||
if (StringUtils.isEmpty(jobId)) {
|
||||
return AjaxResult.error("作业ID不能为空");
|
||||
}
|
||||
MydoJob job = mydoJobMapper.selectMydoJobByJobId(jobId);
|
||||
if (job == null) {
|
||||
return AjaxResult.error("出库作业不存在");
|
||||
}
|
||||
if (job.getInOrOut() == null || job.getInOrOut() < 20) {
|
||||
return AjaxResult.error("请选择出库作业");
|
||||
}
|
||||
if (!"1".equals(job.getStatus())) {
|
||||
if ("2".equals(job.getStatus()) || "3".equals(job.getStatus())) {
|
||||
return AjaxResult.success("出库作业已下发,无需重复推送");
|
||||
}
|
||||
return AjaxResult.error("当前作业状态不允许下发WCS");
|
||||
}
|
||||
|
||||
ApiResponse<String> response = wcsClient.sendRetrieveTask(job);
|
||||
if (response == null || !response.isSuccess()) {
|
||||
String errorMessage = response == null ? "WCS接口无返回" : response.getMessage();
|
||||
return AjaxResult.error("WCS出库作业下发失败:" + errorMessage);
|
||||
}
|
||||
|
||||
MydoJob updateJob = new MydoJob();
|
||||
updateJob.setJobId(job.getJobId());
|
||||
updateJob.setStatus("3");
|
||||
updateJob.setExecutionPhase("1");
|
||||
updateJob.setStartTime(DateUtils.getTime());
|
||||
if (mydoJobMapper.updateMydoJob(updateJob) <= 0) {
|
||||
return AjaxResult.error("出库作业状态更新失败");
|
||||
}
|
||||
return AjaxResult.success("WCS出库作业下发成功:" + Convert.toStr(response.getData()));
|
||||
return outboundDispatchService.dispatch(jobId, DispatchTrigger.INITIAL);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
/**
|
||||
* 密集库作业下发资格。
|
||||
*/
|
||||
public class DispatchEligibility
|
||||
{
|
||||
private final boolean eligible;
|
||||
private final String reason;
|
||||
|
||||
private DispatchEligibility(boolean eligible, String reason)
|
||||
{
|
||||
this.eligible = eligible;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public static DispatchEligibility allowed()
|
||||
{
|
||||
return new DispatchEligibility(true, null);
|
||||
}
|
||||
|
||||
public static DispatchEligibility blocked(String reason)
|
||||
{
|
||||
return new DispatchEligibility(false, reason);
|
||||
}
|
||||
|
||||
public boolean isEligible()
|
||||
{
|
||||
return eligible;
|
||||
}
|
||||
|
||||
public String getReason()
|
||||
{
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.out.dense.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 密集库通道查询 Mapper。
|
||||
*/
|
||||
public interface DenseLaneMapper
|
||||
{
|
||||
Integer isCurrentOutermost(@Param("cargoSpaceId") String cargoSpaceId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ruoyi.out.dense.service;
|
||||
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.out.dense.domain.DispatchEligibility;
|
||||
|
||||
/**
|
||||
* 密集库通道服务。
|
||||
*/
|
||||
public interface IDenseLaneService
|
||||
{
|
||||
DispatchEligibility checkDispatchEligibility(MydoJob job);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ruoyi.out.dense.service.impl;
|
||||
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.out.dense.domain.DispatchEligibility;
|
||||
import com.ruoyi.out.dense.mapper.DenseLaneMapper;
|
||||
import com.ruoyi.out.dense.service.IDenseLaneService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 密集库通道服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class DenseLaneServiceImpl implements IDenseLaneService
|
||||
{
|
||||
private final DenseLaneMapper denseLaneMapper;
|
||||
|
||||
public DenseLaneServiceImpl(DenseLaneMapper denseLaneMapper)
|
||||
{
|
||||
this.denseLaneMapper = denseLaneMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchEligibility checkDispatchEligibility(MydoJob job)
|
||||
{
|
||||
if (job == null) {
|
||||
return DispatchEligibility.blocked("出库作业不存在");
|
||||
}
|
||||
if (!"03".equals(job.getWarehouseId())) {
|
||||
return DispatchEligibility.allowed();
|
||||
}
|
||||
if (!StringUtils.hasText(job.getCargoSpaceId())) {
|
||||
return DispatchEligibility.blocked("03仓出库作业未分配源货位");
|
||||
}
|
||||
Integer outermost = denseLaneMapper.isCurrentOutermost(job.getCargoSpaceId());
|
||||
if (Integer.valueOf(1).equals(outermost)) {
|
||||
return DispatchEligibility.allowed();
|
||||
}
|
||||
return DispatchEligibility.blocked("当前货位不是密集库通道最外侧货位");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.out.dispatch.domain;
|
||||
|
||||
/**
|
||||
* WCS 下发触发方式。
|
||||
*/
|
||||
public enum DispatchTrigger
|
||||
{
|
||||
INITIAL,
|
||||
AUTO_RETRY,
|
||||
MANUAL
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.out.dispatch.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
|
||||
/**
|
||||
* 出库作业 WCS 下发服务。
|
||||
*/
|
||||
public interface IOutboundDispatchService
|
||||
{
|
||||
AjaxResult dispatch(String jobId, DispatchTrigger trigger);
|
||||
|
||||
int retryFailedJobs();
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.ruoyi.out.dispatch.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.api.client.WcsClient;
|
||||
import com.ruoyi.api.domain.ApiResponse;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.text.Convert;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.job.mapper.MydoJobMapper;
|
||||
import com.ruoyi.out.dense.domain.DispatchEligibility;
|
||||
import com.ruoyi.out.dense.service.IDenseLaneService;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.domain.WcsJobDispatch;
|
||||
import com.ruoyi.out.dispatch.mapper.WcsJobDispatchMapper;
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 出库作业 WCS 下发服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class OutboundDispatchServiceImpl implements IOutboundDispatchService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(OutboundDispatchServiceImpl.class);
|
||||
private static final int MAX_AUTO_RETRY_COUNT = 5;
|
||||
private static final int RETRY_BATCH_SIZE = 100;
|
||||
private static final long RETRY_DELAY_MILLIS = 60_000L;
|
||||
|
||||
private final MydoJobMapper jobMapper;
|
||||
private final WcsJobDispatchMapper dispatchMapper;
|
||||
private final IDenseLaneService denseLaneService;
|
||||
private final WcsClient wcsClient;
|
||||
|
||||
public OutboundDispatchServiceImpl(MydoJobMapper jobMapper,
|
||||
WcsJobDispatchMapper dispatchMapper,
|
||||
IDenseLaneService denseLaneService,
|
||||
WcsClient wcsClient)
|
||||
{
|
||||
this.jobMapper = jobMapper;
|
||||
this.dispatchMapper = dispatchMapper;
|
||||
this.denseLaneService = denseLaneService;
|
||||
this.wcsClient = wcsClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult dispatch(String jobId, DispatchTrigger trigger)
|
||||
{
|
||||
if (!StringUtils.hasText(jobId)) {
|
||||
return AjaxResult.error("作业ID不能为空");
|
||||
}
|
||||
if (trigger == null) {
|
||||
return AjaxResult.error("WCS下发触发方式不能为空");
|
||||
}
|
||||
|
||||
MydoJob job = jobMapper.selectMydoJobByJobId(jobId.trim());
|
||||
AjaxResult validation = validateJob(job);
|
||||
if (validation != null) {
|
||||
return validation;
|
||||
}
|
||||
|
||||
dispatchMapper.insertIfAbsent(job.getJobId());
|
||||
WcsJobDispatch dispatch = dispatchMapper.selectByJobId(job.getJobId());
|
||||
if (dispatch == null) {
|
||||
return AjaxResult.error("WCS下发记录创建失败");
|
||||
}
|
||||
if (isActiveSending(dispatch)) {
|
||||
return AjaxResult.success("作业正在下发WCS,无需重复处理");
|
||||
}
|
||||
if (trigger == DispatchTrigger.AUTO_RETRY
|
||||
&& retryCount(dispatch) >= MAX_AUTO_RETRY_COUNT) {
|
||||
if (!WcsJobDispatch.EXHAUSTED.equals(dispatch.getDispatchStatus())) {
|
||||
Map<String, Object> exhaustedParams = markParams(
|
||||
job.getJobId(), dispatch, "WCS自动重试次数已达上限", new Date());
|
||||
exhaustedParams.put("autoRetryCount", retryCount(dispatch));
|
||||
dispatchMapper.markExhausted(exhaustedParams);
|
||||
}
|
||||
return AjaxResult.success("WCS自动重试次数已达上限,等待人工下发");
|
||||
}
|
||||
|
||||
DispatchEligibility eligibility = denseLaneService.checkDispatchEligibility(job);
|
||||
if (eligibility == null || !eligibility.isEligible()) {
|
||||
String reason = eligibility == null ? "密集库下发资格校验无返回" : eligibility.getReason();
|
||||
dispatchMapper.markBlocked(markParams(job.getJobId(), dispatch, reason, new Date()));
|
||||
return AjaxResult.success("作业暂未下发WCS:" + reason);
|
||||
}
|
||||
|
||||
DispatchTrigger attemptTrigger = resolveAttemptTrigger(trigger, dispatch);
|
||||
Date attemptTime = new Date();
|
||||
Map<String, Object> claimParams = new HashMap<>();
|
||||
claimParams.put("jobId", job.getJobId());
|
||||
claimParams.put("trigger", attemptTrigger.name());
|
||||
claimParams.put("attemptTime", attemptTime);
|
||||
claimParams.put("versionNo", version(dispatch));
|
||||
if (dispatchMapper.claimForSend(claimParams) != 1) {
|
||||
return AjaxResult.success("作业正在下发或状态已变化,无需重复处理");
|
||||
}
|
||||
int claimedVersion = version(dispatch) + 1;
|
||||
|
||||
ApiResponse<String> response;
|
||||
try {
|
||||
response = sendByType(job);
|
||||
} catch (Exception e) {
|
||||
log.error("WCS出库作业下发异常,jobId={}", job.getJobId(), e);
|
||||
return handleFailure(job, dispatch, claimedVersion, attemptTrigger, e.getMessage());
|
||||
}
|
||||
if (response == null || !response.isSuccess()) {
|
||||
String errorMessage = response == null ? "WCS接口无返回" : response.getMessage();
|
||||
return handleFailure(job, dispatch, claimedVersion, attemptTrigger, errorMessage);
|
||||
}
|
||||
|
||||
MydoJob updateJob = new MydoJob();
|
||||
updateJob.setJobId(job.getJobId());
|
||||
updateJob.setStatus("3");
|
||||
updateJob.setExecutionPhase("1");
|
||||
updateJob.setStartTime(DateUtils.getTime());
|
||||
if (jobMapper.updateMydoJob(updateJob) != 1) {
|
||||
return handleFailure(job, dispatch, claimedVersion, attemptTrigger, "WCS已接收,但出库作业状态更新失败");
|
||||
}
|
||||
|
||||
Map<String, Object> successParams = new HashMap<>();
|
||||
successParams.put("jobId", job.getJobId());
|
||||
successParams.put("successTime", new Date());
|
||||
successParams.put("versionNo", claimedVersion);
|
||||
dispatchMapper.markSuccess(successParams);
|
||||
return AjaxResult.success("WCS出库作业下发成功:" + Convert.toStr(response.getData()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int retryFailedJobs()
|
||||
{
|
||||
List<WcsJobDispatch> candidates = dispatchMapper.selectRetryCandidates(new Date(), RETRY_BATCH_SIZE);
|
||||
int processed = 0;
|
||||
for (WcsJobDispatch candidate : candidates) {
|
||||
try {
|
||||
dispatch(candidate.getJobId(), DispatchTrigger.AUTO_RETRY);
|
||||
} catch (Exception e) {
|
||||
log.error("WCS出库作业自动补偿异常,jobId={}", candidate.getJobId(), e);
|
||||
}
|
||||
processed++;
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
private AjaxResult validateJob(MydoJob job)
|
||||
{
|
||||
if (job == null) {
|
||||
return AjaxResult.error("出库作业不存在");
|
||||
}
|
||||
if (job.getInOrOut() == null || job.getInOrOut() < 20) {
|
||||
return AjaxResult.error("请选择出库作业");
|
||||
}
|
||||
if (!"1".equals(job.getStatus())) {
|
||||
if ("2".equals(job.getStatus()) || "3".equals(job.getStatus())) {
|
||||
return AjaxResult.success("出库作业已下发,无需重复推送");
|
||||
}
|
||||
return AjaxResult.error("当前作业状态不允许下发WCS");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ApiResponse<String> sendByType(MydoJob job)
|
||||
{
|
||||
if (Long.valueOf(20L).equals(job.getInOrOut())) {
|
||||
return wcsClient.sendLocationOutboundTask(job);
|
||||
}
|
||||
if (Long.valueOf(23L).equals(job.getInOrOut())) {
|
||||
return wcsClient.sendRelocationTask(job);
|
||||
}
|
||||
return wcsClient.sendRetrieveTask(job);
|
||||
}
|
||||
|
||||
private AjaxResult handleFailure(MydoJob job, WcsJobDispatch dispatch, int claimedVersion,
|
||||
DispatchTrigger trigger, String errorMessage)
|
||||
{
|
||||
int autoRetryCount = retryCount(dispatch)
|
||||
+ (trigger == DispatchTrigger.AUTO_RETRY ? 1 : 0);
|
||||
String reason = StringUtils.hasText(errorMessage) ? errorMessage : "WCS下发失败";
|
||||
Map<String, Object> params = markParams(job.getJobId(), dispatch, reason, new Date());
|
||||
params.put("autoRetryCount", autoRetryCount);
|
||||
params.put("versionNo", claimedVersion);
|
||||
|
||||
if (trigger == DispatchTrigger.MANUAL || autoRetryCount >= MAX_AUTO_RETRY_COUNT) {
|
||||
dispatchMapper.markExhausted(params);
|
||||
if (trigger == DispatchTrigger.MANUAL) {
|
||||
return AjaxResult.error("WCS手动下发失败:" + reason);
|
||||
}
|
||||
return AjaxResult.success("WCS自动重试已达5次,等待人工下发:" + reason);
|
||||
}
|
||||
|
||||
params.put("nextRetryTime", new Date(System.currentTimeMillis() + RETRY_DELAY_MILLIS));
|
||||
dispatchMapper.markRetryWait(params);
|
||||
return AjaxResult.success("出库作业已生成,WCS下发失败,已进入补偿队列:" + reason);
|
||||
}
|
||||
|
||||
private Map<String, Object> markParams(String jobId, WcsJobDispatch dispatch,
|
||||
String lastError, Date updateTime)
|
||||
{
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("jobId", jobId);
|
||||
params.put("lastError", lastError);
|
||||
params.put("updateTime", updateTime);
|
||||
params.put("versionNo", version(dispatch));
|
||||
params.put("autoRetryCount", retryCount(dispatch));
|
||||
params.put("nextRetryTime", new Date(updateTime.getTime() + RETRY_DELAY_MILLIS));
|
||||
return params;
|
||||
}
|
||||
|
||||
private int retryCount(WcsJobDispatch dispatch)
|
||||
{
|
||||
return dispatch.getAutoRetryCount() == null ? 0 : dispatch.getAutoRetryCount();
|
||||
}
|
||||
|
||||
private DispatchTrigger resolveAttemptTrigger(DispatchTrigger requestedTrigger, WcsJobDispatch dispatch)
|
||||
{
|
||||
if (requestedTrigger == DispatchTrigger.AUTO_RETRY && dispatch.getLastAttemptTime() == null) {
|
||||
return DispatchTrigger.INITIAL;
|
||||
}
|
||||
return requestedTrigger;
|
||||
}
|
||||
|
||||
private boolean isActiveSending(WcsJobDispatch dispatch)
|
||||
{
|
||||
return WcsJobDispatch.SENDING.equals(dispatch.getDispatchStatus())
|
||||
&& dispatch.getLastAttemptTime() != null
|
||||
&& dispatch.getLastAttemptTime().getTime() > System.currentTimeMillis() - 5 * 60_000L;
|
||||
}
|
||||
|
||||
private int version(WcsJobDispatch dispatch)
|
||||
{
|
||||
return dispatch.getVersionNo() == null ? 0 : dispatch.getVersionNo();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.out.dispatch.task;
|
||||
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* WCS 出库作业失败补偿任务。
|
||||
*/
|
||||
@Component("wcsOutboundRetryTask")
|
||||
public class WcsOutboundRetryTask
|
||||
{
|
||||
private final IOutboundDispatchService outboundDispatchService;
|
||||
|
||||
public WcsOutboundRetryTask(IOutboundDispatchService outboundDispatchService)
|
||||
{
|
||||
this.outboundDispatchService = outboundDispatchService;
|
||||
}
|
||||
|
||||
public int retryFailedJobs()
|
||||
{
|
||||
return outboundDispatchService.retryFailedJobs();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.job.service.IMydoJobService;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -37,6 +39,9 @@ public class OutMydoJobController extends BaseController
|
||||
@Autowired
|
||||
private IMydoJobService mydoJobService;
|
||||
|
||||
@Autowired
|
||||
private IOutboundDispatchService outboundDispatchService;
|
||||
|
||||
/**
|
||||
* 查询作业信息列表
|
||||
*/
|
||||
@@ -147,11 +152,11 @@ public class OutMydoJobController extends BaseController
|
||||
/**
|
||||
* 推送出库作业到 WCS
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('out:job:edit')")
|
||||
@PreAuthorize("@ss.hasPermi('out:job:pushWcs')")
|
||||
@Log(title = "作业信息-WCS推送", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/pushWcs/{jobId}")
|
||||
public AjaxResult pushWcs(@PathVariable("jobId") String jobId)
|
||||
{
|
||||
return mydoJobService.pushOutJobToWcs(jobId);
|
||||
return outboundDispatchService.dispatch(jobId, DispatchTrigger.MANUAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,8 +176,10 @@ external:
|
||||
'02': ${WCS_02_BASE_URL:${WCS_DEFAULT_BASE_URL:http://172.16.4.63:8084}}
|
||||
'03': ${WCS_03_BASE_URL:${WCS_DEFAULT_BASE_URL:http://172.16.4.63:8084}}
|
||||
apis:
|
||||
sendPutawayTask: /wcs/api/receiveWmsJob
|
||||
sendRetrieveTask: /wcs/api/receiveWmsJob
|
||||
sendPutawayTask: ${WCS_PUTAWAY_PATH:/wcs/api/receiveWmsJob}
|
||||
sendRetrieveTask: ${WCS_OUTBOUND_PATH:/wcs/api/receiveWmsOutJob}
|
||||
sendLocationOutboundTask: ${WCS_LOCATION_OUTBOUND_PATH:/wcs/api/receiveWmsLocationOutJob}
|
||||
sendRelocationTask: ${WCS_RELOCATION_PATH:/wcs/api/receiveWmsMoveJob}
|
||||
queryDeviceStatus: /device/status
|
||||
sendCraneMove: /crane/move
|
||||
cancelTask: /task/cancel
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.out.dense.mapper.DenseLaneMapper">
|
||||
|
||||
<select id="isCurrentOutermost" resultType="java.lang.Integer">
|
||||
select case
|
||||
when source_space.warehouse_id != '03'
|
||||
or source_space.group_coding is null
|
||||
or ltrim(rtrim(source_space.group_coding)) = '' then 1
|
||||
when not exists
|
||||
(
|
||||
select 1
|
||||
from bas_cargo_space occupied
|
||||
cross apply
|
||||
(
|
||||
select min(lane_space.the_column) as min_column,
|
||||
max(lane_space.the_column) as max_column
|
||||
from bas_cargo_space lane_space
|
||||
where lane_space.warehouse_id = source_space.warehouse_id
|
||||
and lane_space.group_coding = source_space.group_coding
|
||||
and lane_space.the_floor = source_space.the_floor
|
||||
) lane_boundary
|
||||
where occupied.warehouse_id = source_space.warehouse_id
|
||||
and occupied.group_coding = source_space.group_coding
|
||||
and occupied.the_floor = source_space.the_floor
|
||||
and occupied.cargo_space_id != source_space.cargo_space_id
|
||||
and occupied.status in ('2', '3', '4')
|
||||
and occupied.tray_code is not null
|
||||
and ltrim(rtrim(occupied.tray_code)) != ''
|
||||
and
|
||||
(
|
||||
(isnull(area_config.direction_type, '1') = '1'
|
||||
and occupied.the_column > source_space.the_column)
|
||||
or (area_config.direction_type = '2'
|
||||
and occupied.the_column < source_space.the_column)
|
||||
or (area_config.direction_type = '3'
|
||||
and case
|
||||
when occupied.the_column - lane_boundary.min_column
|
||||
<= lane_boundary.max_column - occupied.the_column
|
||||
then occupied.the_column - lane_boundary.min_column
|
||||
else lane_boundary.max_column - occupied.the_column
|
||||
end
|
||||
< case
|
||||
when source_space.the_column - lane_boundary.min_column
|
||||
<= lane_boundary.max_column - source_space.the_column
|
||||
then source_space.the_column - lane_boundary.min_column
|
||||
else lane_boundary.max_column - source_space.the_column
|
||||
end)
|
||||
)
|
||||
) then 1
|
||||
else 0
|
||||
end
|
||||
from bas_cargo_space source_space
|
||||
left join bas_cargo_area_config area_config
|
||||
on area_config.warehouse_id = source_space.warehouse_id
|
||||
and area_config.cargo_area_code = source_space.this_cargo_area
|
||||
and area_config.is_active = '1'
|
||||
where source_space.cargo_space_id = #{cargoSpaceId}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -60,6 +60,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where job_id = #{jobId}
|
||||
and version_no = #{versionNo}
|
||||
and dispatch_status in ('PENDING', 'BLOCKED', 'RETRY_WAIT', 'EXHAUSTED', 'SENDING')
|
||||
and (dispatch_status != 'SENDING'
|
||||
or last_attempt_time <= dateadd(minute, -5, #{attemptTime}))
|
||||
and (#{trigger} != 'AUTO_RETRY' or auto_retry_count < 5)
|
||||
</update>
|
||||
|
||||
@@ -67,27 +69,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
update wcs_job_dispatch
|
||||
set dispatch_status = 'BLOCKED', last_error = #{lastError}, next_retry_time = #{nextRetryTime},
|
||||
update_time = #{updateTime}, version_no = version_no + 1
|
||||
where job_id = #{jobId} and dispatch_status != 'SUCCESS'
|
||||
where job_id = #{jobId}
|
||||
and version_no = #{versionNo}
|
||||
and dispatch_status != 'SUCCESS'
|
||||
</update>
|
||||
|
||||
<update id="markRetryWait" parameterType="map">
|
||||
update wcs_job_dispatch
|
||||
set dispatch_status = 'RETRY_WAIT', last_error = #{lastError}, next_retry_time = #{nextRetryTime},
|
||||
update_time = #{updateTime}, version_no = version_no + 1
|
||||
where job_id = #{jobId} and dispatch_status = 'SENDING'
|
||||
where job_id = #{jobId}
|
||||
and version_no = #{versionNo}
|
||||
and dispatch_status = 'SENDING'
|
||||
</update>
|
||||
|
||||
<update id="markSuccess" parameterType="map">
|
||||
update wcs_job_dispatch
|
||||
set dispatch_status = 'SUCCESS', last_error = null, next_retry_time = null,
|
||||
success_time = #{successTime}, update_time = #{successTime}, version_no = version_no + 1
|
||||
where job_id = #{jobId} and dispatch_status = 'SENDING'
|
||||
where job_id = #{jobId}
|
||||
and version_no = #{versionNo}
|
||||
and dispatch_status = 'SENDING'
|
||||
</update>
|
||||
|
||||
<update id="markExhausted" parameterType="map">
|
||||
update wcs_job_dispatch
|
||||
set dispatch_status = 'EXHAUSTED', last_error = #{lastError}, next_retry_time = null,
|
||||
update_time = #{updateTime}, version_no = version_no + 1
|
||||
where job_id = #{jobId} and dispatch_status = 'SENDING'
|
||||
where job_id = #{jobId}
|
||||
and version_no = #{versionNo}
|
||||
and dispatch_status in ('SENDING', 'BLOCKED', 'RETRY_WAIT')
|
||||
</update>
|
||||
</mapper>
|
||||
|
||||
@@ -35,6 +35,10 @@ class WcsClientTest {
|
||||
.isEqualTo("MOCK_WCS_RETRIEVE_OK");
|
||||
assertThat(client.sendRetrieveTask(outboundJob).getData())
|
||||
.isEqualTo("MOCK_WCS_RETRIEVE_OK");
|
||||
assertThat(client.sendLocationOutboundTask(outboundJob).getData())
|
||||
.isEqualTo("MOCK_WCS_LOCATION_OUT_OK");
|
||||
assertThat(client.sendRelocationTask(outboundJob).getData())
|
||||
.isEqualTo("MOCK_WCS_RELOCATION_OK");
|
||||
assertThat(client.sendCraneMove("CR01", "A0101", "A0102", "MOVE").getData())
|
||||
.isEqualTo("MOCK_WCS_CRANE_MOVE_OK");
|
||||
assertThat(client.cancelTask("TASK001").getData())
|
||||
@@ -64,6 +68,10 @@ class WcsClientTest {
|
||||
.isEqualTo("ACCEPTED");
|
||||
assertThat(client.sendRetrieveTask(outboundJob).getData())
|
||||
.isEqualTo("ACCEPTED");
|
||||
assertThat(client.sendLocationOutboundTask(outboundJob).getData())
|
||||
.isEqualTo("ACCEPTED");
|
||||
assertThat(client.sendRelocationTask(outboundJob).getData())
|
||||
.isEqualTo("ACCEPTED");
|
||||
assertThat(client.queryDeviceStatus("CRANE").getData().get(0).get("status"))
|
||||
.isEqualTo("ONLINE");
|
||||
assertThat(client.sendCraneMove("CR01", "A0101", "A0102", "MOVE").getData())
|
||||
@@ -75,7 +83,9 @@ class WcsClientTest {
|
||||
"http://wcs/wcs/api/receiveWmsJob",
|
||||
"http://wcs/wcs/api/receiveWmsJob",
|
||||
"http://wcs/task/dispatch",
|
||||
"http://wcs/wcs/api/receiveWmsJob",
|
||||
"http://wcs/task/dispatch",
|
||||
"http://wcs/location/dispatch",
|
||||
"http://wcs/relocation/dispatch",
|
||||
"http://wcs/device/status",
|
||||
"http://wcs/crane/move",
|
||||
"http://wcs/task/cancel");
|
||||
@@ -84,6 +94,8 @@ class WcsClientTest {
|
||||
MsgTypeEnum.JSON,
|
||||
MsgTypeEnum.TEXT,
|
||||
MsgTypeEnum.JSON,
|
||||
MsgTypeEnum.JSON,
|
||||
MsgTypeEnum.JSON,
|
||||
MsgTypeEnum.TEXT,
|
||||
MsgTypeEnum.TEXT,
|
||||
MsgTypeEnum.TEXT);
|
||||
@@ -219,6 +231,8 @@ class WcsClientTest {
|
||||
Map<String, String> apis = new HashMap<>();
|
||||
apis.put("sendPutawayTask", "/wcs/api/receiveWmsJob");
|
||||
apis.put("sendRetrieveTask", "/task/dispatch");
|
||||
apis.put("sendLocationOutboundTask", "/location/dispatch");
|
||||
apis.put("sendRelocationTask", "/relocation/dispatch");
|
||||
apis.put("queryDeviceStatus", "/device/status");
|
||||
apis.put("sendCraneMove", "/crane/move");
|
||||
apis.put("cancelTask", "/task/cancel");
|
||||
|
||||
@@ -20,6 +20,8 @@ import com.ruoyi.in.stockin.domain.InStockin;
|
||||
import com.ruoyi.in.stockin.mapper.InStockinMapper;
|
||||
import com.ruoyi.out.stockout.mapper.OutStockoutMapper;
|
||||
import com.ruoyi.out.stockoutdetail.mapper.OutStockoutDetailMapper;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import com.ruoyi.pd.diff.mapper.PdDifferenceRecordMapper;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -213,6 +215,9 @@ class MydoJobServiceImplTest {
|
||||
@Mock
|
||||
private PdDifferenceRecordMapper pdDifferenceRecordMapper;
|
||||
|
||||
@Mock
|
||||
private IOutboundDispatchService outboundDispatchService;
|
||||
|
||||
@InjectMocks
|
||||
private MydoJobServiceImpl service;
|
||||
|
||||
@@ -257,57 +262,16 @@ class MydoJobServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushOutJobToWcsRejectsInboundJob() {
|
||||
MydoJob job = new MydoJob();
|
||||
job.setJobId("RK001");
|
||||
job.setStatus("1");
|
||||
job.setInOrOut(1L);
|
||||
when(mydoJobMapper.selectMydoJobByJobId("RK001")).thenReturn(job);
|
||||
|
||||
AjaxResult result = service.pushOutJobToWcs("RK001");
|
||||
|
||||
assertThat(result.isError()).isTrue();
|
||||
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("请选择出库作业");
|
||||
verifyNoInteractions(wcsClient);
|
||||
verify(mydoJobMapper, never()).updateMydoJob(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushOutJobToWcsDoesNotSendAlreadyPushedJobAgain() {
|
||||
MydoJob job = new MydoJob();
|
||||
job.setJobId("CK001");
|
||||
job.setStatus("2");
|
||||
job.setInOrOut(20L);
|
||||
when(mydoJobMapper.selectMydoJobByJobId("CK001")).thenReturn(job);
|
||||
|
||||
AjaxResult result = service.pushOutJobToWcs("CK001");
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("出库作业已下发,无需重复推送");
|
||||
verifyNoInteractions(wcsClient);
|
||||
verify(mydoJobMapper, never()).updateMydoJob(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushOutJobToWcsMarksPendingOutJobAsPushedAfterWcsAcceptsIt() {
|
||||
MydoJob job = new MydoJob();
|
||||
job.setJobId("CK002");
|
||||
job.setStatus("1");
|
||||
job.setInOrOut(20L);
|
||||
when(mydoJobMapper.selectMydoJobByJobId("CK002")).thenReturn(job);
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.ok("OK"));
|
||||
when(mydoJobMapper.updateMydoJob(any(MydoJob.class))).thenReturn(1);
|
||||
void pushOutJobToWcsDelegatesInitialDispatchForExistingBusinessCallers() {
|
||||
AjaxResult expected = AjaxResult.success("WCS出库作业下发成功:OK");
|
||||
when(outboundDispatchService.dispatch("CK002", DispatchTrigger.INITIAL)).thenReturn(expected);
|
||||
|
||||
AjaxResult result = service.pushOutJobToWcs("CK002");
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
ArgumentCaptor<MydoJob> updateCaptor = ArgumentCaptor.forClass(MydoJob.class);
|
||||
verify(mydoJobMapper).updateMydoJob(updateCaptor.capture());
|
||||
MydoJob update = updateCaptor.getValue();
|
||||
assertThat(update.getJobId()).isEqualTo("CK002");
|
||||
assertThat(update.getStatus()).isEqualTo("3");
|
||||
assertThat(update.getExecutionPhase()).isEqualTo("1");
|
||||
assertThat(update.getStartTime()).isNotBlank();
|
||||
assertThat(result).isSameAs(expected);
|
||||
verify(outboundDispatchService).dispatch("CK002", DispatchTrigger.INITIAL);
|
||||
verifyNoInteractions(mydoJobMapper);
|
||||
verifyNoInteractions(wcsClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -26,6 +26,7 @@ class WcsJobDispatchMapperSqlTest
|
||||
.contains("EXHAUSTED")
|
||||
.contains("auto_retry_count < 5")
|
||||
.contains("dispatch_status = 'SENDING'")
|
||||
.contains("last_attempt_time <= dateadd(minute, -5")
|
||||
.contains("version_no = version_no + 1")
|
||||
.contains("selectRetryCandidates")
|
||||
.contains("claimForSend")
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.ruoyi.out.dispatch.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.api.client.WcsClient;
|
||||
import com.ruoyi.api.domain.ApiResponse;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.job.mapper.MydoJobMapper;
|
||||
import com.ruoyi.out.dense.domain.DispatchEligibility;
|
||||
import com.ruoyi.out.dense.service.IDenseLaneService;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.domain.WcsJobDispatch;
|
||||
import com.ruoyi.out.dispatch.mapper.WcsJobDispatchMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class OutboundDispatchServiceImplTest
|
||||
{
|
||||
private MydoJobMapper jobMapper;
|
||||
private WcsJobDispatchMapper dispatchMapper;
|
||||
private IDenseLaneService denseLaneService;
|
||||
private WcsClient wcsClient;
|
||||
private OutboundDispatchServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
jobMapper = mock(MydoJobMapper.class);
|
||||
dispatchMapper = mock(WcsJobDispatchMapper.class);
|
||||
denseLaneService = mock(IDenseLaneService.class);
|
||||
wcsClient = mock(WcsClient.class);
|
||||
service = new OutboundDispatchServiceImpl(jobMapper, dispatchMapper, denseLaneService, wcsClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void initialFailureKeepsBusinessSuccessAndStartsRetryCountAtZero()
|
||||
{
|
||||
MydoJob job = jobOfType("CK1", 21L);
|
||||
stubClaim(job, dispatch("CK1", WcsJobDispatch.PENDING, 0));
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.fail("WCS_ERROR", "连接失败"));
|
||||
|
||||
AjaxResult result = service.dispatch("CK1", DispatchTrigger.INITIAL);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(dispatchMapper).markRetryWait(argThat(params ->
|
||||
"CK1".equals(params.get("jobId"))
|
||||
&& Integer.valueOf(0).equals(params.get("autoRetryCount"))
|
||||
&& String.valueOf(params.get("lastError")).contains("连接失败")));
|
||||
verify(jobMapper, never()).updateMydoJob(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fifthAutomaticRetryFailureBecomesExhausted()
|
||||
{
|
||||
MydoJob job = jobOfType("CK2", 21L);
|
||||
stubClaim(job, dispatch("CK2", WcsJobDispatch.RETRY_WAIT, 4));
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.fail("WCS_ERROR", "仍然失败"));
|
||||
|
||||
AjaxResult result = service.dispatch("CK2", DispatchTrigger.AUTO_RETRY);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(dispatchMapper).claimForSend(argThat(params ->
|
||||
DispatchTrigger.AUTO_RETRY.name().equals(params.get("trigger"))));
|
||||
verify(dispatchMapper).markExhausted(argThat(params ->
|
||||
Integer.valueOf(5).equals(params.get("autoRetryCount"))));
|
||||
verify(dispatchMapper, never()).markRetryWait(any(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void automaticRetryBelowLimitReturnsToRetryWait()
|
||||
{
|
||||
MydoJob job = jobOfType("CK3", 21L);
|
||||
stubClaim(job, dispatch("CK3", WcsJobDispatch.RETRY_WAIT, 3));
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.fail("WCS_ERROR", "暂时失败"));
|
||||
|
||||
service.dispatch("CK3", DispatchTrigger.AUTO_RETRY);
|
||||
|
||||
verify(dispatchMapper).markRetryWait(argThat(params ->
|
||||
Integer.valueOf(4).equals(params.get("autoRetryCount"))));
|
||||
verify(dispatchMapper, never()).markExhausted(any(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleSendingRecordAtRetryLimitIsMarkedExhaustedWithoutCallingWcs()
|
||||
{
|
||||
MydoJob job = jobOfType("CK-STALE", 21L);
|
||||
WcsJobDispatch dispatch = dispatch("CK-STALE", WcsJobDispatch.SENDING, 5);
|
||||
dispatch.setLastAttemptTime(new java.util.Date(System.currentTimeMillis() - 6 * 60_000L));
|
||||
when(jobMapper.selectMydoJobByJobId("CK-STALE")).thenReturn(job);
|
||||
when(dispatchMapper.selectByJobId("CK-STALE")).thenReturn(dispatch);
|
||||
|
||||
AjaxResult result = service.dispatch("CK-STALE", DispatchTrigger.AUTO_RETRY);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(dispatchMapper).markExhausted(argThat(params ->
|
||||
Integer.valueOf(5).equals(params.get("autoRetryCount"))));
|
||||
verify(wcsClient, never()).sendRetrieveTask(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualDispatchCanRecoverAnExhaustedJob()
|
||||
{
|
||||
MydoJob job = jobOfType("CK4", 21L);
|
||||
stubClaim(job, dispatch("CK4", WcsJobDispatch.EXHAUSTED, 5));
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.ok("OK"));
|
||||
when(jobMapper.updateMydoJob(any(MydoJob.class))).thenReturn(1);
|
||||
|
||||
AjaxResult result = service.dispatch("CK4", DispatchTrigger.MANUAL);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(dispatchMapper).claimForSend(argThat(params ->
|
||||
DispatchTrigger.MANUAL.name().equals(params.get("trigger"))));
|
||||
verify(dispatchMapper).markSuccess(any(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulDispatchUpdatesBusinessJobOnlyAfterWcsAccepts()
|
||||
{
|
||||
MydoJob job = jobOfType("CK5", 21L);
|
||||
stubClaim(job, dispatch("CK5", WcsJobDispatch.PENDING, 0));
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.ok("WCS-OK"));
|
||||
when(jobMapper.updateMydoJob(any(MydoJob.class))).thenReturn(1);
|
||||
|
||||
AjaxResult result = service.dispatch("CK5", DispatchTrigger.INITIAL);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
ArgumentCaptor<MydoJob> updateCaptor = ArgumentCaptor.forClass(MydoJob.class);
|
||||
verify(jobMapper).updateMydoJob(updateCaptor.capture());
|
||||
assertThat(updateCaptor.getValue().getJobId()).isEqualTo("CK5");
|
||||
assertThat(updateCaptor.getValue().getStatus()).isEqualTo("3");
|
||||
assertThat(updateCaptor.getValue().getExecutionPhase()).isEqualTo("1");
|
||||
assertThat(updateCaptor.getValue().getStartTime()).isNotBlank();
|
||||
verify(dispatchMapper).markSuccess(any(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectedAtomicClaimDoesNotCallWcs()
|
||||
{
|
||||
MydoJob job = jobOfType("CK6", 21L);
|
||||
when(jobMapper.selectMydoJobByJobId("CK6")).thenReturn(job);
|
||||
when(dispatchMapper.selectByJobId("CK6"))
|
||||
.thenReturn(dispatch("CK6", WcsJobDispatch.RETRY_WAIT, 1));
|
||||
when(denseLaneService.checkDispatchEligibility(job)).thenReturn(DispatchEligibility.allowed());
|
||||
when(dispatchMapper.claimForSend(any(Map.class))).thenReturn(0);
|
||||
|
||||
AjaxResult result = service.dispatch("CK6", DispatchTrigger.AUTO_RETRY);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(wcsClient, never()).sendRetrieveTask(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeSendingRecordCannotBeClaimedAgain()
|
||||
{
|
||||
MydoJob job = jobOfType("CK-SENDING", 21L);
|
||||
WcsJobDispatch dispatch = dispatch("CK-SENDING", WcsJobDispatch.SENDING, 1);
|
||||
dispatch.setLastAttemptTime(new java.util.Date());
|
||||
when(jobMapper.selectMydoJobByJobId("CK-SENDING")).thenReturn(job);
|
||||
when(dispatchMapper.selectByJobId("CK-SENDING")).thenReturn(dispatch);
|
||||
when(denseLaneService.checkDispatchEligibility(job)).thenReturn(DispatchEligibility.allowed());
|
||||
when(dispatchMapper.claimForSend(any(Map.class))).thenReturn(1);
|
||||
|
||||
AjaxResult result = service.dispatch("CK-SENDING", DispatchTrigger.MANUAL);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(dispatchMapper, never()).claimForSend(any(Map.class));
|
||||
verify(wcsClient, never()).sendRetrieveTask(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blockedDenseJobDoesNotConsumeRetryOrCallWcs()
|
||||
{
|
||||
MydoJob job = jobOfType("CK7", 21L);
|
||||
when(jobMapper.selectMydoJobByJobId("CK7")).thenReturn(job);
|
||||
when(dispatchMapper.selectByJobId("CK7"))
|
||||
.thenReturn(dispatch("CK7", WcsJobDispatch.RETRY_WAIT, 2));
|
||||
when(denseLaneService.checkDispatchEligibility(job))
|
||||
.thenReturn(DispatchEligibility.blocked("前方仍有2个托盘"));
|
||||
|
||||
AjaxResult result = service.dispatch("CK7", DispatchTrigger.AUTO_RETRY);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(dispatchMapper).markBlocked(argThat(params ->
|
||||
"前方仍有2个托盘".equals(params.get("lastError"))));
|
||||
verify(dispatchMapper, never()).claimForSend(any(Map.class));
|
||||
verify(wcsClient, never()).sendRetrieveTask(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstActualSendAfterBlockedStateDoesNotCountAsAutomaticRetry()
|
||||
{
|
||||
MydoJob job = jobOfType("CK-BLOCKED-FIRST", 21L);
|
||||
WcsJobDispatch dispatch = dispatch("CK-BLOCKED-FIRST", WcsJobDispatch.BLOCKED, 0);
|
||||
stubClaim(job, dispatch);
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.fail("WCS_ERROR", "首次调用失败"));
|
||||
|
||||
AjaxResult result = service.dispatch("CK-BLOCKED-FIRST", DispatchTrigger.AUTO_RETRY);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(dispatchMapper).claimForSend(argThat(params ->
|
||||
DispatchTrigger.INITIAL.name().equals(params.get("trigger"))));
|
||||
verify(dispatchMapper).markRetryWait(argThat(params ->
|
||||
Integer.valueOf(0).equals(params.get("autoRetryCount"))));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(longs = {20L, 21L, 23L})
|
||||
void routesOutboundByJobType(long jobType)
|
||||
{
|
||||
String jobId = "CK-TYPE-" + jobType;
|
||||
MydoJob job = jobOfType(jobId, jobType);
|
||||
stubClaim(job, dispatch(jobId, WcsJobDispatch.PENDING, 0));
|
||||
when(jobMapper.updateMydoJob(any(MydoJob.class))).thenReturn(1);
|
||||
when(wcsClient.sendLocationOutboundTask(job)).thenReturn(ApiResponse.ok("LOCATION-OK"));
|
||||
when(wcsClient.sendRetrieveTask(job)).thenReturn(ApiResponse.ok("RETRIEVE-OK"));
|
||||
when(wcsClient.sendRelocationTask(job)).thenReturn(ApiResponse.ok("MOVE-OK"));
|
||||
|
||||
service.dispatch(jobId, DispatchTrigger.INITIAL);
|
||||
|
||||
if (jobType == 20L) {
|
||||
verify(wcsClient).sendLocationOutboundTask(job);
|
||||
verify(wcsClient, never()).sendRetrieveTask(job);
|
||||
verify(wcsClient, never()).sendRelocationTask(job);
|
||||
} else if (jobType == 23L) {
|
||||
verify(wcsClient).sendRelocationTask(job);
|
||||
verify(wcsClient, never()).sendRetrieveTask(job);
|
||||
verify(wcsClient, never()).sendLocationOutboundTask(job);
|
||||
} else {
|
||||
verify(wcsClient).sendRetrieveTask(job);
|
||||
verify(wcsClient, never()).sendLocationOutboundTask(job);
|
||||
verify(wcsClient, never()).sendRelocationTask(job);
|
||||
}
|
||||
reset(wcsClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryTaskDispatchesEveryCandidateAsAutomaticRetry()
|
||||
{
|
||||
WcsJobDispatch first = dispatch("CK8", WcsJobDispatch.RETRY_WAIT, 1);
|
||||
WcsJobDispatch second = dispatch("CK9", WcsJobDispatch.BLOCKED, 0);
|
||||
when(dispatchMapper.selectRetryCandidates(any(), any(Integer.class)))
|
||||
.thenReturn(List.of(first, second));
|
||||
MydoJob firstJob = jobOfType("CK8", 21L);
|
||||
MydoJob secondJob = jobOfType("CK9", 21L);
|
||||
when(jobMapper.selectMydoJobByJobId("CK8")).thenReturn(firstJob);
|
||||
when(jobMapper.selectMydoJobByJobId("CK9")).thenReturn(secondJob);
|
||||
when(dispatchMapper.selectByJobId("CK8")).thenReturn(first);
|
||||
when(dispatchMapper.selectByJobId("CK9")).thenReturn(second);
|
||||
when(denseLaneService.checkDispatchEligibility(any(MydoJob.class)))
|
||||
.thenReturn(DispatchEligibility.allowed());
|
||||
when(dispatchMapper.claimForSend(any(Map.class))).thenReturn(0);
|
||||
|
||||
int processed = service.retryFailedJobs();
|
||||
|
||||
assertThat(processed).isEqualTo(2);
|
||||
verify(dispatchMapper).claimForSend(argThat(params -> "CK8".equals(params.get("jobId"))));
|
||||
verify(dispatchMapper).claimForSend(argThat(params -> "CK9".equals(params.get("jobId"))));
|
||||
}
|
||||
|
||||
private void stubClaim(MydoJob job, WcsJobDispatch dispatch)
|
||||
{
|
||||
when(jobMapper.selectMydoJobByJobId(job.getJobId())).thenReturn(job);
|
||||
when(dispatchMapper.selectByJobId(job.getJobId())).thenReturn(dispatch);
|
||||
when(denseLaneService.checkDispatchEligibility(job)).thenReturn(DispatchEligibility.allowed());
|
||||
when(dispatchMapper.claimForSend(any(Map.class))).thenReturn(1);
|
||||
}
|
||||
|
||||
private MydoJob jobOfType(String jobId, Long inOrOut)
|
||||
{
|
||||
MydoJob job = new MydoJob();
|
||||
job.setJobId(jobId);
|
||||
job.setInOrOut(inOrOut);
|
||||
job.setStatus("1");
|
||||
job.setExecutionPhase("0");
|
||||
job.setWarehouseId("03");
|
||||
job.setCargoSpaceId("CS-" + jobId);
|
||||
return job;
|
||||
}
|
||||
|
||||
private WcsJobDispatch dispatch(String jobId, String status, int autoRetryCount)
|
||||
{
|
||||
WcsJobDispatch dispatch = new WcsJobDispatch();
|
||||
dispatch.setJobId(jobId);
|
||||
dispatch.setDispatchStatus(status);
|
||||
dispatch.setAutoRetryCount(autoRetryCount);
|
||||
dispatch.setManualRetryCount(0);
|
||||
dispatch.setVersionNo(0);
|
||||
if (!WcsJobDispatch.PENDING.equals(status) && !WcsJobDispatch.BLOCKED.equals(status)) {
|
||||
dispatch.setLastAttemptTime(new java.util.Date());
|
||||
}
|
||||
return dispatch;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user